Файловый менеджер - Редактировать - /home/infrafs/INFRABIKEIT/wp-content/plugins/woocommerce.tar
Назад
vendor/symfony/css-selector/Parser/Parser.php 0000644 00000030523 15132754523 0015366 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer; /** * CSS selector parser. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Parser implements ParserInterface { /** * @var Tokenizer */ private $tokenizer; /** * Constructor. * * @param null|Tokenizer $tokenizer */ public function __construct(Tokenizer $tokenizer = null) { $this->tokenizer = $tokenizer ?: new Tokenizer(); } /** * {@inheritdoc} */ public function parse($source) { $reader = new Reader($source); $stream = $this->tokenizer->tokenize($reader); return $this->parseSelectorList($stream); } /** * Parses the arguments for ":nth-child()" and friends. * * @param Token[] $tokens * * @return array * * @throws SyntaxErrorException */ public static function parseSeries(array $tokens) { foreach ($tokens as $token) { if ($token->isString()) { throw SyntaxErrorException::stringAsFunctionArgument(); } } $joined = trim(implode('', array_map(function (Token $token) { return $token->getValue(); }, $tokens))); $int = function ($string) { if (!is_numeric($string)) { throw SyntaxErrorException::stringAsFunctionArgument(); } return (int) $string; }; switch (true) { case 'odd' === $joined: return array(2, 1); case 'even' === $joined: return array(2, 0); case 'n' === $joined: return array(1, 0); case false === strpos($joined, 'n'): return array(0, $int($joined)); } $split = explode('n', $joined); $first = isset($split[0]) ? $split[0] : null; return array( $first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1, isset($split[1]) && $split[1] ? $int($split[1]) : 0, ); } /** * Parses selector nodes. * * @param TokenStream $stream * * @return array */ private function parseSelectorList(TokenStream $stream) { $stream->skipWhitespace(); $selectors = array(); while (true) { $selectors[] = $this->parserSelectorNode($stream); if ($stream->getPeek()->isDelimiter(array(','))) { $stream->getNext(); $stream->skipWhitespace(); } else { break; } } return $selectors; } /** * Parses next selector or combined node. * * @param TokenStream $stream * * @return Node\SelectorNode * * @throws SyntaxErrorException */ private function parserSelectorNode(TokenStream $stream) { list($result, $pseudoElement) = $this->parseSimpleSelector($stream); while (true) { $stream->skipWhitespace(); $peek = $stream->getPeek(); if ($peek->isFileEnd() || $peek->isDelimiter(array(','))) { break; } if (null !== $pseudoElement) { throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector'); } if ($peek->isDelimiter(array('+', '>', '~'))) { $combinator = $stream->getNext()->getValue(); $stream->skipWhitespace(); } else { $combinator = ' '; } list($nextSelector, $pseudoElement) = $this->parseSimpleSelector($stream); $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector); } return new Node\SelectorNode($result, $pseudoElement); } /** * Parses next simple node (hash, class, pseudo, negation). * * @param TokenStream $stream * @param bool $insideNegation * * @return array * * @throws SyntaxErrorException */ private function parseSimpleSelector(TokenStream $stream, $insideNegation = false) { $stream->skipWhitespace(); $selectorStart = count($stream->getUsed()); $result = $this->parseElementNode($stream); $pseudoElement = null; while (true) { $peek = $stream->getPeek(); if ($peek->isWhitespace() || $peek->isFileEnd() || $peek->isDelimiter(array(',', '+', '>', '~')) || ($insideNegation && $peek->isDelimiter(array(')'))) ) { break; } if (null !== $pseudoElement) { throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector'); } if ($peek->isHash()) { $result = new Node\HashNode($result, $stream->getNext()->getValue()); } elseif ($peek->isDelimiter(array('.'))) { $stream->getNext(); $result = new Node\ClassNode($result, $stream->getNextIdentifier()); } elseif ($peek->isDelimiter(array('['))) { $stream->getNext(); $result = $this->parseAttributeNode($result, $stream); } elseif ($peek->isDelimiter(array(':'))) { $stream->getNext(); if ($stream->getPeek()->isDelimiter(array(':'))) { $stream->getNext(); $pseudoElement = $stream->getNextIdentifier(); continue; } $identifier = $stream->getNextIdentifier(); if (in_array(strtolower($identifier), array('first-line', 'first-letter', 'before', 'after'))) { // Special case: CSS 2.1 pseudo-elements can have a single ':'. // Any new pseudo-element must have two. $pseudoElement = $identifier; continue; } if (!$stream->getPeek()->isDelimiter(array('('))) { $result = new Node\PseudoNode($result, $identifier); continue; } $stream->getNext(); $stream->skipWhitespace(); if ('not' === strtolower($identifier)) { if ($insideNegation) { throw SyntaxErrorException::nestedNot(); } list($argument, $argumentPseudoElement) = $this->parseSimpleSelector($stream, true); $next = $stream->getNext(); if (null !== $argumentPseudoElement) { throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()'); } if (!$next->isDelimiter(array(')'))) { throw SyntaxErrorException::unexpectedToken('")"', $next); } $result = new Node\NegationNode($result, $argument); } else { $arguments = array(); $next = null; while (true) { $stream->skipWhitespace(); $next = $stream->getNext(); if ($next->isIdentifier() || $next->isString() || $next->isNumber() || $next->isDelimiter(array('+', '-')) ) { $arguments[] = $next; } elseif ($next->isDelimiter(array(')'))) { break; } else { throw SyntaxErrorException::unexpectedToken('an argument', $next); } } if (empty($arguments)) { throw SyntaxErrorException::unexpectedToken('at least one argument', $next); } $result = new Node\FunctionNode($result, $identifier, $arguments); } } else { throw SyntaxErrorException::unexpectedToken('selector', $peek); } } if (count($stream->getUsed()) === $selectorStart) { throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek()); } return array($result, $pseudoElement); } /** * Parses next element node. * * @param TokenStream $stream * * @return Node\ElementNode */ private function parseElementNode(TokenStream $stream) { $peek = $stream->getPeek(); if ($peek->isIdentifier() || $peek->isDelimiter(array('*'))) { if ($peek->isIdentifier()) { $namespace = $stream->getNext()->getValue(); } else { $stream->getNext(); $namespace = null; } if ($stream->getPeek()->isDelimiter(array('|'))) { $stream->getNext(); $element = $stream->getNextIdentifierOrStar(); } else { $element = $namespace; $namespace = null; } } else { $element = $namespace = null; } return new Node\ElementNode($namespace, $element); } /** * Parses next attribute node. * * @param Node\NodeInterface $selector * @param TokenStream $stream * * @return Node\AttributeNode * * @throws SyntaxErrorException */ private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream) { $stream->skipWhitespace(); $attribute = $stream->getNextIdentifierOrStar(); if (null === $attribute && !$stream->getPeek()->isDelimiter(array('|'))) { throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek()); } if ($stream->getPeek()->isDelimiter(array('|'))) { $stream->getNext(); if ($stream->getPeek()->isDelimiter(array('='))) { $namespace = null; $stream->getNext(); $operator = '|='; } else { $namespace = $attribute; $attribute = $stream->getNextIdentifier(); $operator = null; } } else { $namespace = $operator = null; } if (null === $operator) { $stream->skipWhitespace(); $next = $stream->getNext(); if ($next->isDelimiter(array(']'))) { return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null); } elseif ($next->isDelimiter(array('='))) { $operator = '='; } elseif ($next->isDelimiter(array('^', '$', '*', '~', '|', '!')) && $stream->getPeek()->isDelimiter(array('=')) ) { $operator = $next->getValue().'='; $stream->getNext(); } else { throw SyntaxErrorException::unexpectedToken('operator', $next); } } $stream->skipWhitespace(); $value = $stream->getNext(); if ($value->isNumber()) { // if the value is a number, it's casted into a string $value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition()); } if (!($value->isIdentifier() || $value->isString())) { throw SyntaxErrorException::unexpectedToken('string or identifier', $value); } $stream->skipWhitespace(); $next = $stream->getNext(); if (!$next->isDelimiter(array(']'))) { throw SyntaxErrorException::unexpectedToken('"]"', $next); } return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue()); } } vendor/symfony/css-selector/Parser/Token.php 0000644 00000005677 15132754523 0015226 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser; /** * CSS selector token. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Token { const TYPE_FILE_END = 'eof'; const TYPE_DELIMITER = 'delimiter'; const TYPE_WHITESPACE = 'whitespace'; const TYPE_IDENTIFIER = 'identifier'; const TYPE_HASH = 'hash'; const TYPE_NUMBER = 'number'; const TYPE_STRING = 'string'; /** * @var int */ private $type; /** * @var string */ private $value; /** * @var int */ private $position; /** * @param int $type * @param string $value * @param int $position */ public function __construct($type, $value, $position) { $this->type = $type; $this->value = $value; $this->position = $position; } /** * @return int */ public function getType() { return $this->type; } /** * @return string */ public function getValue() { return $this->value; } /** * @return int */ public function getPosition() { return $this->position; } /** * @return bool */ public function isFileEnd() { return self::TYPE_FILE_END === $this->type; } /** * @param array $values * * @return bool */ public function isDelimiter(array $values = array()) { if (self::TYPE_DELIMITER !== $this->type) { return false; } if (empty($values)) { return true; } return in_array($this->value, $values); } /** * @return bool */ public function isWhitespace() { return self::TYPE_WHITESPACE === $this->type; } /** * @return bool */ public function isIdentifier() { return self::TYPE_IDENTIFIER === $this->type; } /** * @return bool */ public function isHash() { return self::TYPE_HASH === $this->type; } /** * @return bool */ public function isNumber() { return self::TYPE_NUMBER === $this->type; } /** * @return bool */ public function isString() { return self::TYPE_STRING === $this->type; } /** * @return string */ public function __toString() { if ($this->value) { return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position); } return sprintf('<%s at %s>', $this->type, $this->position); } } vendor/symfony/css-selector/Parser/Shortcut/HashParser.php 0000644 00000003060 15132754523 0020001 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Shortcut; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\HashNode; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\ParserInterface; /** * CSS selector hash parser shortcut. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class HashParser implements ParserInterface { /** * {@inheritdoc} */ public function parse($source) { // Matches an optional namespace, optional element, and required id // $source = 'test|input#ab6bd_field'; // $matches = array (size=4) // 0 => string 'test|input#ab6bd_field' (length=22) // 1 => string 'test' (length=4) // 2 => string 'input' (length=5) // 3 => string 'ab6bd_field' (length=11) if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) { return array( new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])), ); } return array(); } } vendor/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php 0000644 00000002312 15132754523 0021402 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Shortcut; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\ParserInterface; /** * CSS selector class parser shortcut. * * This shortcut ensure compatibility with previous version. * - The parser fails to parse an empty string. * - In the previous version, an empty string matches each tags. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class EmptyStringParser implements ParserInterface { /** * {@inheritdoc} */ public function parse($source) { // Matches an empty string if ($source == '') { return array(new SelectorNode(new ElementNode(null, '*'))); } return array(); } } vendor/symfony/css-selector/Parser/Shortcut/ClassParser.php 0000644 00000003070 15132754523 0020164 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Shortcut; use Symfony\Component\CssSelector\Node\ClassNode; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\ParserInterface; /** * CSS selector class parser shortcut. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class ClassParser implements ParserInterface { /** * {@inheritdoc} */ public function parse($source) { // Matches an optional namespace, optional element, and required class // $source = 'test|input.ab6bd_field'; // $matches = array (size=4) // 0 => string 'test|input.ab6bd_field' (length=22) // 1 => string 'test' (length=4) // 2 => string 'input' (length=5) // 3 => string 'ab6bd_field' (length=11) if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) { return array( new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])), ); } return array(); } } vendor/symfony/css-selector/Parser/Shortcut/ElementParser.php 0000644 00000002550 15132754523 0020512 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Shortcut; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\ParserInterface; /** * CSS selector element parser shortcut. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class ElementParser implements ParserInterface { /** * {@inheritdoc} */ public function parse($source) { // Matches an optional namespace, required element or `*` // $source = 'testns|testel'; // $matches = array (size=3) // 0 => string 'testns|testel' (length=13) // 1 => string 'testns' (length=6) // 2 => string 'testel' (length=6) if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) { return array(new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))); } return array(); } } vendor/symfony/css-selector/Parser/TokenStream.php 0000644 00000007107 15132754523 0016370 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser; use Symfony\Component\CssSelector\Exception\InternalErrorException; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; /** * CSS selector token stream. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class TokenStream { /** * @var Token[] */ private $tokens = array(); /** * @var bool */ private $frozen = false; /** * @var Token[] */ private $used = array(); /** * @var int */ private $cursor = 0; /** * @var Token|null */ private $peeked = null; /** * @var bool */ private $peeking = false; /** * Pushes a token. * * @param Token $token * * @return $this */ public function push(Token $token) { $this->tokens[] = $token; return $this; } /** * Freezes stream. * * @return $this */ public function freeze() { $this->frozen = true; return $this; } /** * Returns next token. * * @return Token * * @throws InternalErrorException If there is no more token */ public function getNext() { if ($this->peeking) { $this->peeking = false; $this->used[] = $this->peeked; return $this->peeked; } if (!isset($this->tokens[$this->cursor])) { throw new InternalErrorException('Unexpected token stream end.'); } return $this->tokens[$this->cursor++]; } /** * Returns peeked token. * * @return Token */ public function getPeek() { if (!$this->peeking) { $this->peeked = $this->getNext(); $this->peeking = true; } return $this->peeked; } /** * Returns used tokens. * * @return Token[] */ public function getUsed() { return $this->used; } /** * Returns nex identifier token. * * @return string The identifier token value * * @throws SyntaxErrorException If next token is not an identifier */ public function getNextIdentifier() { $next = $this->getNext(); if (!$next->isIdentifier()) { throw SyntaxErrorException::unexpectedToken('identifier', $next); } return $next->getValue(); } /** * Returns nex identifier or star delimiter token. * * @return null|string The identifier token value or null if star found * * @throws SyntaxErrorException If next token is not an identifier or a star delimiter */ public function getNextIdentifierOrStar() { $next = $this->getNext(); if ($next->isIdentifier()) { return $next->getValue(); } if ($next->isDelimiter(array('*'))) { return; } throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next); } /** * Skips next whitespace if any. */ public function skipWhitespace() { $peek = $this->getPeek(); if ($peek->isWhitespace()) { $this->getNext(); } } } vendor/symfony/css-selector/Parser/ParserInterface.php 0000644 00000001477 15132754523 0017215 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser; use Symfony\Component\CssSelector\Node\SelectorNode; /** * CSS selector parser interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ interface ParserInterface { /** * Parses given selector source into an array of tokens. * * @param string $source * * @return SelectorNode[] */ public function parse($source); } vendor/symfony/css-selector/Parser/Handler/HandlerInterface.php 0000644 00000001561 15132754523 0020705 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\TokenStream; /** * CSS selector handler interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ interface HandlerInterface { /** * @param Reader $reader * @param TokenStream $stream * * @return bool */ public function handle(Reader $reader, TokenStream $stream); } vendor/symfony/css-selector/Parser/Handler/IdentifierHandler.php 0000644 00000003420 15132754523 0021063 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; /** * CSS selector comment handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class IdentifierHandler implements HandlerInterface { /** * @var TokenizerPatterns */ private $patterns; /** * @var TokenizerEscaping */ private $escaping; /** * @param TokenizerPatterns $patterns * @param TokenizerEscaping $escaping */ public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) { $this->patterns = $patterns; $this->escaping = $escaping; } /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { $match = $reader->findPattern($this->patterns->getIdentifierPattern()); if (!$match) { return false; } $value = $this->escaping->escapeUnicode($match[0]); $stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition())); $reader->moveForward(strlen($match[0])); return true; } } vendor/symfony/css-selector/Parser/Handler/StringHandler.php 0000644 00000005103 15132754523 0020247 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Exception\InternalErrorException; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; /** * CSS selector comment handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class StringHandler implements HandlerInterface { /** * @var TokenizerPatterns */ private $patterns; /** * @var TokenizerEscaping */ private $escaping; /** * @param TokenizerPatterns $patterns * @param TokenizerEscaping $escaping */ public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) { $this->patterns = $patterns; $this->escaping = $escaping; } /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { $quote = $reader->getSubstring(1); if (!in_array($quote, array("'", '"'))) { return false; } $reader->moveForward(1); $match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote)); if (!$match) { throw new InternalErrorException(sprintf('Should have found at least an empty match at %s.', $reader->getPosition())); } // check unclosed strings if (strlen($match[0]) === $reader->getRemainingLength()) { throw SyntaxErrorException::unclosedString($reader->getPosition() - 1); } // check quotes pairs validity if ($quote !== $reader->getSubstring(1, strlen($match[0]))) { throw SyntaxErrorException::unclosedString($reader->getPosition() - 1); } $string = $this->escaping->escapeUnicodeAndNewLine($match[0]); $stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition())); $reader->moveForward(strlen($match[0]) + 1); return true; } } vendor/symfony/css-selector/Parser/Handler/WhitespaceHandler.php 0000644 00000002240 15132754523 0021074 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; /** * CSS selector whitespace handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class WhitespaceHandler implements HandlerInterface { /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { $match = $reader->findPattern('~^[ \t\r\n\f]+~'); if (false === $match) { return false; } $stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition())); $reader->moveForward(strlen($match[0])); return true; } } vendor/symfony/css-selector/Parser/Handler/CommentHandler.php 0000644 00000002154 15132754523 0020406 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\TokenStream; /** * CSS selector comment handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class CommentHandler implements HandlerInterface { /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { if ('/*' !== $reader->getSubstring(2)) { return false; } $offset = $reader->getOffset('*/'); if (false === $offset) { $reader->moveToEnd(); } else { $reader->moveForward($offset + 2); } return true; } } vendor/symfony/css-selector/Parser/Handler/NumberHandler.php 0000644 00000002723 15132754523 0020236 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; /** * CSS selector comment handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class NumberHandler implements HandlerInterface { /** * @var TokenizerPatterns */ private $patterns; /** * @param TokenizerPatterns $patterns */ public function __construct(TokenizerPatterns $patterns) { $this->patterns = $patterns; } /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { $match = $reader->findPattern($this->patterns->getNumberPattern()); if (!$match) { return false; } $stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition())); $reader->moveForward(strlen($match[0])); return true; } } vendor/symfony/css-selector/Parser/Handler/HashHandler.php 0000644 00000003376 15132754523 0017676 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; /** * CSS selector comment handler. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class HashHandler implements HandlerInterface { /** * @var TokenizerPatterns */ private $patterns; /** * @var TokenizerEscaping */ private $escaping; /** * @param TokenizerPatterns $patterns * @param TokenizerEscaping $escaping */ public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) { $this->patterns = $patterns; $this->escaping = $escaping; } /** * {@inheritdoc} */ public function handle(Reader $reader, TokenStream $stream) { $match = $reader->findPattern($this->patterns->getHashPattern()); if (!$match) { return false; } $value = $this->escaping->escapeUnicode($match[1]); $stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition())); $reader->moveForward(strlen($match[0])); return true; } } vendor/symfony/css-selector/Parser/Reader.php 0000644 00000004423 15132754523 0015334 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser; /** * CSS selector reader. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Reader { /** * @var string */ private $source; /** * @var int */ private $length; /** * @var int */ private $position = 0; /** * @param string $source */ public function __construct($source) { $this->source = $source; $this->length = strlen($source); } /** * @return bool */ public function isEOF() { return $this->position >= $this->length; } /** * @return int */ public function getPosition() { return $this->position; } /** * @return int */ public function getRemainingLength() { return $this->length - $this->position; } /** * @param int $length * @param int $offset * * @return string */ public function getSubstring($length, $offset = 0) { return substr($this->source, $this->position + $offset, $length); } /** * @param string $string * * @return int */ public function getOffset($string) { $position = strpos($this->source, $string, $this->position); return false === $position ? false : $position - $this->position; } /** * @param string $pattern * * @return array|false */ public function findPattern($pattern) { $source = substr($this->source, $this->position); if (preg_match($pattern, $source, $matches)) { return $matches; } return false; } /** * @param int $length */ public function moveForward($length) { $this->position += $length; } public function moveToEnd() { $this->position = $this->length; } } vendor/symfony/css-selector/Parser/Tokenizer/Tokenizer.php 0000644 00000004124 15132754523 0020054 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Tokenizer; use Symfony\Component\CssSelector\Parser\Handler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; /** * CSS selector tokenizer. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Tokenizer { /** * @var Handler\HandlerInterface[] */ private $handlers; /** * Constructor. */ public function __construct() { $patterns = new TokenizerPatterns(); $escaping = new TokenizerEscaping($patterns); $this->handlers = array( new Handler\WhitespaceHandler(), new Handler\IdentifierHandler($patterns, $escaping), new Handler\HashHandler($patterns, $escaping), new Handler\StringHandler($patterns, $escaping), new Handler\NumberHandler($patterns), new Handler\CommentHandler(), ); } /** * Tokenize selector source code. * * @param Reader $reader * * @return TokenStream */ public function tokenize(Reader $reader) { $stream = new TokenStream(); while (!$reader->isEOF()) { foreach ($this->handlers as $handler) { if ($handler->handle($reader, $stream)) { continue 2; } } $stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition())); $reader->moveForward(1); } return $stream ->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition())) ->freeze(); } } vendor/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php 0000644 00000006630 15132754523 0021601 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Tokenizer; /** * CSS selector tokenizer patterns builder. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class TokenizerPatterns { /** * @var string */ private $unicodeEscapePattern; /** * @var string */ private $simpleEscapePattern; /** * @var string */ private $newLineEscapePattern; /** * @var string */ private $escapePattern; /** * @var string */ private $stringEscapePattern; /** * @var string */ private $nonAsciiPattern; /** * @var string */ private $nmCharPattern; /** * @var string */ private $nmStartPattern; /** * @var string */ private $identifierPattern; /** * @var string */ private $hashPattern; /** * @var string */ private $numberPattern; /** * @var string */ private $quotedStringPattern; /** * Constructor. */ public function __construct() { $this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?'; $this->simpleEscapePattern = '\\\\(.)'; $this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)'; $this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]'; $this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern; $this->nonAsciiPattern = '[^\x00-\x7F]'; $this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern; $this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern; $this->identifierPattern = '(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*'; $this->hashPattern = '#((?:'.$this->nmCharPattern.')+)'; $this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)'; $this->quotedStringPattern = '([^\n\r\f%s]|'.$this->stringEscapePattern.')*'; } /** * @return string */ public function getNewLineEscapePattern() { return '~^'.$this->newLineEscapePattern.'~'; } /** * @return string */ public function getSimpleEscapePattern() { return '~^'.$this->simpleEscapePattern.'~'; } /** * @return string */ public function getUnicodeEscapePattern() { return '~^'.$this->unicodeEscapePattern.'~i'; } /** * @return string */ public function getIdentifierPattern() { return '~^'.$this->identifierPattern.'~i'; } /** * @return string */ public function getHashPattern() { return '~^'.$this->hashPattern.'~i'; } /** * @return string */ public function getNumberPattern() { return '~^'.$this->numberPattern.'~'; } /** * @param string $quote * * @return string */ public function getQuotedStringPattern($quote) { return '~^'.sprintf($this->quotedStringPattern, $quote).'~i'; } } vendor/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php 0000644 00000003750 15132754523 0021532 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Parser\Tokenizer; /** * CSS selector tokenizer escaping applier. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class TokenizerEscaping { /** * @var TokenizerPatterns */ private $patterns; /** * @param TokenizerPatterns $patterns */ public function __construct(TokenizerPatterns $patterns) { $this->patterns = $patterns; } /** * @param string $value * * @return string */ public function escapeUnicode($value) { $value = $this->replaceUnicodeSequences($value); return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value); } /** * @param string $value * * @return string */ public function escapeUnicodeAndNewLine($value) { $value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value); return $this->escapeUnicode($value); } /** * @param string $value * * @return string */ private function replaceUnicodeSequences($value) { return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) { $c = hexdec($match[1]); if (0x80 > $c %= 0x200000) { return chr($c); } if (0x800 > $c) { return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F); } if (0x10000 > $c) { return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); } }, $value); } } vendor/symfony/css-selector/Node/CombinedSelectorNode.php 0000644 00000003726 15132754523 0017617 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a combined node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class CombinedSelectorNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $combinator; /** * @var NodeInterface */ private $subSelector; /** * @param NodeInterface $selector * @param string $combinator * @param NodeInterface $subSelector */ public function __construct(NodeInterface $selector, $combinator, NodeInterface $subSelector) { $this->selector = $selector; $this->combinator = $combinator; $this->subSelector = $subSelector; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getCombinator() { return $this->combinator; } /** * @return NodeInterface */ public function getSubSelector() { return $this->subSelector; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); } /** * {@inheritdoc} */ public function __toString() { $combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator; return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector); } } vendor/symfony/css-selector/Node/ClassNode.php 0000644 00000002775 15132754523 0015446 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>.<name>" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class ClassNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $name; /** * @param NodeInterface $selector * @param string $name */ public function __construct(NodeInterface $selector, $name) { $this->selector = $selector; $this->name = $name; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getName() { return $this->name; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); } /** * {@inheritdoc} */ public function __toString() { return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name); } } vendor/symfony/css-selector/Node/HashNode.php 0000644 00000002752 15132754523 0015257 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>#<id>" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class HashNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $id; /** * @param NodeInterface $selector * @param string $id */ public function __construct(NodeInterface $selector, $id) { $this->selector = $selector; $this->id = $id; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getId() { return $this->id; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0)); } /** * {@inheritdoc} */ public function __toString() { return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id); } } vendor/symfony/css-selector/Node/NegationNode.php 0000644 00000003160 15132754523 0016132 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>:not(<identifier>)" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class NegationNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var NodeInterface */ private $subSelector; /** * @param NodeInterface $selector * @param NodeInterface $subSelector */ public function __construct(NodeInterface $selector, NodeInterface $subSelector) { $this->selector = $selector; $this->subSelector = $subSelector; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return NodeInterface */ public function getSubSelector() { return $this->subSelector; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); } /** * {@inheritdoc} */ public function __toString() { return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); } } vendor/symfony/css-selector/Node/AttributeNode.php 0000644 00000005131 15132754523 0016331 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>[<namespace>|<attribute> <operator> <value>]" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class AttributeNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $namespace; /** * @var string */ private $attribute; /** * @var string */ private $operator; /** * @var string */ private $value; /** * @param NodeInterface $selector * @param string $namespace * @param string $attribute * @param string $operator * @param string $value */ public function __construct(NodeInterface $selector, $namespace, $attribute, $operator, $value) { $this->selector = $selector; $this->namespace = $namespace; $this->attribute = $attribute; $this->operator = $operator; $this->value = $value; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getNamespace() { return $this->namespace; } /** * @return string */ public function getAttribute() { return $this->attribute; } /** * @return string */ public function getOperator() { return $this->operator; } /** * @return string */ public function getValue() { return $this->value; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); } /** * {@inheritdoc} */ public function __toString() { $attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute; return 'exists' === $this->operator ? sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute) : sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value); } } vendor/symfony/css-selector/Node/FunctionNode.php 0000644 00000004051 15132754523 0016153 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Parser\Token; /** * Represents a "<selector>:<name>(<arguments>)" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class FunctionNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $name; /** * @var Token[] */ private $arguments; /** * @param NodeInterface $selector * @param string $name * @param Token[] $arguments */ public function __construct(NodeInterface $selector, $name, array $arguments = array()) { $this->selector = $selector; $this->name = strtolower($name); $this->arguments = $arguments; } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getName() { return $this->name; } /** * @return Token[] */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); } /** * {@inheritdoc} */ public function __toString() { $arguments = implode(', ', array_map(function (Token $token) { return "'".$token->getValue()."'"; }, $this->arguments)); return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : ''); } } vendor/symfony/css-selector/Node/NodeInterface.php 0000644 00000001646 15132754523 0016275 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Interface for nodes. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ interface NodeInterface { /** * Returns node's name. * * @return string */ public function getNodeName(); /** * Returns node's specificity. * * @return Specificity */ public function getSpecificity(); /** * Returns node's string representation. * * @return string */ public function __toString(); } vendor/symfony/css-selector/Node/ElementNode.php 0000644 00000003124 15132754523 0015757 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<namespace>|<element>" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class ElementNode extends AbstractNode { /** * @var string|null */ private $namespace; /** * @var string|null */ private $element; /** * @param string|null $namespace * @param string|null $element */ public function __construct($namespace = null, $element = null) { $this->namespace = $namespace; $this->element = $element; } /** * @return null|string */ public function getNamespace() { return $this->namespace; } /** * @return null|string */ public function getElement() { return $this->element; } /** * {@inheritdoc} */ public function getSpecificity() { return new Specificity(0, 0, $this->element ? 1 : 0); } /** * {@inheritdoc} */ public function __toString() { $element = $this->element ?: '*'; return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element); } } vendor/symfony/css-selector/Node/PseudoNode.php 0000644 00000003100 15132754523 0015617 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>:<identifier>" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class PseudoNode extends AbstractNode { /** * @var NodeInterface */ private $selector; /** * @var string */ private $identifier; /** * @param NodeInterface $selector * @param string $identifier */ public function __construct(NodeInterface $selector, $identifier) { $this->selector = $selector; $this->identifier = strtolower($identifier); } /** * @return NodeInterface */ public function getSelector() { return $this->selector; } /** * @return string */ public function getIdentifier() { return $this->identifier; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); } /** * {@inheritdoc} */ public function __toString() { return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier); } } vendor/symfony/css-selector/Node/Specificity.php 0000644 00000004242 15132754523 0016035 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a node specificity. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @see http://www.w3.org/TR/selectors/#specificity * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Specificity { const A_FACTOR = 100; const B_FACTOR = 10; const C_FACTOR = 1; /** * @var int */ private $a; /** * @var int */ private $b; /** * @var int */ private $c; /** * Constructor. * * @param int $a * @param int $b * @param int $c */ public function __construct($a, $b, $c) { $this->a = $a; $this->b = $b; $this->c = $c; } /** * @param Specificity $specificity * * @return self */ public function plus(Specificity $specificity) { return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c); } /** * Returns global specificity value. * * @return int */ public function getValue() { return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR; } /** * Returns -1 if the object specificity is lower than the argument, * 0 if they are equal, and 1 if the argument is lower. * * @param Specificity $specificity * * @return int */ public function compareTo(Specificity $specificity) { if ($this->a !== $specificity->a) { return $this->a > $specificity->a ? 1 : -1; } if ($this->b !== $specificity->b) { return $this->b > $specificity->b ? 1 : -1; } if ($this->c !== $specificity->c) { return $this->c > $specificity->c ? 1 : -1; } return 0; } } vendor/symfony/css-selector/Node/AbstractNode.php 0000644 00000001646 15132754523 0016140 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Abstract base node class. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ abstract class AbstractNode implements NodeInterface { /** * @var string */ private $nodeName; /** * @return string */ public function getNodeName() { if (null === $this->nodeName) { $this->nodeName = preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', get_called_class()); } return $this->nodeName; } } vendor/symfony/css-selector/Node/SelectorNode.php 0000644 00000003242 15132754523 0016147 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * Represents a "<selector>(::|:)<pseudoElement>" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class SelectorNode extends AbstractNode { /** * @var NodeInterface */ private $tree; /** * @var null|string */ private $pseudoElement; /** * @param NodeInterface $tree * @param null|string $pseudoElement */ public function __construct(NodeInterface $tree, $pseudoElement = null) { $this->tree = $tree; $this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null; } /** * @return NodeInterface */ public function getTree() { return $this->tree; } /** * @return null|string */ public function getPseudoElement() { return $this->pseudoElement; } /** * {@inheritdoc} */ public function getSpecificity() { return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0)); } /** * {@inheritdoc} */ public function __toString() { return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : ''); } } vendor/symfony/css-selector/XPath/TranslatorInterface.php 0000644 00000002241 15132754523 0017670 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath; use Symfony\Component\CssSelector\Node\SelectorNode; /** * XPath expression translator interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ interface TranslatorInterface { /** * Translates a CSS selector to an XPath expression. * * @param string $cssExpr * @param string $prefix * * @return string */ public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::'); /** * Translates a parsed selector node to an XPath expression. * * @param SelectorNode $selector * @param string $prefix * * @return string */ public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::'); } vendor/symfony/css-selector/XPath/Translator.php 0000644 00000020414 15132754523 0016051 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath; use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\Node\NodeInterface; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Parser; use Symfony\Component\CssSelector\Parser\ParserInterface; /** * XPath expression translator interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class Translator implements TranslatorInterface { /** * @var ParserInterface */ private $mainParser; /** * @var ParserInterface[] */ private $shortcutParsers = array(); /** * @var Extension\ExtensionInterface */ private $extensions = array(); /** * @var array */ private $nodeTranslators = array(); /** * @var array */ private $combinationTranslators = array(); /** * @var array */ private $functionTranslators = array(); /** * @var array */ private $pseudoClassTranslators = array(); /** * @var array */ private $attributeMatchingTranslators = array(); public function __construct(ParserInterface $parser = null) { $this->mainParser = $parser ?: new Parser(); $this ->registerExtension(new Extension\NodeExtension()) ->registerExtension(new Extension\CombinationExtension()) ->registerExtension(new Extension\FunctionExtension()) ->registerExtension(new Extension\PseudoClassExtension()) ->registerExtension(new Extension\AttributeMatchingExtension()) ; } /** * @param string $element * * @return string */ public static function getXpathLiteral($element) { if (false === strpos($element, "'")) { return "'".$element."'"; } if (false === strpos($element, '"')) { return '"'.$element.'"'; } $string = $element; $parts = array(); while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf('concat(%s)', implode($parts, ', ')); } /** * {@inheritdoc} */ public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::') { $selectors = $this->parseSelectors($cssExpr); /** @var SelectorNode $selector */ foreach ($selectors as $index => $selector) { if (null !== $selector->getPseudoElement()) { throw new ExpressionErrorException('Pseudo-elements are not supported.'); } $selectors[$index] = $this->selectorToXPath($selector, $prefix); } return implode(' | ', $selectors); } /** * {@inheritdoc} */ public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::') { return ($prefix ?: '').$this->nodeToXPath($selector); } /** * Registers an extension. * * @param Extension\ExtensionInterface $extension * * @return $this */ public function registerExtension(Extension\ExtensionInterface $extension) { $this->extensions[$extension->getName()] = $extension; $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators()); $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators()); $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators()); $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators()); $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators()); return $this; } /** * @param string $name * * @return Extension\ExtensionInterface * * @throws ExpressionErrorException */ public function getExtension($name) { if (!isset($this->extensions[$name])) { throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name)); } return $this->extensions[$name]; } /** * Registers a shortcut parser. * * @param ParserInterface $shortcut * * @return $this */ public function registerParserShortcut(ParserInterface $shortcut) { $this->shortcutParsers[] = $shortcut; return $this; } /** * @param NodeInterface $node * * @return XPathExpr * * @throws ExpressionErrorException */ public function nodeToXPath(NodeInterface $node) { if (!isset($this->nodeTranslators[$node->getNodeName()])) { throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName())); } return call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this); } /** * @param string $combiner * @param NodeInterface $xpath * @param NodeInterface $combinedXpath * * @return XPathExpr * * @throws ExpressionErrorException */ public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath) { if (!isset($this->combinationTranslators[$combiner])) { throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner)); } return call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath)); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr * * @throws ExpressionErrorException */ public function addFunction(XPathExpr $xpath, FunctionNode $function) { if (!isset($this->functionTranslators[$function->getName()])) { throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName())); } return call_user_func($this->functionTranslators[$function->getName()], $xpath, $function); } /** * @param XPathExpr $xpath * @param string $pseudoClass * * @return XPathExpr * * @throws ExpressionErrorException */ public function addPseudoClass(XPathExpr $xpath, $pseudoClass) { if (!isset($this->pseudoClassTranslators[$pseudoClass])) { throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass)); } return call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath); } /** * @param XPathExpr $xpath * @param string $operator * @param string $attribute * @param string $value * * @return XPathExpr * * @throws ExpressionErrorException */ public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value) { if (!isset($this->attributeMatchingTranslators[$operator])) { throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator)); } return call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value); } /** * @param string $css * * @return SelectorNode[] */ private function parseSelectors($css) { foreach ($this->shortcutParsers as $shortcut) { $tokens = $shortcut->parse($css); if (!empty($tokens)) { return $tokens; } } return $this->mainParser->parse($css); } } vendor/symfony/css-selector/XPath/XPathExpr.php 0000644 00000005461 15132754523 0015610 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath; /** * XPath expression translator interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class XPathExpr { /** * @var string */ private $path; /** * @var string */ private $element; /** * @var string */ private $condition; /** * @param string $path * @param string $element * @param string $condition * @param bool $starPrefix */ public function __construct($path = '', $element = '*', $condition = '', $starPrefix = false) { $this->path = $path; $this->element = $element; $this->condition = $condition; if ($starPrefix) { $this->addStarPrefix(); } } /** * @return string */ public function getElement() { return $this->element; } /** * @param $condition * * @return $this */ public function addCondition($condition) { $this->condition = $this->condition ? sprintf('%s and (%s)', $this->condition, $condition) : $condition; return $this; } /** * @return string */ public function getCondition() { return $this->condition; } /** * @return $this */ public function addNameTest() { if ('*' !== $this->element) { $this->addCondition('name() = '.Translator::getXpathLiteral($this->element)); $this->element = '*'; } return $this; } /** * @return $this */ public function addStarPrefix() { $this->path .= '*/'; return $this; } /** * Joins another XPathExpr with a combiner. * * @param string $combiner * @param XPathExpr $expr * * @return $this */ public function join($combiner, XPathExpr $expr) { $path = $this->__toString().$combiner; if ('*/' !== $expr->path) { $path .= $expr->path; } $this->path = $path; $this->element = $expr->element; $this->condition = $expr->condition; return $this; } /** * @return string */ public function __toString() { $path = $this->path.$this->element; $condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']'; return $path.$condition; } } vendor/symfony/css-selector/XPath/Extension/PseudoClassExtension.php 0000644 00000007701 15132754523 0022022 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator pseudo-class extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class PseudoClassExtension extends AbstractExtension { /** * {@inheritdoc} */ public function getPseudoClassTranslators() { return array( 'root' => array($this, 'translateRoot'), 'first-child' => array($this, 'translateFirstChild'), 'last-child' => array($this, 'translateLastChild'), 'first-of-type' => array($this, 'translateFirstOfType'), 'last-of-type' => array($this, 'translateLastOfType'), 'only-child' => array($this, 'translateOnlyChild'), 'only-of-type' => array($this, 'translateOnlyOfType'), 'empty' => array($this, 'translateEmpty'), ); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateRoot(XPathExpr $xpath) { return $xpath->addCondition('not(parent::*)'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateFirstChild(XPathExpr $xpath) { return $xpath ->addStarPrefix() ->addNameTest() ->addCondition('position() = 1'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateLastChild(XPathExpr $xpath) { return $xpath ->addStarPrefix() ->addNameTest() ->addCondition('position() = last()'); } /** * @param XPathExpr $xpath * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateFirstOfType(XPathExpr $xpath) { if ('*' === $xpath->getElement()) { throw new ExpressionErrorException('"*:first-of-type" is not implemented.'); } return $xpath ->addStarPrefix() ->addCondition('position() = 1'); } /** * @param XPathExpr $xpath * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateLastOfType(XPathExpr $xpath) { if ('*' === $xpath->getElement()) { throw new ExpressionErrorException('"*:last-of-type" is not implemented.'); } return $xpath ->addStarPrefix() ->addCondition('position() = last()'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateOnlyChild(XPathExpr $xpath) { return $xpath ->addStarPrefix() ->addNameTest() ->addCondition('last() = 1'); } /** * @param XPathExpr $xpath * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateOnlyOfType(XPathExpr $xpath) { if ('*' === $xpath->getElement()) { throw new ExpressionErrorException('"*:only-of-type" is not implemented.'); } return $xpath->addCondition('last() = 1'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateEmpty(XPathExpr $xpath) { return $xpath->addCondition('not(*) and not(string-length())'); } /** * {@inheritdoc} */ public function getName() { return 'pseudo-class'; } } vendor/symfony/css-selector/XPath/Extension/NodeExtension.php 0000644 00000016044 15132754523 0020462 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator node extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class NodeExtension extends AbstractExtension { const ELEMENT_NAME_IN_LOWER_CASE = 1; const ATTRIBUTE_NAME_IN_LOWER_CASE = 2; const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4; /** * @var int */ private $flags; /** * Constructor. * * @param int $flags */ public function __construct($flags = 0) { $this->flags = $flags; } /** * @param int $flag * @param bool $on * * @return $this */ public function setFlag($flag, $on) { if ($on && !$this->hasFlag($flag)) { $this->flags += $flag; } if (!$on && $this->hasFlag($flag)) { $this->flags -= $flag; } return $this; } /** * @param int $flag * * @return bool */ public function hasFlag($flag) { return (bool) ($this->flags & $flag); } /** * {@inheritdoc} */ public function getNodeTranslators() { return array( 'Selector' => array($this, 'translateSelector'), 'CombinedSelector' => array($this, 'translateCombinedSelector'), 'Negation' => array($this, 'translateNegation'), 'Function' => array($this, 'translateFunction'), 'Pseudo' => array($this, 'translatePseudo'), 'Attribute' => array($this, 'translateAttribute'), 'Class' => array($this, 'translateClass'), 'Hash' => array($this, 'translateHash'), 'Element' => array($this, 'translateElement'), ); } /** * @param Node\SelectorNode $node * @param Translator $translator * * @return XPathExpr */ public function translateSelector(Node\SelectorNode $node, Translator $translator) { return $translator->nodeToXPath($node->getTree()); } /** * @param Node\CombinedSelectorNode $node * @param Translator $translator * * @return XPathExpr */ public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator) { return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector()); } /** * @param Node\NegationNode $node * @param Translator $translator * * @return XPathExpr */ public function translateNegation(Node\NegationNode $node, Translator $translator) { $xpath = $translator->nodeToXPath($node->getSelector()); $subXpath = $translator->nodeToXPath($node->getSubSelector()); $subXpath->addNameTest(); if ($subXpath->getCondition()) { return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition())); } return $xpath->addCondition('0'); } /** * @param Node\FunctionNode $node * @param Translator $translator * * @return XPathExpr */ public function translateFunction(Node\FunctionNode $node, Translator $translator) { $xpath = $translator->nodeToXPath($node->getSelector()); return $translator->addFunction($xpath, $node); } /** * @param Node\PseudoNode $node * @param Translator $translator * * @return XPathExpr */ public function translatePseudo(Node\PseudoNode $node, Translator $translator) { $xpath = $translator->nodeToXPath($node->getSelector()); return $translator->addPseudoClass($xpath, $node->getIdentifier()); } /** * @param Node\AttributeNode $node * @param Translator $translator * * @return XPathExpr */ public function translateAttribute(Node\AttributeNode $node, Translator $translator) { $name = $node->getAttribute(); $safe = $this->isSafeName($name); if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) { $name = strtolower($name); } if ($node->getNamespace()) { $name = sprintf('%s:%s', $node->getNamespace(), $name); $safe = $safe && $this->isSafeName($node->getNamespace()); } $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name)); $value = $node->getValue(); $xpath = $translator->nodeToXPath($node->getSelector()); if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) { $value = strtolower($value); } return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value); } /** * @param Node\ClassNode $node * @param Translator $translator * * @return XPathExpr */ public function translateClass(Node\ClassNode $node, Translator $translator) { $xpath = $translator->nodeToXPath($node->getSelector()); return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName()); } /** * @param Node\HashNode $node * @param Translator $translator * * @return XPathExpr */ public function translateHash(Node\HashNode $node, Translator $translator) { $xpath = $translator->nodeToXPath($node->getSelector()); return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId()); } /** * @param Node\ElementNode $node * * @return XPathExpr */ public function translateElement(Node\ElementNode $node) { $element = $node->getElement(); if ($this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) { $element = strtolower($element); } if ($element) { $safe = $this->isSafeName($element); } else { $element = '*'; $safe = true; } if ($node->getNamespace()) { $element = sprintf('%s:%s', $node->getNamespace(), $element); $safe = $safe && $this->isSafeName($node->getNamespace()); } $xpath = new XPathExpr('', $element); if (!$safe) { $xpath->addNameTest(); } return $xpath; } /** * {@inheritdoc} */ public function getName() { return 'node'; } /** * Tests if given name is safe. * * @param string $name * * @return bool */ private function isSafeName($name) { return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name); } } vendor/symfony/css-selector/XPath/Extension/CombinationExtension.php 0000644 00000004513 15132754523 0022035 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator combination extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class CombinationExtension extends AbstractExtension { /** * {@inheritdoc} */ public function getCombinationTranslators() { return array( ' ' => array($this, 'translateDescendant'), '>' => array($this, 'translateChild'), '+' => array($this, 'translateDirectAdjacent'), '~' => array($this, 'translateIndirectAdjacent'), ); } /** * @param XPathExpr $xpath * @param XPathExpr $combinedXpath * * @return XPathExpr */ public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath) { return $xpath->join('/descendant-or-self::*/', $combinedXpath); } /** * @param XPathExpr $xpath * @param XPathExpr $combinedXpath * * @return XPathExpr */ public function translateChild(XPathExpr $xpath, XPathExpr $combinedXpath) { return $xpath->join('/', $combinedXpath); } /** * @param XPathExpr $xpath * @param XPathExpr $combinedXpath * * @return XPathExpr */ public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath) { return $xpath ->join('/following-sibling::', $combinedXpath) ->addNameTest() ->addCondition('position() = 1'); } /** * @param XPathExpr $xpath * @param XPathExpr $combinedXpath * * @return XPathExpr */ public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath) { return $xpath->join('/following-sibling::', $combinedXpath); } /** * {@inheritdoc} */ public function getName() { return 'combination'; } } vendor/symfony/css-selector/XPath/Extension/ExtensionInterface.php 0000644 00000002767 15132754523 0021504 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; /** * XPath expression translator extension interface. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ interface ExtensionInterface { /** * Returns node translators. * * These callables will receive the node as first argument and the translator as second argument. * * @return callable[] */ public function getNodeTranslators(); /** * Returns combination translators. * * @return callable[] */ public function getCombinationTranslators(); /** * Returns function translators. * * @return callable[] */ public function getFunctionTranslators(); /** * Returns pseudo-class translators. * * @return callable[] */ public function getPseudoClassTranslators(); /** * Returns attribute operation translators. * * @return callable[] */ public function getAttributeMatchingTranslators(); /** * Returns extension name. * * @return string */ public function getName(); } vendor/symfony/css-selector/XPath/Extension/HtmlExtension.php 0000644 00000015105 15132754523 0020476 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator HTML extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class HtmlExtension extends AbstractExtension { /** * Constructor. * * @param Translator $translator */ public function __construct(Translator $translator) { $translator ->getExtension('node') ->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true) ->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true); } /** * {@inheritdoc} */ public function getPseudoClassTranslators() { return array( 'checked' => array($this, 'translateChecked'), 'link' => array($this, 'translateLink'), 'disabled' => array($this, 'translateDisabled'), 'enabled' => array($this, 'translateEnabled'), 'selected' => array($this, 'translateSelected'), 'invalid' => array($this, 'translateInvalid'), 'hover' => array($this, 'translateHover'), 'visited' => array($this, 'translateVisited'), ); } /** * {@inheritdoc} */ public function getFunctionTranslators() { return array( 'lang' => array($this, 'translateLang'), ); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateChecked(XPathExpr $xpath) { return $xpath->addCondition( '(@checked ' ."and (name(.) = 'input' or name(.) = 'command')" ."and (@type = 'checkbox' or @type = 'radio'))" ); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateLink(XPathExpr $xpath) { return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')"); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateDisabled(XPathExpr $xpath) { return $xpath->addCondition( '(' .'@disabled and' .'(' ."(name(.) = 'input' and @type != 'hidden')" ." or name(.) = 'button'" ." or name(.) = 'select'" ." or name(.) = 'textarea'" ." or name(.) = 'command'" ." or name(.) = 'fieldset'" ." or name(.) = 'optgroup'" ." or name(.) = 'option'" .')' .') or (' ."(name(.) = 'input' and @type != 'hidden')" ." or name(.) = 'button'" ." or name(.) = 'select'" ." or name(.) = 'textarea'" .')' .' and ancestor::fieldset[@disabled]' ); // todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any." } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateEnabled(XPathExpr $xpath) { return $xpath->addCondition( '(' .'@href and (' ."name(.) = 'a'" ." or name(.) = 'link'" ." or name(.) = 'area'" .')' .') or (' .'(' ."name(.) = 'command'" ." or name(.) = 'fieldset'" ." or name(.) = 'optgroup'" .')' .' and not(@disabled)' .') or (' .'(' ."(name(.) = 'input' and @type != 'hidden')" ." or name(.) = 'button'" ." or name(.) = 'select'" ." or name(.) = 'textarea'" ." or name(.) = 'keygen'" .')' .' and not (@disabled or ancestor::fieldset[@disabled])' .') or (' ."name(.) = 'option' and not(" .'@disabled or ancestor::optgroup[@disabled]' .')' .')' ); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateLang(XPathExpr $xpath, FunctionNode $function) { $arguments = $function->getArguments(); foreach ($arguments as $token) { if (!($token->isString() || $token->isIdentifier())) { throw new ExpressionErrorException( 'Expected a single string or identifier for :lang(), got ' .implode(', ', $arguments) ); } } return $xpath->addCondition(sprintf( 'ancestor-or-self::*[@lang][1][starts-with(concat(' ."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')" .', %s)]', 'lang', Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-') )); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateSelected(XPathExpr $xpath) { return $xpath->addCondition("(@selected and name(.) = 'option')"); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateInvalid(XPathExpr $xpath) { return $xpath->addCondition('0'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateHover(XPathExpr $xpath) { return $xpath->addCondition('0'); } /** * @param XPathExpr $xpath * * @return XPathExpr */ public function translateVisited(XPathExpr $xpath) { return $xpath->addCondition('0'); } /** * {@inheritdoc} */ public function getName() { return 'html'; } } vendor/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php 0000644 00000011303 15132754523 0023204 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator attribute extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class AttributeMatchingExtension extends AbstractExtension { /** * {@inheritdoc} */ public function getAttributeMatchingTranslators() { return array( 'exists' => array($this, 'translateExists'), '=' => array($this, 'translateEquals'), '~=' => array($this, 'translateIncludes'), '|=' => array($this, 'translateDashMatch'), '^=' => array($this, 'translatePrefixMatch'), '$=' => array($this, 'translateSuffixMatch'), '*=' => array($this, 'translateSubstringMatch'), '!=' => array($this, 'translateDifferent'), ); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateExists(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition($attribute); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateEquals(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value))); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateIncludes(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition($value ? sprintf( '%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)', $attribute, Translator::getXpathLiteral(' '.$value.' ') ) : '0'); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateDashMatch(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition(sprintf( '%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))', $attribute, Translator::getXpathLiteral($value), Translator::getXpathLiteral($value.'-') )); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translatePrefixMatch(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition($value ? sprintf( '%1$s and starts-with(%1$s, %2$s)', $attribute, Translator::getXpathLiteral($value) ) : '0'); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateSuffixMatch(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition($value ? sprintf( '%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s', $attribute, strlen($value) - 1, Translator::getXpathLiteral($value) ) : '0'); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateSubstringMatch(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition($value ? sprintf( '%1$s and contains(%1$s, %2$s)', $attribute, Translator::getXpathLiteral($value) ) : '0'); } /** * @param XPathExpr $xpath * @param string $attribute * @param string $value * * @return XPathExpr */ public function translateDifferent(XPathExpr $xpath, $attribute, $value) { return $xpath->addCondition(sprintf( $value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s', $attribute, Translator::getXpathLiteral($value) )); } /** * {@inheritdoc} */ public function getName() { return 'attribute-matching'; } } vendor/symfony/css-selector/XPath/Extension/AbstractExtension.php 0000644 00000002353 15132754523 0021336 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; /** * XPath expression translator abstract extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ abstract class AbstractExtension implements ExtensionInterface { /** * {@inheritdoc} */ public function getNodeTranslators() { return array(); } /** * {@inheritdoc} */ public function getCombinationTranslators() { return array(); } /** * {@inheritdoc} */ public function getFunctionTranslators() { return array(); } /** * {@inheritdoc} */ public function getPseudoClassTranslators() { return array(); } /** * {@inheritdoc} */ public function getAttributeMatchingTranslators() { return array(); } } vendor/symfony/css-selector/XPath/Extension/FunctionExtension.php 0000644 00000013635 15132754523 0021365 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\XPath\Extension; use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\Parser\Parser; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; /** * XPath expression translator function extension. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> * * @internal */ class FunctionExtension extends AbstractExtension { /** * {@inheritdoc} */ public function getFunctionTranslators() { return array( 'nth-child' => array($this, 'translateNthChild'), 'nth-last-child' => array($this, 'translateNthLastChild'), 'nth-of-type' => array($this, 'translateNthOfType'), 'nth-last-of-type' => array($this, 'translateNthLastOfType'), 'contains' => array($this, 'translateContains'), 'lang' => array($this, 'translateLang'), ); } /** * @param XPathExpr $xpath * @param FunctionNode $function * @param bool $last * @param bool $addNameTest * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true) { try { list($a, $b) = Parser::parseSeries($function->getArguments()); } catch (SyntaxErrorException $e) { throw new ExpressionErrorException(sprintf('Invalid series: %s', implode(', ', $function->getArguments())), 0, $e); } $xpath->addStarPrefix(); if ($addNameTest) { $xpath->addNameTest(); } if (0 === $a) { return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b)); } if ($a < 0) { if ($b < 1) { return $xpath->addCondition('false()'); } $sign = '<='; } else { $sign = '>='; } $expr = 'position()'; if ($last) { $expr = 'last() - '.$expr; --$b; } if (0 !== $b) { $expr .= ' - '.$b; } $conditions = array(sprintf('%s %s 0', $expr, $sign)); if (1 !== $a && -1 !== $a) { $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a); } return $xpath->addCondition(implode(' and ', $conditions)); // todo: handle an+b, odd, even // an+b means every-a, plus b, e.g., 2n+1 means odd // 0n+b means b // n+0 means a=1, i.e., all elements // an means every a elements, i.e., 2n means even // -n means -1n // -1n+6 means elements 6 and previous } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr */ public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function) { return $this->translateNthChild($xpath, $function, true); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr */ public function translateNthOfType(XPathExpr $xpath, FunctionNode $function) { return $this->translateNthChild($xpath, $function, false, false); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function) { if ('*' === $xpath->getElement()) { throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.'); } return $this->translateNthChild($xpath, $function, true, false); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateContains(XPathExpr $xpath, FunctionNode $function) { $arguments = $function->getArguments(); foreach ($arguments as $token) { if (!($token->isString() || $token->isIdentifier())) { throw new ExpressionErrorException( 'Expected a single string or identifier for :contains(), got ' .implode(', ', $arguments) ); } } return $xpath->addCondition(sprintf( 'contains(string(.), %s)', Translator::getXpathLiteral($arguments[0]->getValue()) )); } /** * @param XPathExpr $xpath * @param FunctionNode $function * * @return XPathExpr * * @throws ExpressionErrorException */ public function translateLang(XPathExpr $xpath, FunctionNode $function) { $arguments = $function->getArguments(); foreach ($arguments as $token) { if (!($token->isString() || $token->isIdentifier())) { throw new ExpressionErrorException( 'Expected a single string or identifier for :lang(), got ' .implode(', ', $arguments) ); } } return $xpath->addCondition(sprintf( 'lang(%s)', Translator::getXpathLiteral($arguments[0]->getValue()) )); } /** * {@inheritdoc} */ public function getName() { return 'function'; } } vendor/symfony/css-selector/LICENSE 0000644 00000002051 15132754523 0013165 0 ustar 00 Copyright (c) 2004-2017 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/symfony/css-selector/Tests/Node/NegationNodeTest.php 0000644 00000001663 15132754523 0020102 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ClassNode; use Symfony\Component\CssSelector\Node\NegationNode; use Symfony\Component\CssSelector\Node\ElementNode; class NegationNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 'Negation[Element[*]:not(Class[Element[*].class])]'), ); } public function getSpecificityValueTestData() { return array( array(new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 10), ); } } vendor/symfony/css-selector/Tests/Node/AbstractNodeTest.php 0000644 00000001717 15132754523 0020101 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\NodeInterface; abstract class AbstractNodeTest extends TestCase { /** @dataProvider getToStringConversionTestData */ public function testToStringConversion(NodeInterface $node, $representation) { $this->assertEquals($representation, (string) $node); } /** @dataProvider getSpecificityValueTestData */ public function testSpecificityValue(NodeInterface $node, $value) { $this->assertEquals($value, $node->getSpecificity()->getValue()); } abstract public function getToStringConversionTestData(); abstract public function getSpecificityValueTestData(); } vendor/symfony/css-selector/Tests/Node/ClassNodeTest.php 0000644 00000001550 15132754523 0017376 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ClassNode; use Symfony\Component\CssSelector\Node\ElementNode; class ClassNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new ClassNode(new ElementNode(), 'class'), 'Class[Element[*].class]'), ); } public function getSpecificityValueTestData() { return array( array(new ClassNode(new ElementNode(), 'class'), 10), array(new ClassNode(new ElementNode(null, 'element'), 'class'), 11), ); } } vendor/symfony/css-selector/Tests/Node/CombinedSelectorNodeTest.php 0000644 00000002343 15132754523 0021553 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\CombinedSelectorNode; use Symfony\Component\CssSelector\Node\ElementNode; class CombinedSelectorNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 'CombinedSelector[Element[*] > Element[*]]'), array(new CombinedSelectorNode(new ElementNode(), ' ', new ElementNode()), 'CombinedSelector[Element[*] <followed> Element[*]]'), ); } public function getSpecificityValueTestData() { return array( array(new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 0), array(new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode()), 1), array(new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode(null, 'element')), 2), ); } } vendor/symfony/css-selector/Tests/Node/FunctionNodeTest.php 0000644 00000003242 15132754523 0020116 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\Parser\Token; class FunctionNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new FunctionNode(new ElementNode(), 'function'), 'Function[Element[*]:function()]'), array(new FunctionNode(new ElementNode(), 'function', array( new Token(Token::TYPE_IDENTIFIER, 'value', 0), )), "Function[Element[*]:function(['value'])]"), array(new FunctionNode(new ElementNode(), 'function', array( new Token(Token::TYPE_STRING, 'value1', 0), new Token(Token::TYPE_NUMBER, 'value2', 0), )), "Function[Element[*]:function(['value1', 'value2'])]"), ); } public function getSpecificityValueTestData() { return array( array(new FunctionNode(new ElementNode(), 'function'), 10), array(new FunctionNode(new ElementNode(), 'function', array( new Token(Token::TYPE_IDENTIFIER, 'value', 0), )), 10), array(new FunctionNode(new ElementNode(), 'function', array( new Token(Token::TYPE_STRING, 'value1', 0), new Token(Token::TYPE_NUMBER, 'value2', 0), )), 10), ); } } vendor/symfony/css-selector/Tests/Node/HashNodeTest.php 0000644 00000001526 15132754523 0017217 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\HashNode; use Symfony\Component\CssSelector\Node\ElementNode; class HashNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new HashNode(new ElementNode(), 'id'), 'Hash[Element[*]#id]'), ); } public function getSpecificityValueTestData() { return array( array(new HashNode(new ElementNode(), 'id'), 100), array(new HashNode(new ElementNode(null, 'id'), 'class'), 101), ); } } vendor/symfony/css-selector/Tests/Node/ElementNodeTest.php 0000644 00000001703 15132754523 0017722 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ElementNode; class ElementNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new ElementNode(), 'Element[*]'), array(new ElementNode(null, 'element'), 'Element[element]'), array(new ElementNode('namespace', 'element'), 'Element[namespace|element]'), ); } public function getSpecificityValueTestData() { return array( array(new ElementNode(), 0), array(new ElementNode(null, 'element'), 1), array(new ElementNode('namespace', 'element'), 1), ); } } vendor/symfony/css-selector/Tests/Node/AttributeNodeTest.php 0000644 00000002675 15132754523 0020305 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\AttributeNode; use Symfony\Component\CssSelector\Node\ElementNode; class AttributeNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 'Attribute[Element[*][attribute]]'), array(new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), "Attribute[Element[*][attribute $= 'value']]"), array(new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), "Attribute[Element[*][namespace|attribute $= 'value']]"), ); } public function getSpecificityValueTestData() { return array( array(new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 10), array(new AttributeNode(new ElementNode(null, 'element'), null, 'attribute', 'exists', null), 11), array(new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), 10), array(new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), 10), ); } } vendor/symfony/css-selector/Tests/Node/SelectorNodeTest.php 0000644 00000001664 15132754523 0020117 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\SelectorNode; class SelectorNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new SelectorNode(new ElementNode()), 'Selector[Element[*]]'), array(new SelectorNode(new ElementNode(), 'pseudo'), 'Selector[Element[*]::pseudo]'), ); } public function getSpecificityValueTestData() { return array( array(new SelectorNode(new ElementNode()), 0), array(new SelectorNode(new ElementNode(), 'pseudo'), 1), ); } } vendor/symfony/css-selector/Tests/Node/PseudoNodeTest.php 0000644 00000001437 15132754523 0017574 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Node\PseudoNode; class PseudoNodeTest extends AbstractNodeTest { public function getToStringConversionTestData() { return array( array(new PseudoNode(new ElementNode(), 'pseudo'), 'Pseudo[Element[*]:pseudo]'), ); } public function getSpecificityValueTestData() { return array( array(new PseudoNode(new ElementNode(), 'pseudo'), 10), ); } } vendor/symfony/css-selector/Tests/Node/SpecificityTest.php 0000644 00000004204 15132754523 0017775 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\Specificity; class SpecificityTest extends TestCase { /** @dataProvider getValueTestData */ public function testValue(Specificity $specificity, $value) { $this->assertEquals($value, $specificity->getValue()); } /** @dataProvider getValueTestData */ public function testPlusValue(Specificity $specificity, $value) { $this->assertEquals($value + 123, $specificity->plus(new Specificity(1, 2, 3))->getValue()); } public function getValueTestData() { return array( array(new Specificity(0, 0, 0), 0), array(new Specificity(0, 0, 2), 2), array(new Specificity(0, 3, 0), 30), array(new Specificity(4, 0, 0), 400), array(new Specificity(4, 3, 2), 432), ); } /** @dataProvider getCompareTestData */ public function testCompareTo(Specificity $a, Specificity $b, $result) { $this->assertEquals($result, $a->compareTo($b)); } public function getCompareTestData() { return array( array(new Specificity(0, 0, 0), new Specificity(0, 0, 0), 0), array(new Specificity(0, 0, 1), new Specificity(0, 0, 1), 0), array(new Specificity(0, 0, 2), new Specificity(0, 0, 1), 1), array(new Specificity(0, 0, 2), new Specificity(0, 0, 3), -1), array(new Specificity(0, 4, 0), new Specificity(0, 4, 0), 0), array(new Specificity(0, 6, 0), new Specificity(0, 5, 11), 1), array(new Specificity(0, 7, 0), new Specificity(0, 8, 0), -1), array(new Specificity(9, 0, 0), new Specificity(9, 0, 0), 0), array(new Specificity(11, 0, 0), new Specificity(10, 11, 0), 1), array(new Specificity(12, 11, 0), new Specificity(13, 0, 0), -1), ); } } vendor/symfony/css-selector/Tests/XPath/TranslatorTest.php 0000644 00000041515 15132754523 0020020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\XPath; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; use Symfony\Component\CssSelector\XPath\Translator; class TranslatorTest extends TestCase { /** @dataProvider getXpathLiteralTestData */ public function testXpathLiteral($value, $literal) { $this->assertEquals($literal, Translator::getXpathLiteral($value)); } /** @dataProvider getCssToXPathTestData */ public function testCssToXPath($css, $xpath) { $translator = new Translator(); $translator->registerExtension(new HtmlExtension($translator)); $this->assertEquals($xpath, $translator->cssToXPath($css, '')); } /** @dataProvider getXmlLangTestData */ public function testXmlLang($css, array $elementsId) { $translator = new Translator(); $document = new \SimpleXMLElement(file_get_contents(__DIR__.'/Fixtures/lang.xml')); $elements = $document->xpath($translator->cssToXPath($css)); $this->assertEquals(count($elementsId), count($elements)); foreach ($elements as $element) { $this->assertTrue(in_array($element->attributes()->id, $elementsId)); } } /** @dataProvider getHtmlIdsTestData */ public function testHtmlIds($css, array $elementsId) { $translator = new Translator(); $translator->registerExtension(new HtmlExtension($translator)); $document = new \DOMDocument(); $document->strictErrorChecking = false; $internalErrors = libxml_use_internal_errors(true); $document->loadHTMLFile(__DIR__.'/Fixtures/ids.html'); $document = simplexml_import_dom($document); $elements = $document->xpath($translator->cssToXPath($css)); $this->assertCount(count($elementsId), $elementsId); foreach ($elements as $element) { if (null !== $element->attributes()->id) { $this->assertTrue(in_array($element->attributes()->id, $elementsId)); } } libxml_clear_errors(); libxml_use_internal_errors($internalErrors); } /** @dataProvider getHtmlShakespearTestData */ public function testHtmlShakespear($css, $count) { $translator = new Translator(); $translator->registerExtension(new HtmlExtension($translator)); $document = new \DOMDocument(); $document->strictErrorChecking = false; $document->loadHTMLFile(__DIR__.'/Fixtures/shakespear.html'); $document = simplexml_import_dom($document); $bodies = $document->xpath('//body'); $elements = $bodies[0]->xpath($translator->cssToXPath($css)); $this->assertCount($count, $elements); } public function getXpathLiteralTestData() { return array( array('foo', "'foo'"), array("foo's bar", '"foo\'s bar"'), array("foo's \"middle\" bar", 'concat(\'foo\', "\'", \'s "middle" bar\')'), array("foo's 'middle' \"bar\"", 'concat(\'foo\', "\'", \'s \', "\'", \'middle\', "\'", \' "bar"\')'), ); } public function getCssToXPathTestData() { return array( array('*', '*'), array('e', 'e'), array('*|e', 'e'), array('e|f', 'e:f'), array('e[foo]', 'e[@foo]'), array('e[foo|bar]', 'e[@foo:bar]'), array('e[foo="bar"]', "e[@foo = 'bar']"), array('e[foo~="bar"]', "e[@foo and contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]"), array('e[foo^="bar"]', "e[@foo and starts-with(@foo, 'bar')]"), array('e[foo$="bar"]', "e[@foo and substring(@foo, string-length(@foo)-2) = 'bar']"), array('e[foo*="bar"]', "e[@foo and contains(@foo, 'bar')]"), array('e[hreflang|="en"]', "e[@hreflang and (@hreflang = 'en' or starts-with(@hreflang, 'en-'))]"), array('e:nth-child(1)', "*/*[name() = 'e' and (position() = 1)]"), array('e:nth-last-child(1)', "*/*[name() = 'e' and (position() = last() - 0)]"), array('e:nth-last-child(2n+2)', "*/*[name() = 'e' and (last() - position() - 1 >= 0 and (last() - position() - 1) mod 2 = 0)]"), array('e:nth-of-type(1)', '*/e[position() = 1]'), array('e:nth-last-of-type(1)', '*/e[position() = last() - 0]'), array('div e:nth-last-of-type(1) .aclass', "div/descendant-or-self::*/e[position() = last() - 0]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' aclass ')]"), array('e:first-child', "*/*[name() = 'e' and (position() = 1)]"), array('e:last-child', "*/*[name() = 'e' and (position() = last())]"), array('e:first-of-type', '*/e[position() = 1]'), array('e:last-of-type', '*/e[position() = last()]'), array('e:only-child', "*/*[name() = 'e' and (last() = 1)]"), array('e:only-of-type', 'e[last() = 1]'), array('e:empty', 'e[not(*) and not(string-length())]'), array('e:EmPTY', 'e[not(*) and not(string-length())]'), array('e:root', 'e[not(parent::*)]'), array('e:hover', 'e[0]'), array('e:contains("foo")', "e[contains(string(.), 'foo')]"), array('e:ConTains(foo)', "e[contains(string(.), 'foo')]"), array('e.warning', "e[@class and contains(concat(' ', normalize-space(@class), ' '), ' warning ')]"), array('e#myid', "e[@id = 'myid']"), array('e:not(:nth-child(odd))', 'e[not(position() - 1 >= 0 and (position() - 1) mod 2 = 0)]'), array('e:nOT(*)', 'e[0]'), array('e f', 'e/descendant-or-self::*/f'), array('e > f', 'e/f'), array('e + f', "e/following-sibling::*[name() = 'f' and (position() = 1)]"), array('e ~ f', 'e/following-sibling::f'), array('div#container p', "div[@id = 'container']/descendant-or-self::*/p"), ); } public function getXmlLangTestData() { return array( array(':lang("EN")', array('first', 'second', 'third', 'fourth')), array(':lang("en-us")', array('second', 'fourth')), array(':lang(en-nz)', array('third')), array(':lang(fr)', array('fifth')), array(':lang(ru)', array('sixth')), array(":lang('ZH')", array('eighth')), array(':lang(de) :lang(zh)', array('eighth')), array(':lang(en), :lang(zh)', array('first', 'second', 'third', 'fourth', 'eighth')), array(':lang(es)', array()), ); } public function getHtmlIdsTestData() { return array( array('div', array('outer-div', 'li-div', 'foobar-div')), array('DIV', array('outer-div', 'li-div', 'foobar-div')), // case-insensitive in HTML array('div div', array('li-div')), array('div, div div', array('outer-div', 'li-div', 'foobar-div')), array('a[name]', array('name-anchor')), array('a[NAme]', array('name-anchor')), // case-insensitive in HTML: array('a[rel]', array('tag-anchor', 'nofollow-anchor')), array('a[rel="tag"]', array('tag-anchor')), array('a[href*="localhost"]', array('tag-anchor')), array('a[href*=""]', array()), array('a[href^="http"]', array('tag-anchor', 'nofollow-anchor')), array('a[href^="http:"]', array('tag-anchor')), array('a[href^=""]', array()), array('a[href$="org"]', array('nofollow-anchor')), array('a[href$=""]', array()), array('div[foobar~="bc"]', array('foobar-div')), array('div[foobar~="cde"]', array('foobar-div')), array('[foobar~="ab bc"]', array('foobar-div')), array('[foobar~=""]', array()), array('[foobar~=" \t"]', array()), array('div[foobar~="cd"]', array()), array('*[lang|="En"]', array('second-li')), array('[lang|="En-us"]', array('second-li')), // Attribute values are case sensitive array('*[lang|="en"]', array()), array('[lang|="en-US"]', array()), array('*[lang|="e"]', array()), // ... :lang() is not. array(':lang("EN")', array('second-li', 'li-div')), array('*:lang(en-US)', array('second-li', 'li-div')), array(':lang("e")', array()), array('li:nth-child(3)', array('third-li')), array('li:nth-child(10)', array()), array('li:nth-child(2n)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-child(even)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-child(2n+0)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-child(+2n+1)', array('first-li', 'third-li', 'fifth-li', 'seventh-li')), array('li:nth-child(odd)', array('first-li', 'third-li', 'fifth-li', 'seventh-li')), array('li:nth-child(2n+4)', array('fourth-li', 'sixth-li')), array('li:nth-child(3n+1)', array('first-li', 'fourth-li', 'seventh-li')), array('li:nth-child(n)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-child(n-1)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-child(n+1)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-child(n+3)', array('third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-child(-n)', array()), array('li:nth-child(-n-1)', array()), array('li:nth-child(-n+1)', array('first-li')), array('li:nth-child(-n+3)', array('first-li', 'second-li', 'third-li')), array('li:nth-last-child(0)', array()), array('li:nth-last-child(2n)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-last-child(even)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-last-child(2n+2)', array('second-li', 'fourth-li', 'sixth-li')), array('li:nth-last-child(n)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-last-child(n-1)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-last-child(n-3)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-last-child(n+1)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li')), array('li:nth-last-child(n+3)', array('first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li')), array('li:nth-last-child(-n)', array()), array('li:nth-last-child(-n-1)', array()), array('li:nth-last-child(-n+1)', array('seventh-li')), array('li:nth-last-child(-n+3)', array('fifth-li', 'sixth-li', 'seventh-li')), array('ol:first-of-type', array('first-ol')), array('ol:nth-child(1)', array('first-ol')), array('ol:nth-of-type(2)', array('second-ol')), array('ol:nth-last-of-type(1)', array('second-ol')), array('span:only-child', array('foobar-span')), array('li div:only-child', array('li-div')), array('div *:only-child', array('li-div', 'foobar-span')), array('p:only-of-type', array('paragraph')), array('a:empty', array('name-anchor')), array('a:EMpty', array('name-anchor')), array('li:empty', array('third-li', 'fourth-li', 'fifth-li', 'sixth-li')), array(':root', array('html')), array('html:root', array('html')), array('li:root', array()), array('* :root', array()), array('*:contains("link")', array('html', 'outer-div', 'tag-anchor', 'nofollow-anchor')), array(':CONtains("link")', array('html', 'outer-div', 'tag-anchor', 'nofollow-anchor')), array('*:contains("LInk")', array()), // case sensitive array('*:contains("e")', array('html', 'nil', 'outer-div', 'first-ol', 'first-li', 'paragraph', 'p-em')), array('*:contains("E")', array()), // case-sensitive array('.a', array('first-ol')), array('.b', array('first-ol')), array('*.a', array('first-ol')), array('ol.a', array('first-ol')), array('.c', array('first-ol', 'third-li', 'fourth-li')), array('*.c', array('first-ol', 'third-li', 'fourth-li')), array('ol *.c', array('third-li', 'fourth-li')), array('ol li.c', array('third-li', 'fourth-li')), array('li ~ li.c', array('third-li', 'fourth-li')), array('ol > li.c', array('third-li', 'fourth-li')), array('#first-li', array('first-li')), array('li#first-li', array('first-li')), array('*#first-li', array('first-li')), array('li div', array('li-div')), array('li > div', array('li-div')), array('div div', array('li-div')), array('div > div', array()), array('div>.c', array('first-ol')), array('div > .c', array('first-ol')), array('div + div', array('foobar-div')), array('a ~ a', array('tag-anchor', 'nofollow-anchor')), array('a[rel="tag"] ~ a', array('nofollow-anchor')), array('ol#first-ol li:last-child', array('seventh-li')), array('ol#first-ol *:last-child', array('li-div', 'seventh-li')), array('#outer-div:first-child', array('outer-div')), array('#outer-div :first-child', array('name-anchor', 'first-li', 'li-div', 'p-b', 'checkbox-fieldset-disabled', 'area-href')), array('a[href]', array('tag-anchor', 'nofollow-anchor')), array(':not(*)', array()), array('a:not([href])', array('name-anchor')), array('ol :Not(li[class])', array('first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li')), // HTML-specific array(':link', array('link-href', 'tag-anchor', 'nofollow-anchor', 'area-href')), array(':visited', array()), array(':enabled', array('link-href', 'tag-anchor', 'nofollow-anchor', 'checkbox-unchecked', 'text-checked', 'checkbox-checked', 'area-href')), array(':disabled', array('checkbox-disabled', 'checkbox-disabled-checked', 'fieldset', 'checkbox-fieldset-disabled')), array(':checked', array('checkbox-checked', 'checkbox-disabled-checked')), ); } public function getHtmlShakespearTestData() { return array( array('*', 246), array('div:contains(CELIA)', 26), array('div:only-child', 22), // ? array('div:nth-child(even)', 106), array('div:nth-child(2n)', 106), array('div:nth-child(odd)', 137), array('div:nth-child(2n+1)', 137), array('div:nth-child(n)', 243), array('div:last-child', 53), array('div:first-child', 51), array('div > div', 242), array('div + div', 190), array('div ~ div', 190), array('body', 1), array('body div', 243), array('div', 243), array('div div', 242), array('div div div', 241), array('div, div, div', 243), array('div, a, span', 243), array('.dialog', 51), array('div.dialog', 51), array('div .dialog', 51), array('div.character, div.dialog', 99), array('div.direction.dialog', 0), array('div.dialog.direction', 0), array('div.dialog.scene', 1), array('div.scene.scene', 1), array('div.scene .scene', 0), array('div.direction .dialog ', 0), array('div .dialog .direction', 4), array('div.dialog .dialog .direction', 4), array('#speech5', 1), array('div#speech5', 1), array('div #speech5', 1), array('div.scene div.dialog', 49), array('div#scene1 div.dialog div', 142), array('#scene1 #speech1', 1), array('div[class]', 103), array('div[class=dialog]', 50), array('div[class^=dia]', 51), array('div[class$=log]', 50), array('div[class*=sce]', 1), array('div[class|=dialog]', 50), // ? Seems right array('div[class!=madeup]', 243), // ? Seems right array('div[class~=dialog]', 51), // ? Seems right ); } } vendor/symfony/css-selector/Tests/XPath/Fixtures/ids.html 0000644 00000003067 15132754523 0017574 0 ustar 00 <html id="html"><head> <link id="link-href" href="foo" /> <link id="link-nohref" /> </head><body> <div id="outer-div"> <a id="name-anchor" name="foo"></a> <a id="tag-anchor" rel="tag" href="http://localhost/foo">link</a> <a id="nofollow-anchor" rel="nofollow" href="https://example.org"> link</a> <ol id="first-ol" class="a b c"> <li id="first-li">content</li> <li id="second-li" lang="En-us"> <div id="li-div"> </div> </li> <li id="third-li" class="ab c"></li> <li id="fourth-li" class="ab c"></li> <li id="fifth-li"></li> <li id="sixth-li"></li> <li id="seventh-li"> </li> </ol> <p id="paragraph"> <b id="p-b">hi</b> <em id="p-em">there</em> <b id="p-b2">guy</b> <input type="checkbox" id="checkbox-unchecked" /> <input type="checkbox" id="checkbox-disabled" disabled="" /> <input type="text" id="text-checked" checked="checked" /> <input type="hidden" /> <input type="hidden" disabled="disabled" /> <input type="checkbox" id="checkbox-checked" checked="checked" /> <input type="checkbox" id="checkbox-disabled-checked" disabled="disabled" checked="checked" /> <fieldset id="fieldset" disabled="disabled"> <input type="checkbox" id="checkbox-fieldset-disabled" /> <input type="hidden" /> </fieldset> </p> <ol id="second-ol"> </ol> <map name="dummymap"> <area shape="circle" coords="200,250,25" href="foo.html" id="area-href" /> <area shape="default" id="area-nohref" /> </map> </div> <div id="foobar-div" foobar="ab bc cde"><span id="foobar-span"></span></div> </body></html> vendor/symfony/css-selector/Tests/XPath/Fixtures/shakespear.html 0000644 00000035204 15132754523 0021141 0 ustar 00 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" debug="true"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> </head> <body> <div id="test"> <div class="dialog"> <h2>As You Like It</h2> <div id="playwright"> by William Shakespeare </div> <div class="dialog scene thirdClass" id="scene1"> <h3>ACT I, SCENE III. A room in the palace.</h3> <div class="dialog"> <div class="direction">Enter CELIA and ROSALIND</div> </div> <div id="speech1" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.1">Why, cousin! why, Rosalind! Cupid have mercy! not a word?</div> </div> <div id="speech2" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.2">Not one to throw at a dog.</div> </div> <div id="speech3" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.3">No, thy words are too precious to be cast away upon</div> <div id="scene1.3.4">curs; throw some of them at me; come, lame me with reasons.</div> </div> <div id="speech4" class="character">ROSALIND</div> <div id="speech5" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.8">But is all this for your father?</div> </div> <div class="dialog"> <div id="scene1.3.5">Then there were two cousins laid up; when the one</div> <div id="scene1.3.6">should be lamed with reasons and the other mad</div> <div id="scene1.3.7">without any.</div> </div> <div id="speech6" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.9">No, some of it is for my child's father. O, how</div> <div id="scene1.3.10">full of briers is this working-day world!</div> </div> <div id="speech7" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.11">They are but burs, cousin, thrown upon thee in</div> <div id="scene1.3.12">holiday foolery: if we walk not in the trodden</div> <div id="scene1.3.13">paths our very petticoats will catch them.</div> </div> <div id="speech8" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.14">I could shake them off my coat: these burs are in my heart.</div> </div> <div id="speech9" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.15">Hem them away.</div> </div> <div id="speech10" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.16">I would try, if I could cry 'hem' and have him.</div> </div> <div id="speech11" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.17">Come, come, wrestle with thy affections.</div> </div> <div id="speech12" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.18">O, they take the part of a better wrestler than myself!</div> </div> <div id="speech13" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.19">O, a good wish upon you! you will try in time, in</div> <div id="scene1.3.20">despite of a fall. But, turning these jests out of</div> <div id="scene1.3.21">service, let us talk in good earnest: is it</div> <div id="scene1.3.22">possible, on such a sudden, you should fall into so</div> <div id="scene1.3.23">strong a liking with old Sir Rowland's youngest son?</div> </div> <div id="speech14" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.24">The duke my father loved his father dearly.</div> </div> <div id="speech15" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.25">Doth it therefore ensue that you should love his son</div> <div id="scene1.3.26">dearly? By this kind of chase, I should hate him,</div> <div id="scene1.3.27">for my father hated his father dearly; yet I hate</div> <div id="scene1.3.28">not Orlando.</div> </div> <div id="speech16" class="character">ROSALIND</div> <div title="wtf" class="dialog"> <div id="scene1.3.29">No, faith, hate him not, for my sake.</div> </div> <div id="speech17" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.30">Why should I not? doth he not deserve well?</div> </div> <div id="speech18" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.31">Let me love him for that, and do you love him</div> <div id="scene1.3.32">because I do. Look, here comes the duke.</div> </div> <div id="speech19" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.33">With his eyes full of anger.</div> <div class="direction">Enter DUKE FREDERICK, with Lords</div> </div> <div id="speech20" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.34">Mistress, dispatch you with your safest haste</div> <div id="scene1.3.35">And get you from our court.</div> </div> <div id="speech21" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.36">Me, uncle?</div> </div> <div id="speech22" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.37">You, cousin</div> <div id="scene1.3.38">Within these ten days if that thou be'st found</div> <div id="scene1.3.39">So near our public court as twenty miles,</div> <div id="scene1.3.40">Thou diest for it.</div> </div> <div id="speech23" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.41"> I do beseech your grace,</div> <div id="scene1.3.42">Let me the knowledge of my fault bear with me:</div> <div id="scene1.3.43">If with myself I hold intelligence</div> <div id="scene1.3.44">Or have acquaintance with mine own desires,</div> <div id="scene1.3.45">If that I do not dream or be not frantic,--</div> <div id="scene1.3.46">As I do trust I am not--then, dear uncle,</div> <div id="scene1.3.47">Never so much as in a thought unborn</div> <div id="scene1.3.48">Did I offend your highness.</div> </div> <div id="speech24" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.49">Thus do all traitors:</div> <div id="scene1.3.50">If their purgation did consist in words,</div> <div id="scene1.3.51">They are as innocent as grace itself:</div> <div id="scene1.3.52">Let it suffice thee that I trust thee not.</div> </div> <div id="speech25" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.53">Yet your mistrust cannot make me a traitor:</div> <div id="scene1.3.54">Tell me whereon the likelihood depends.</div> </div> <div id="speech26" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.55">Thou art thy father's daughter; there's enough.</div> </div> <div id="speech27" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.56">So was I when your highness took his dukedom;</div> <div id="scene1.3.57">So was I when your highness banish'd him:</div> <div id="scene1.3.58">Treason is not inherited, my lord;</div> <div id="scene1.3.59">Or, if we did derive it from our friends,</div> <div id="scene1.3.60">What's that to me? my father was no traitor:</div> <div id="scene1.3.61">Then, good my liege, mistake me not so much</div> <div id="scene1.3.62">To think my poverty is treacherous.</div> </div> <div id="speech28" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.63">Dear sovereign, hear me speak.</div> </div> <div id="speech29" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.64">Ay, Celia; we stay'd her for your sake,</div> <div id="scene1.3.65">Else had she with her father ranged along.</div> </div> <div id="speech30" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.66">I did not then entreat to have her stay;</div> <div id="scene1.3.67">It was your pleasure and your own remorse:</div> <div id="scene1.3.68">I was too young that time to value her;</div> <div id="scene1.3.69">But now I know her: if she be a traitor,</div> <div id="scene1.3.70">Why so am I; we still have slept together,</div> <div id="scene1.3.71">Rose at an instant, learn'd, play'd, eat together,</div> <div id="scene1.3.72">And wheresoever we went, like Juno's swans,</div> <div id="scene1.3.73">Still we went coupled and inseparable.</div> </div> <div id="speech31" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.74">She is too subtle for thee; and her smoothness,</div> <div id="scene1.3.75">Her very silence and her patience</div> <div id="scene1.3.76">Speak to the people, and they pity her.</div> <div id="scene1.3.77">Thou art a fool: she robs thee of thy name;</div> <div id="scene1.3.78">And thou wilt show more bright and seem more virtuous</div> <div id="scene1.3.79">When she is gone. Then open not thy lips:</div> <div id="scene1.3.80">Firm and irrevocable is my doom</div> <div id="scene1.3.81">Which I have pass'd upon her; she is banish'd.</div> </div> <div id="speech32" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.82">Pronounce that sentence then on me, my liege:</div> <div id="scene1.3.83">I cannot live out of her company.</div> </div> <div id="speech33" class="character">DUKE FREDERICK</div> <div class="dialog"> <div id="scene1.3.84">You are a fool. You, niece, provide yourself:</div> <div id="scene1.3.85">If you outstay the time, upon mine honour,</div> <div id="scene1.3.86">And in the greatness of my word, you die.</div> <div class="direction">Exeunt DUKE FREDERICK and Lords</div> </div> <div id="speech34" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.87">O my poor Rosalind, whither wilt thou go?</div> <div id="scene1.3.88">Wilt thou change fathers? I will give thee mine.</div> <div id="scene1.3.89">I charge thee, be not thou more grieved than I am.</div> </div> <div id="speech35" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.90">I have more cause.</div> </div> <div id="speech36" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.91"> Thou hast not, cousin;</div> <div id="scene1.3.92">Prithee be cheerful: know'st thou not, the duke</div> <div id="scene1.3.93">Hath banish'd me, his daughter?</div> </div> <div id="speech37" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.94">That he hath not.</div> </div> <div id="speech38" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.95">No, hath not? Rosalind lacks then the love</div> <div id="scene1.3.96">Which teacheth thee that thou and I am one:</div> <div id="scene1.3.97">Shall we be sunder'd? shall we part, sweet girl?</div> <div id="scene1.3.98">No: let my father seek another heir.</div> <div id="scene1.3.99">Therefore devise with me how we may fly,</div> <div id="scene1.3.100">Whither to go and what to bear with us;</div> <div id="scene1.3.101">And do not seek to take your change upon you,</div> <div id="scene1.3.102">To bear your griefs yourself and leave me out;</div> <div id="scene1.3.103">For, by this heaven, now at our sorrows pale,</div> <div id="scene1.3.104">Say what thou canst, I'll go along with thee.</div> </div> <div id="speech39" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.105">Why, whither shall we go?</div> </div> <div id="speech40" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.106">To seek my uncle in the forest of Arden.</div> </div> <div id="speech41" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.107">Alas, what danger will it be to us,</div> <div id="scene1.3.108">Maids as we are, to travel forth so far!</div> <div id="scene1.3.109">Beauty provoketh thieves sooner than gold.</div> </div> <div id="speech42" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.110">I'll put myself in poor and mean attire</div> <div id="scene1.3.111">And with a kind of umber smirch my face;</div> <div id="scene1.3.112">The like do you: so shall we pass along</div> <div id="scene1.3.113">And never stir assailants.</div> </div> <div id="speech43" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.114">Were it not better,</div> <div id="scene1.3.115">Because that I am more than common tall,</div> <div id="scene1.3.116">That I did suit me all points like a man?</div> <div id="scene1.3.117">A gallant curtle-axe upon my thigh,</div> <div id="scene1.3.118">A boar-spear in my hand; and--in my heart</div> <div id="scene1.3.119">Lie there what hidden woman's fear there will--</div> <div id="scene1.3.120">We'll have a swashing and a martial outside,</div> <div id="scene1.3.121">As many other mannish cowards have</div> <div id="scene1.3.122">That do outface it with their semblances.</div> </div> <div id="speech44" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.123">What shall I call thee when thou art a man?</div> </div> <div id="speech45" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.124">I'll have no worse a name than Jove's own page;</div> <div id="scene1.3.125">And therefore look you call me Ganymede.</div> <div id="scene1.3.126">But what will you be call'd?</div> </div> <div id="speech46" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.127">Something that hath a reference to my state</div> <div id="scene1.3.128">No longer Celia, but Aliena.</div> </div> <div id="speech47" class="character">ROSALIND</div> <div class="dialog"> <div id="scene1.3.129">But, cousin, what if we assay'd to steal</div> <div id="scene1.3.130">The clownish fool out of your father's court?</div> <div id="scene1.3.131">Would he not be a comfort to our travel?</div> </div> <div id="speech48" class="character">CELIA</div> <div class="dialog"> <div id="scene1.3.132">He'll go along o'er the wide world with me;</div> <div id="scene1.3.133">Leave me alone to woo him. Let's away,</div> <div id="scene1.3.134">And get our jewels and our wealth together,</div> <div id="scene1.3.135">Devise the fittest time and safest way</div> <div id="scene1.3.136">To hide us from pursuit that will be made</div> <div id="scene1.3.137">After my flight. Now go we in content</div> <div id="scene1.3.138">To liberty and not to banishment.</div> <div class="direction">Exeunt</div> </div> </div> </div> </div> </body> </html> vendor/symfony/css-selector/Tests/XPath/Fixtures/lang.xml 0000644 00000000475 15132754523 0017572 0 ustar 00 <test> <a id="first" xml:lang="en">a</a> <b id="second" xml:lang="en-US">b</b> <c id="third" xml:lang="en-Nz">c</c> <d id="fourth" xml:lang="En-us">d</d> <e id="fifth" xml:lang="fr">e</e> <f id="sixth" xml:lang="ru">f</f> <g id="seventh" xml:lang="de"> <h id="eighth" xml:lang="zh"/> </g> </test> vendor/symfony/css-selector/Tests/Parser/ParserTest.php 0000644 00000031351 15132754523 0017330 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Parser; use Symfony\Component\CssSelector\Parser\Token; class ParserTest extends TestCase { /** @dataProvider getParserTestData */ public function testParser($source, $representation) { $parser = new Parser(); $this->assertEquals($representation, array_map(function (SelectorNode $node) { return (string) $node->getTree(); }, $parser->parse($source))); } /** @dataProvider getParserExceptionTestData */ public function testParserException($source, $message) { $parser = new Parser(); try { $parser->parse($source); $this->fail('Parser should throw a SyntaxErrorException.'); } catch (SyntaxErrorException $e) { $this->assertEquals($message, $e->getMessage()); } } /** @dataProvider getPseudoElementsTestData */ public function testPseudoElements($source, $element, $pseudo) { $parser = new Parser(); $selectors = $parser->parse($source); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals($element, (string) $selector->getTree()); $this->assertEquals($pseudo, (string) $selector->getPseudoElement()); } /** @dataProvider getSpecificityTestData */ public function testSpecificity($source, $value) { $parser = new Parser(); $selectors = $parser->parse($source); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals($value, $selector->getSpecificity()->getValue()); } /** @dataProvider getParseSeriesTestData */ public function testParseSeries($series, $a, $b) { $parser = new Parser(); $selectors = $parser->parse(sprintf(':nth-child(%s)', $series)); $this->assertCount(1, $selectors); /** @var FunctionNode $function */ $function = $selectors[0]->getTree(); $this->assertEquals(array($a, $b), Parser::parseSeries($function->getArguments())); } /** @dataProvider getParseSeriesExceptionTestData */ public function testParseSeriesException($series) { $parser = new Parser(); $selectors = $parser->parse(sprintf(':nth-child(%s)', $series)); $this->assertCount(1, $selectors); /** @var FunctionNode $function */ $function = $selectors[0]->getTree(); $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\CssSelector\Exception\SyntaxErrorException'); Parser::parseSeries($function->getArguments()); } public function getParserTestData() { return array( array('*', array('Element[*]')), array('*|*', array('Element[*]')), array('*|foo', array('Element[foo]')), array('foo|*', array('Element[foo|*]')), array('foo|bar', array('Element[foo|bar]')), array('#foo#bar', array('Hash[Hash[Element[*]#foo]#bar]')), array('div>.foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')), array('div> .foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')), array('div >.foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')), array('div > .foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')), array("div \n> \t \t .foo", array('CombinedSelector[Element[div] > Class[Element[*].foo]]')), array('td.foo,.bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array('td.foo, .bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array("td.foo\t\r\n\f ,\t\r\n\f .bar", array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array('td.foo,.bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array('td.foo, .bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array("td.foo\t\r\n\f ,\t\r\n\f .bar", array('Class[Element[td].foo]', 'Class[Element[*].bar]')), array('div, td.foo, div.bar span', array('Element[div]', 'Class[Element[td].foo]', 'CombinedSelector[Class[Element[div].bar] <followed> Element[span]]')), array('div > p', array('CombinedSelector[Element[div] > Element[p]]')), array('td:first', array('Pseudo[Element[td]:first]')), array('td :first', array('CombinedSelector[Element[td] <followed> Pseudo[Element[*]:first]]')), array('a[name]', array('Attribute[Element[a][name]]')), array("a[ name\t]", array('Attribute[Element[a][name]]')), array('a [name]', array('CombinedSelector[Element[a] <followed> Attribute[Element[*][name]]]')), array('a[rel="include"]', array("Attribute[Element[a][rel = 'include']]")), array('a[rel = include]', array("Attribute[Element[a][rel = 'include']]")), array("a[hreflang |= 'en']", array("Attribute[Element[a][hreflang |= 'en']]")), array('a[hreflang|=en]', array("Attribute[Element[a][hreflang |= 'en']]")), array('div:nth-child(10)', array("Function[Element[div]:nth-child(['10'])]")), array(':nth-child(2n+2)', array("Function[Element[*]:nth-child(['2', 'n', '+2'])]")), array('div:nth-of-type(10)', array("Function[Element[div]:nth-of-type(['10'])]")), array('div div:nth-of-type(10) .aclass', array("CombinedSelector[CombinedSelector[Element[div] <followed> Function[Element[div]:nth-of-type(['10'])]] <followed> Class[Element[*].aclass]]")), array('label:only', array('Pseudo[Element[label]:only]')), array('a:lang(fr)', array("Function[Element[a]:lang(['fr'])]")), array('div:contains("foo")', array("Function[Element[div]:contains(['foo'])]")), array('div#foobar', array('Hash[Element[div]#foobar]')), array('div:not(div.foo)', array('Negation[Element[div]:not(Class[Element[div].foo])]')), array('td ~ th', array('CombinedSelector[Element[td] ~ Element[th]]')), array('.foo[data-bar][data-baz=0]', array("Attribute[Attribute[Class[Element[*].foo][data-bar]][data-baz = '0']]")), ); } public function getParserExceptionTestData() { return array( array('attributes(href)/html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()), array('attributes(href)', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()), array('html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '/', 4))->getMessage()), array(' ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 1))->getMessage()), array('div, ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 5))->getMessage()), array(' , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 1))->getMessage()), array('p, , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 3))->getMessage()), array('div > ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 6))->getMessage()), array(' > div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '>', 2))->getMessage()), array('foo|#bar', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_HASH, 'bar', 4))->getMessage()), array('#.foo', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '#', 0))->getMessage()), array('.#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()), array(':#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()), array('[*]', SyntaxErrorException::unexpectedToken('"|"', new Token(Token::TYPE_DELIMITER, ']', 2))->getMessage()), array('[foo|]', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_DELIMITER, ']', 5))->getMessage()), array('[#]', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_DELIMITER, '#', 1))->getMessage()), array('[foo=#]', SyntaxErrorException::unexpectedToken('string or identifier', new Token(Token::TYPE_DELIMITER, '#', 5))->getMessage()), array(':nth-child()', SyntaxErrorException::unexpectedToken('at least one argument', new Token(Token::TYPE_DELIMITER, ')', 11))->getMessage()), array('[href]a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_IDENTIFIER, 'a', 6))->getMessage()), array('[rel:stylesheet]', SyntaxErrorException::unexpectedToken('operator', new Token(Token::TYPE_DELIMITER, ':', 4))->getMessage()), array('[rel=stylesheet', SyntaxErrorException::unexpectedToken('"]"', new Token(Token::TYPE_FILE_END, '', 15))->getMessage()), array(':lang(fr', SyntaxErrorException::unexpectedToken('an argument', new Token(Token::TYPE_FILE_END, '', 8))->getMessage()), array(':contains("foo', SyntaxErrorException::unclosedString(10)->getMessage()), array('foo!', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '!', 3))->getMessage()), ); } public function getPseudoElementsTestData() { return array( array('foo', 'Element[foo]', ''), array('*', 'Element[*]', ''), array(':empty', 'Pseudo[Element[*]:empty]', ''), array(':BEfore', 'Element[*]', 'before'), array(':aftER', 'Element[*]', 'after'), array(':First-Line', 'Element[*]', 'first-line'), array(':First-Letter', 'Element[*]', 'first-letter'), array('::befoRE', 'Element[*]', 'before'), array('::AFter', 'Element[*]', 'after'), array('::firsT-linE', 'Element[*]', 'first-line'), array('::firsT-letteR', 'Element[*]', 'first-letter'), array('::Selection', 'Element[*]', 'selection'), array('foo:after', 'Element[foo]', 'after'), array('foo::selection', 'Element[foo]', 'selection'), array('lorem#ipsum ~ a#b.c[href]:empty::selection', 'CombinedSelector[Hash[Element[lorem]#ipsum] ~ Pseudo[Attribute[Class[Hash[Element[a]#b].c][href]]:empty]]', 'selection'), ); } public function getSpecificityTestData() { return array( array('*', 0), array(' foo', 1), array(':empty ', 10), array(':before', 1), array('*:before', 1), array(':nth-child(2)', 10), array('.bar', 10), array('[baz]', 10), array('[baz="4"]', 10), array('[baz^="4"]', 10), array('#lipsum', 100), array(':not(*)', 0), array(':not(foo)', 1), array(':not(.foo)', 10), array(':not([foo])', 10), array(':not(:empty)', 10), array(':not(#foo)', 100), array('foo:empty', 11), array('foo:before', 2), array('foo::before', 2), array('foo:empty::before', 12), array('#lorem + foo#ipsum:first-child > bar:first-line', 213), ); } public function getParseSeriesTestData() { return array( array('1n+3', 1, 3), array('1n +3', 1, 3), array('1n + 3', 1, 3), array('1n+ 3', 1, 3), array('1n-3', 1, -3), array('1n -3', 1, -3), array('1n - 3', 1, -3), array('1n- 3', 1, -3), array('n-5', 1, -5), array('odd', 2, 1), array('even', 2, 0), array('3n', 3, 0), array('n', 1, 0), array('+n', 1, 0), array('-n', -1, 0), array('5', 0, 5), ); } public function getParseSeriesExceptionTestData() { return array( array('foo'), array('n+'), ); } } vendor/symfony/css-selector/Tests/Parser/ReaderTest.php 0000644 00000005613 15132754523 0017300 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Parser\Reader; class ReaderTest extends TestCase { public function testIsEOF() { $reader = new Reader(''); $this->assertTrue($reader->isEOF()); $reader = new Reader('hello'); $this->assertFalse($reader->isEOF()); $this->assignPosition($reader, 2); $this->assertFalse($reader->isEOF()); $this->assignPosition($reader, 5); $this->assertTrue($reader->isEOF()); } public function testGetRemainingLength() { $reader = new Reader('hello'); $this->assertEquals(5, $reader->getRemainingLength()); $this->assignPosition($reader, 2); $this->assertEquals(3, $reader->getRemainingLength()); $this->assignPosition($reader, 5); $this->assertEquals(0, $reader->getRemainingLength()); } public function testGetSubstring() { $reader = new Reader('hello'); $this->assertEquals('he', $reader->getSubstring(2)); $this->assertEquals('el', $reader->getSubstring(2, 1)); $this->assignPosition($reader, 2); $this->assertEquals('ll', $reader->getSubstring(2)); $this->assertEquals('lo', $reader->getSubstring(2, 1)); } public function testGetOffset() { $reader = new Reader('hello'); $this->assertEquals(2, $reader->getOffset('ll')); $this->assertFalse($reader->getOffset('w')); $this->assignPosition($reader, 2); $this->assertEquals(0, $reader->getOffset('ll')); $this->assertFalse($reader->getOffset('he')); } public function testFindPattern() { $reader = new Reader('hello'); $this->assertFalse($reader->findPattern('/world/')); $this->assertEquals(array('hello', 'h'), $reader->findPattern('/^([a-z]).*/')); $this->assignPosition($reader, 2); $this->assertFalse($reader->findPattern('/^h.*/')); $this->assertEquals(array('llo'), $reader->findPattern('/^llo$/')); } public function testMoveForward() { $reader = new Reader('hello'); $this->assertEquals(0, $reader->getPosition()); $reader->moveForward(2); $this->assertEquals(2, $reader->getPosition()); } public function testToEnd() { $reader = new Reader('hello'); $reader->moveToEnd(); $this->assertTrue($reader->isEOF()); } private function assignPosition(Reader $reader, $value) { $position = new \ReflectionProperty($reader, 'position'); $position->setAccessible(true); $position->setValue($reader, $value); } } vendor/symfony/css-selector/Tests/Parser/Shortcut/HashParserTest.php 0000644 00000002562 15132754523 0021751 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Shortcut\HashParser; /** * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class HashParserTest extends TestCase { /** @dataProvider getParseTestData */ public function testParse($source, $representation) { $parser = new HashParser(); $selectors = $parser->parse($source); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals($representation, (string) $selector->getTree()); } public function getParseTestData() { return array( array('#testid', 'Hash[Element[*]#testid]'), array('testel#testid', 'Hash[Element[testel]#testid]'), array('testns|#testid', 'Hash[Element[testns|*]#testid]'), array('testns|*#testid', 'Hash[Element[testns|*]#testid]'), array('testns|testel#testid', 'Hash[Element[testns|testel]#testid]'), ); } } vendor/symfony/css-selector/Tests/Parser/Shortcut/EmptyStringParserTest.php 0000644 00000001777 15132754523 0023362 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; /** * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class EmptyStringParserTest extends TestCase { public function testParse() { $parser = new EmptyStringParser(); $selectors = $parser->parse(''); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals('Element[*]', (string) $selector->getTree()); $selectors = $parser->parse('this will produce an empty array'); $this->assertCount(0, $selectors); } } vendor/symfony/css-selector/Tests/Parser/Shortcut/ClassParserTest.php 0000644 00000002630 15132754523 0022127 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; /** * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class ClassParserTest extends TestCase { /** @dataProvider getParseTestData */ public function testParse($source, $representation) { $parser = new ClassParser(); $selectors = $parser->parse($source); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals($representation, (string) $selector->getTree()); } public function getParseTestData() { return array( array('.testclass', 'Class[Element[*].testclass]'), array('testel.testclass', 'Class[Element[testel].testclass]'), array('testns|.testclass', 'Class[Element[testns|*].testclass]'), array('testns|*.testclass', 'Class[Element[testns|*].testclass]'), array('testns|testel.testclass', 'Class[Element[testns|testel].testclass]'), ); } } vendor/symfony/css-selector/Tests/Parser/Shortcut/ElementParserTest.php 0000644 00000002345 15132754523 0022456 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Node\SelectorNode; use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; /** * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class ElementParserTest extends TestCase { /** @dataProvider getParseTestData */ public function testParse($source, $representation) { $parser = new ElementParser(); $selectors = $parser->parse($source); $this->assertCount(1, $selectors); /** @var SelectorNode $selector */ $selector = $selectors[0]; $this->assertEquals($representation, (string) $selector->getTree()); } public function getParseTestData() { return array( array('*', 'Element[*]'), array('testel', 'Element[testel]'), array('testns|*', 'Element[testns|*]'), array('testns|testel', 'Element[testns|testel]'), ); } } vendor/symfony/css-selector/Tests/Parser/TokenStreamTest.php 0000644 00000006356 15132754523 0020337 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; class TokenStreamTest extends TestCase { public function testGetNext() { $stream = new TokenStream(); $stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0)); $stream->push($t2 = new Token(Token::TYPE_DELIMITER, '.', 2)); $stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'title', 3)); $this->assertSame($t1, $stream->getNext()); $this->assertSame($t2, $stream->getNext()); $this->assertSame($t3, $stream->getNext()); } public function testGetPeek() { $stream = new TokenStream(); $stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0)); $stream->push($t2 = new Token(Token::TYPE_DELIMITER, '.', 2)); $stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'title', 3)); $this->assertSame($t1, $stream->getPeek()); $this->assertSame($t1, $stream->getNext()); $this->assertSame($t2, $stream->getPeek()); $this->assertSame($t2, $stream->getPeek()); $this->assertSame($t2, $stream->getNext()); } public function testGetNextIdentifier() { $stream = new TokenStream(); $stream->push(new Token(Token::TYPE_IDENTIFIER, 'h1', 0)); $this->assertEquals('h1', $stream->getNextIdentifier()); } public function testFailToGetNextIdentifier() { $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\CssSelector\Exception\SyntaxErrorException'); $stream = new TokenStream(); $stream->push(new Token(Token::TYPE_DELIMITER, '.', 2)); $stream->getNextIdentifier(); } public function testGetNextIdentifierOrStar() { $stream = new TokenStream(); $stream->push(new Token(Token::TYPE_IDENTIFIER, 'h1', 0)); $this->assertEquals('h1', $stream->getNextIdentifierOrStar()); $stream->push(new Token(Token::TYPE_DELIMITER, '*', 0)); $this->assertNull($stream->getNextIdentifierOrStar()); } public function testFailToGetNextIdentifierOrStar() { $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\CssSelector\Exception\SyntaxErrorException'); $stream = new TokenStream(); $stream->push(new Token(Token::TYPE_DELIMITER, '.', 2)); $stream->getNextIdentifierOrStar(); } public function testSkipWhitespace() { $stream = new TokenStream(); $stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0)); $stream->push($t2 = new Token(Token::TYPE_WHITESPACE, ' ', 2)); $stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'h1', 3)); $stream->skipWhitespace(); $this->assertSame($t1, $stream->getNext()); $stream->skipWhitespace(); $this->assertSame($t3, $stream->getNext()); } } vendor/symfony/css-selector/Tests/Parser/Handler/IdentifierHandlerTest.php 0000644 00000003001 15132754523 0023020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\IdentifierHandler; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; class IdentifierHandlerTest extends AbstractHandlerTest { public function getHandleValueTestData() { return array( array('foo', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ''), array('foo|bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '|bar'), array('foo.class', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '.class'), array('foo[attr]', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '[attr]'), array('foo bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ' bar'), ); } public function getDontHandleValueTestData() { return array( array('>'), array('+'), array(' '), array('*|foo'), array('/* comment */'), ); } protected function generateHandler() { $patterns = new TokenizerPatterns(); return new IdentifierHandler($patterns, new TokenizerEscaping($patterns)); } } vendor/symfony/css-selector/Tests/Parser/Handler/AbstractHandlerTest.php 0000644 00000004325 15132754523 0022513 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; /** * @author Jean-François Simon <contact@jfsimon.fr> */ abstract class AbstractHandlerTest extends TestCase { /** @dataProvider getHandleValueTestData */ public function testHandleValue($value, Token $expectedToken, $remainingContent) { $reader = new Reader($value); $stream = new TokenStream(); $this->assertTrue($this->generateHandler()->handle($reader, $stream)); $this->assertEquals($expectedToken, $stream->getNext()); $this->assertRemainingContent($reader, $remainingContent); } /** @dataProvider getDontHandleValueTestData */ public function testDontHandleValue($value) { $reader = new Reader($value); $stream = new TokenStream(); $this->assertFalse($this->generateHandler()->handle($reader, $stream)); $this->assertStreamEmpty($stream); $this->assertRemainingContent($reader, $value); } abstract public function getHandleValueTestData(); abstract public function getDontHandleValueTestData(); abstract protected function generateHandler(); protected function assertStreamEmpty(TokenStream $stream) { $property = new \ReflectionProperty($stream, 'tokens'); $property->setAccessible(true); $this->assertEquals(array(), $property->getValue($stream)); } protected function assertRemainingContent(Reader $reader, $remainingContent) { if ('' === $remainingContent) { $this->assertEquals(0, $reader->getRemainingLength()); $this->assertTrue($reader->isEOF()); } else { $this->assertEquals(strlen($remainingContent), $reader->getRemainingLength()); $this->assertEquals(0, $reader->getOffset($remainingContent)); } } } vendor/symfony/css-selector/Tests/Parser/Handler/WhitespaceHandlerTest.php 0000644 00000002263 15132754523 0023043 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\WhitespaceHandler; use Symfony\Component\CssSelector\Parser\Token; class WhitespaceHandlerTest extends AbstractHandlerTest { public function getHandleValueTestData() { return array( array(' ', new Token(Token::TYPE_WHITESPACE, ' ', 0), ''), array("\n", new Token(Token::TYPE_WHITESPACE, "\n", 0), ''), array("\t", new Token(Token::TYPE_WHITESPACE, "\t", 0), ''), array(' foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), 'foo'), array(' .foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), '.foo'), ); } public function getDontHandleValueTestData() { return array( array('>'), array('1'), array('a'), ); } protected function generateHandler() { return new WhitespaceHandler(); } } vendor/symfony/css-selector/Tests/Parser/Handler/NumberHandlerTest.php 0000644 00000002655 15132754523 0022204 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\NumberHandler; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; class NumberHandlerTest extends AbstractHandlerTest { public function getHandleValueTestData() { return array( array('12', new Token(Token::TYPE_NUMBER, '12', 0), ''), array('12.34', new Token(Token::TYPE_NUMBER, '12.34', 0), ''), array('+12.34', new Token(Token::TYPE_NUMBER, '+12.34', 0), ''), array('-12.34', new Token(Token::TYPE_NUMBER, '-12.34', 0), ''), array('12 arg', new Token(Token::TYPE_NUMBER, '12', 0), ' arg'), array('12]', new Token(Token::TYPE_NUMBER, '12', 0), ']'), ); } public function getDontHandleValueTestData() { return array( array('hello'), array('>'), array('+'), array(' '), array('/* comment */'), ); } protected function generateHandler() { $patterns = new TokenizerPatterns(); return new NumberHandler($patterns); } } vendor/symfony/css-selector/Tests/Parser/Handler/CommentHandlerTest.php 0000644 00000003122 15132754523 0022344 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\CommentHandler; use Symfony\Component\CssSelector\Parser\Reader; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\TokenStream; class CommentHandlerTest extends AbstractHandlerTest { /** @dataProvider getHandleValueTestData */ public function testHandleValue($value, Token $unusedArgument, $remainingContent) { $reader = new Reader($value); $stream = new TokenStream(); $this->assertTrue($this->generateHandler()->handle($reader, $stream)); // comments are ignored (not pushed as token in stream) $this->assertStreamEmpty($stream); $this->assertRemainingContent($reader, $remainingContent); } public function getHandleValueTestData() { return array( // 2nd argument only exists for inherited method compatibility array('/* comment */', new Token(null, null, null), ''), array('/* comment */foo', new Token(null, null, null), 'foo'), ); } public function getDontHandleValueTestData() { return array( array('>'), array('+'), array(' '), ); } protected function generateHandler() { return new CommentHandler(); } } vendor/symfony/css-selector/Tests/Parser/Handler/StringHandlerTest.php 0000644 00000002751 15132754523 0022217 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\StringHandler; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; class StringHandlerTest extends AbstractHandlerTest { public function getHandleValueTestData() { return array( array('"hello"', new Token(Token::TYPE_STRING, 'hello', 1), ''), array('"1"', new Token(Token::TYPE_STRING, '1', 1), ''), array('" "', new Token(Token::TYPE_STRING, ' ', 1), ''), array('""', new Token(Token::TYPE_STRING, '', 1), ''), array("'hello'", new Token(Token::TYPE_STRING, 'hello', 1), ''), array("'foo'bar", new Token(Token::TYPE_STRING, 'foo', 1), 'bar'), ); } public function getDontHandleValueTestData() { return array( array('hello'), array('>'), array('1'), array(' '), ); } protected function generateHandler() { $patterns = new TokenizerPatterns(); return new StringHandler($patterns, new TokenizerEscaping($patterns)); } } vendor/symfony/css-selector/Tests/Parser/Handler/HashHandlerTest.php 0000644 00000002562 15132754523 0021634 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Parser\Handler; use Symfony\Component\CssSelector\Parser\Handler\HashHandler; use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; class HashHandlerTest extends AbstractHandlerTest { public function getHandleValueTestData() { return array( array('#id', new Token(Token::TYPE_HASH, 'id', 0), ''), array('#123', new Token(Token::TYPE_HASH, '123', 0), ''), array('#id.class', new Token(Token::TYPE_HASH, 'id', 0), '.class'), array('#id element', new Token(Token::TYPE_HASH, 'id', 0), ' element'), ); } public function getDontHandleValueTestData() { return array( array('id'), array('123'), array('<'), array('<'), array('#'), ); } protected function generateHandler() { $patterns = new TokenizerPatterns(); return new HashHandler($patterns, new TokenizerEscaping($patterns)); } } vendor/symfony/css-selector/Tests/CssSelectorConverterTest.php 0000644 00000006246 15132754523 0020766 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\CssSelector\CssSelectorConverter; class CssSelectorConverterTest extends TestCase { public function testCssToXPath() { $converter = new CssSelectorConverter(); $this->assertEquals('descendant-or-self::*', $converter->toXPath('')); $this->assertEquals('descendant-or-self::h1', $converter->toXPath('h1')); $this->assertEquals("descendant-or-self::h1[@id = 'foo']", $converter->toXPath('h1#foo')); $this->assertEquals("descendant-or-self::h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", $converter->toXPath('h1.foo')); $this->assertEquals('descendant-or-self::foo:h1', $converter->toXPath('foo|h1')); $this->assertEquals('descendant-or-self::h1', $converter->toXPath('H1')); } public function testCssToXPathXml() { $converter = new CssSelectorConverter(false); $this->assertEquals('descendant-or-self::H1', $converter->toXPath('H1')); } /** * @expectedException \Symfony\Component\CssSelector\Exception\ParseException * @expectedExceptionMessage Expected identifier, but <eof at 3> found. */ public function testParseExceptions() { $converter = new CssSelectorConverter(); $converter->toXPath('h1:'); } /** @dataProvider getCssToXPathWithoutPrefixTestData */ public function testCssToXPathWithoutPrefix($css, $xpath) { $converter = new CssSelectorConverter(); $this->assertEquals($xpath, $converter->toXPath($css, ''), '->parse() parses an input string and returns a node'); } public function getCssToXPathWithoutPrefixTestData() { return array( array('h1', 'h1'), array('foo|h1', 'foo:h1'), array('h1, h2, h3', 'h1 | h2 | h3'), array('h1:nth-child(3n+1)', "*/*[name() = 'h1' and (position() - 1 >= 0 and (position() - 1) mod 3 = 0)]"), array('h1 > p', 'h1/p'), array('h1#foo', "h1[@id = 'foo']"), array('h1.foo', "h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('h1[class*="foo bar"]', "h1[@class and contains(@class, 'foo bar')]"), array('h1[foo|class*="foo bar"]', "h1[@foo:class and contains(@foo:class, 'foo bar')]"), array('h1[class]', 'h1[@class]'), array('h1 .foo', "h1/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('h1 #foo', "h1/descendant-or-self::*/*[@id = 'foo']"), array('h1 [class*=foo]', "h1/descendant-or-self::*/*[@class and contains(@class, 'foo')]"), array('div>.foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('div > .foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), ); } } vendor/symfony/css-selector/CssSelectorConverter.php 0000644 00000003713 15132754523 0017020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; use Symfony\Component\CssSelector\Parser\Shortcut\HashParser; use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; use Symfony\Component\CssSelector\XPath\Translator; /** * CssSelectorConverter is the main entry point of the component and can convert CSS * selectors to XPath expressions. * * @author Christophe Coevoet <stof@notk.org> */ class CssSelectorConverter { private $translator; /** * @param bool $html Whether HTML support should be enabled. Disable it for XML documents */ public function __construct($html = true) { $this->translator = new Translator(); if ($html) { $this->translator->registerExtension(new HtmlExtension($this->translator)); } $this->translator ->registerParserShortcut(new EmptyStringParser()) ->registerParserShortcut(new ElementParser()) ->registerParserShortcut(new ClassParser()) ->registerParserShortcut(new HashParser()) ; } /** * Translates a CSS expression to its XPath equivalent. * * Optionally, a prefix can be added to the resulting XPath * expression with the $prefix parameter. * * @param string $cssExpr The CSS expression * @param string $prefix An optional prefix for the XPath expression * * @return string */ public function toXPath($cssExpr, $prefix = 'descendant-or-self::') { return $this->translator->cssToXPath($cssExpr, $prefix); } } vendor/symfony/css-selector/Exception/ExceptionInterface.php 0000644 00000001100 15132754523 0020400 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; /** * Interface for exceptions. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ interface ExceptionInterface { } vendor/symfony/css-selector/Exception/ParseException.php 0000644 00000001176 15132754523 0017567 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; /** * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Fabien Potencier <fabien@symfony.com> */ class ParseException extends \Exception implements ExceptionInterface { } vendor/symfony/css-selector/Exception/ExpressionErrorException.php 0000644 00000001201 15132754523 0021653 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; /** * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class ExpressionErrorException extends ParseException { } vendor/symfony/css-selector/Exception/SyntaxErrorException.php 0000644 00000003456 15132754523 0021020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; use Symfony\Component\CssSelector\Parser\Token; /** * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class SyntaxErrorException extends ParseException { /** * @param string $expectedValue * @param Token $foundToken * * @return self */ public static function unexpectedToken($expectedValue, Token $foundToken) { return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken)); } /** * @param string $pseudoElement * @param string $unexpectedLocation * * @return self */ public static function pseudoElementFound($pseudoElement, $unexpectedLocation) { return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation)); } /** * @param int $position * * @return self */ public static function unclosedString($position) { return new self(sprintf('Unclosed/invalid string at %s.', $position)); } /** * @return self */ public static function nestedNot() { return new self('Got nested ::not().'); } /** * @return self */ public static function stringAsFunctionArgument() { return new self('String not allowed as function argument.'); } } vendor/symfony/css-selector/Exception/InternalErrorException.php 0000644 00000001177 15132754523 0021304 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; /** * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ class InternalErrorException extends ParseException { } vendor/psr/container/src/NotFoundExceptionInterface.php 0000644 00000000400 15132754523 0017364 0 ustar 00 <?php /** * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file) */ namespace Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends ContainerExceptionInterface { } vendor/psr/container/src/ContainerExceptionInterface.php 0000644 00000000370 15132754523 0017560 0 ustar 00 <?php /** * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file) */ namespace Psr\Container; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface { } vendor/psr/container/src/ContainerInterface.php 0000644 00000002112 15132754523 0015675 0 ustar 00 <?php /** * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file) */ namespace Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get($id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has($id); } vendor/psr/container/LICENSE 0000644 00000002171 15132754523 0011644 0 ustar 00 The MIT License (MIT) Copyright (c) 2013-2016 container-interop Copyright (c) 2016 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/jetpack-autoloader/class-hook-manager.php 0000644 00000004145 15132754523 0016016 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * Allows the latest autoloader to register hooks that can be removed when the autoloader is reset. */ class Hook_Manager { /** * An array containing all of the hooks that we've registered. * * @var array */ private $registered_hooks; /** * The constructor. */ public function __construct() { $this->registered_hooks = array(); } /** * Adds an action to WordPress and registers it internally. * * @param string $tag The name of the action which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the action. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_action( $tag, $callable, $priority, $accepted_args ); } /** * Adds a filter to WordPress and registers it internally. * * @param string $tag The name of the filter which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the filter. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_filter( $tag, $callable, $priority, $accepted_args ); } /** * Removes all of the registered hooks. */ public function reset() { foreach ( $this->registered_hooks as $tag => $hooks ) { foreach ( $hooks as $hook ) { remove_filter( $tag, $hook['callable'], $hook['priority'] ); } } $this->registered_hooks = array(); } } vendor/jetpack-autoloader/class-manifest-reader.php 0000644 00000005175 15132754523 0016520 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class reads autoloader manifest files. */ class Manifest_Reader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Reads all of the manifests in the given plugin paths. * * @param array $plugin_paths The paths to the plugins we're loading the manifest in. * @param string $manifest_path The path that we're loading the manifest from in each plugin. * @param array $path_map The path map to add the contents of the manifests to. * * @return array $path_map The path map we've built using the manifests in each plugin. */ public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { $file_paths = array_map( function ( $path ) use ( $manifest_path ) { return trailingslashit( $path ) . $manifest_path; }, $plugin_paths ); foreach ( $file_paths as $path ) { $this->register_manifest( $path, $path_map ); } return $path_map; } /** * Registers a plugin's manifest file with the path map. * * @param string $manifest_path The absolute path to the manifest that we're loading. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_manifest( $manifest_path, &$path_map ) { if ( ! is_readable( $manifest_path ) ) { return; } $manifest = require $manifest_path; if ( ! is_array( $manifest ) ) { return; } foreach ( $manifest as $key => $data ) { $this->register_record( $key, $data, $path_map ); } } /** * Registers an entry from the manifest in the path map. * * @param string $key The identifier for the entry we're registering. * @param array $data The data for the entry we're registering. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_record( $key, $data, &$path_map ) { if ( isset( $path_map[ $key ]['version'] ) ) { $selected_version = $path_map[ $key ]['version']; } else { $selected_version = null; } if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { $path_map[ $key ] = array( 'version' => $data['version'], 'path' => $data['path'], ); } } } vendor/jetpack-autoloader/class-latest-autoloader-guard.php 0000644 00000005364 15132754523 0020203 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class ensures that we're only executing the latest autoloader. */ class Latest_Autoloader_Guard { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The Autoloader_Handler instance. * * @var Autoloader_Handler */ private $autoloader_handler; /** * The Autoloader_locator instance. * * @var Autoloader_Locator */ private $autoloader_locator; /** * The constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance. * @param Autoloader_Handler $autoloader_handler The Autoloader_Handler instance. * @param Autoloader_Locator $autoloader_locator The Autoloader_Locator instance. */ public function __construct( $plugins_handler, $autoloader_handler, $autoloader_locator ) { $this->plugins_handler = $plugins_handler; $this->autoloader_handler = $autoloader_handler; $this->autoloader_locator = $autoloader_locator; } /** * Indicates whether or not the autoloader should be initialized. Note that this function * has the side-effect of actually loading the latest autoloader in the event that this * is not it. * * @param string $current_plugin The current plugin we're checking. * @param string[] $plugins The active plugins to check for autoloaders in. * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. * * @return bool True if we should stop initialization, otherwise false. */ public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { global $jetpack_autoloader_latest_version; // We need to reset the autoloader when the plugins change because // that means the autoloader was generated with a different list. if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { $this->autoloader_handler->reset_autoloader(); } // When the latest autoloader has already been found we don't need to search for it again. // We should take care however because this will also trigger if the autoloader has been // included by an older one. if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { return true; } $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); return true; } return false; } } vendor/jetpack-autoloader/class-container.php 0000644 00000011461 15132754523 0015427 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class manages the files and dependencies of the autoloader. */ class Container { /** * Since each autoloader's class files exist within their own namespace we need a map to * convert between the local class and a shared key. Note that no version checking is * performed on these dependencies and the first autoloader to register will be the * one that is utilized. */ const SHARED_DEPENDENCY_KEYS = array( Hook_Manager::class => 'Hook_Manager', ); /** * A map of all the dependencies we've registered with the container and created. * * @var array */ protected $dependencies; /** * The constructor. */ public function __construct() { $this->dependencies = array(); $this->register_shared_dependencies(); $this->register_dependencies(); $this->initialize_globals(); } /** * Gets a dependency out of the container. * * @param string $class The class to fetch. * * @return mixed * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. */ public function get( $class ) { if ( ! isset( $this->dependencies[ $class ] ) ) { throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); } return $this->dependencies[ $class ]; } /** * Registers all of the dependencies that are shared between all instances of the autoloader. */ private function register_shared_dependencies() { global $jetpack_autoloader_container_shared; if ( ! isset( $jetpack_autoloader_container_shared ) ) { $jetpack_autoloader_container_shared = array(); } $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { require_once __DIR__ . '/class-hook-manager.php'; $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); } $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; } /** * Registers all of the dependencies with the container. */ private function register_dependencies() { require_once __DIR__ . '/class-path-processor.php'; $this->dependencies[ Path_Processor::class ] = new Path_Processor(); require_once __DIR__ . '/class-plugin-locator.php'; $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-version-selector.php'; $this->dependencies[ Version_Selector::class ] = new Version_Selector(); require_once __DIR__ . '/class-autoloader-locator.php'; $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-php-autoloader.php'; $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); require_once __DIR__ . '/class-manifest-reader.php'; $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-plugins-handler.php'; $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( $this->get( Plugin_Locator::class ), $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-autoloader-handler.php'; $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( $this->get( PHP_Autoloader::class ), $this->get( Hook_Manager::class ), $this->get( Manifest_Reader::class ), $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-latest-autoloader-guard.php'; $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( $this->get( Plugins_Handler::class ), $this->get( Autoloader_Handler::class ), $this->get( Autoloader_Locator::class ) ); // Register any classes that we will use elsewhere. require_once __DIR__ . '/class-version-loader.php'; require_once __DIR__ . '/class-shutdown-handler.php'; } /** * Initializes any of the globals needed by the autoloader. */ private function initialize_globals() { /* * This global was retired in version 2.9. The value is set to 'false' to maintain * compatibility with older versions of the autoloader. */ global $jetpack_autoloader_including_latest; $jetpack_autoloader_including_latest = false; // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader // but was not discoverable, we will record them in this array to track them as "active". global $jetpack_autoloader_activating_plugins_paths; if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { $jetpack_autoloader_activating_plugins_paths = array(); } } } vendor/jetpack-autoloader/class-php-autoloader.php 0000644 00000005373 15132754523 0016376 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class PHP_Autoloader { /** * Registers the autoloader with PHP so that it can begin autoloading classes. * * @param Version_Loader $version_loader The class loader to use in the autoloader. */ public function register_autoloader( $version_loader ) { // Make sure no other autoloaders are registered. $this->unregister_autoloader(); // Set the global so that it can be used to load classes. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = $version_loader; // Ensure that the autoloader is first to avoid contention with others. spl_autoload_register( array( self::class, 'load_class' ), true, true ); } /** * Unregisters the active autoloader so that it will no longer autoload classes. */ public function unregister_autoloader() { // Remove any v2 autoloader that we've already registered. $autoload_chain = spl_autoload_functions(); foreach ( $autoload_chain as $autoloader ) { // We can identify a v2 autoloader using the namespace. $namespace_check = null; // Functions are recorded as strings. if ( is_string( $autoloader ) ) { $namespace_check = $autoloader; } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { // Static method calls have the class as the first array element. $namespace_check = $autoloader[0]; } else { // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. continue; } // Check for the namespace without the generated suffix. if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { spl_autoload_unregister( $autoloader ); } } // Clear the global now that the autoloader has been unregistered. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = null; } /** * Loads a class file if one could be found. * * Note: This function is static so that the autoloader can be easily unregistered. If * it was a class method we would have to unwrap the object to check the namespace. * * @param string $class_name The name of the class to autoload. * * @return bool Indicates whether or not a class file was loaded. */ public static function load_class( $class_name ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return; } $file = $jetpack_autoloader_loader->find_class_file( $class_name ); if ( ! isset( $file ) ) { return false; } require $file; return true; } } vendor/jetpack-autoloader/class-autoloader-locator.php 0000644 00000004120 15132754523 0017237 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class locates autoloaders. */ class Autoloader_Locator { /** * The object for comparing autoloader versions. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The version selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Finds the path to the plugin with the latest autoloader. * * @param array $plugin_paths An array of plugin paths. * @param string $latest_version The latest version reference. * * @return string|null */ public function find_latest_autoloader( $plugin_paths, &$latest_version ) { $latest_plugin = null; foreach ( $plugin_paths as $plugin_path ) { $version = $this->get_autoloader_version( $plugin_path ); if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { continue; } $latest_version = $version; $latest_plugin = $plugin_path; } return $latest_plugin; } /** * Gets the path to the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string */ public function get_autoloader_path( $plugin_path ) { return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; } /** * Gets the version for the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string|null */ public function get_autoloader_version( $plugin_path ) { $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; if ( ! file_exists( $classmap ) ) { return null; } $classmap = require $classmap; if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { return $classmap[ AutoloadGenerator::class ]['version']; } return null; } } vendor/jetpack-autoloader/class-shutdown-handler.php 0000644 00000005505 15132754523 0016735 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class handles the shutdown of the autoloader. */ class Shutdown_Handler { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The plugins cached by this autoloader. * * @var string[] */ private $cached_plugins; /** * Indicates whether or not this autoloader was included by another. * * @var bool */ private $was_included_by_autoloader; /** * Constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance to use. * @param string[] $cached_plugins The plugins cached by the autoloaer. * @param bool $was_included_by_autoloader Indicates whether or not the autoloader was included by another. */ public function __construct( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) { $this->plugins_handler = $plugins_handler; $this->cached_plugins = $cached_plugins; $this->was_included_by_autoloader = $was_included_by_autoloader; } /** * Handles the shutdown of the autoloader. */ public function __invoke() { // Don't save a broken cache if an error happens during some plugin's initialization. if ( ! did_action( 'plugins_loaded' ) ) { // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // Load the active plugins fresh since the list we pulled earlier might not contain // plugins that were activated but did not reset the autoloader. This happens // when a plugin is in the cache but not "active" when the autoloader loads. // We also want to make sure that plugins which are deactivating are not // considered "active" so that they will be removed from the cache now. try { $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); } catch ( \Exception $ex ) { // When the package is deleted before shutdown it will throw an exception. // In the event this happens we should erase the cache. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // The paths should be sorted for easy comparisons with those loaded from the cache. // Note we don't need to sort the cached entries because they're already sorted. sort( $active_plugins ); // We don't want to waste time saving a cache that hasn't changed. if ( $this->cached_plugins === $active_plugins ) { return; } $this->plugins_handler->cache_plugins( $active_plugins ); } } vendor/jetpack-autoloader/class-plugin-locator.php 0000644 00000010476 15132754523 0016411 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class scans the WordPress installation to find active plugins. */ class Plugin_Locator { /** * The path processor for finding plugin paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Path_Processor $path_processor The Path_Processor instance. */ public function __construct( $path_processor ) { $this->path_processor = $path_processor; } /** * Finds the path to the current plugin. * * @return string $path The path to the current plugin. * * @throws \RuntimeException If the current plugin does not have an autoloader. */ public function find_current_plugin() { // Escape from `vendor/__DIR__` to root plugin directory. $plugin_directory = dirname( dirname( __DIR__ ) ); // Use the path processor to ensure that this is an autoloader we're referencing. $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); if ( false === $path ) { throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); } return $path; } /** * Checks a given option for plugin paths. * * @param string $option_name The option that we want to check for plugin information. * @param bool $site_option Indicates whether or not we want to check the site option. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_option( $option_name, $site_option = false ) { $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); if ( false === $raw ) { return array(); } return $this->convert_plugins_to_paths( $raw ); } /** * Checks for plugins in the `action` request parameter. * * @param string[] $allowed_actions The actions that we're allowed to return plugins for. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_request_action( $allowed_actions ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended /** * Note: we're not actually checking the nonce here because it's too early * in the execution. The pluggable functions are not yet loaded to give * plugins a chance to plug their versions. Therefore we're doing the bare * minimum: checking whether the nonce exists and it's in the right place. * The request will fail later if the nonce doesn't pass the check. */ if ( empty( $_REQUEST['_wpnonce'] ) ) { return array(); } $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; if ( ! in_array( $action, $allowed_actions, true ) ) { return array(); } $plugin_slugs = array(); switch ( $action ) { case 'activate': case 'deactivate': if ( empty( $_REQUEST['plugin'] ) ) { break; } $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); break; case 'activate-selected': case 'deactivate-selected': if ( empty( $_REQUEST['checked'] ) ) { break; } $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); break; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $this->convert_plugins_to_paths( $plugin_slugs ); } /** * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter * out the plugins that are not directory plugins. Note that array keys will also be included * if they are plugin paths! * * @param string[] $plugins Plugin paths or slugs to filter. * * @return string[] */ private function convert_plugins_to_paths( $plugins ) { if ( ! is_array( $plugins ) || empty( $plugins ) ) { return array(); } // We're going to look for plugins in the standard directories. $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); $plugin_paths = array(); foreach ( $plugins as $key => $value ) { $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } } return $plugin_paths; } } vendor/jetpack-autoloader/class-plugins-handler.php 0000644 00000013373 15132754523 0016545 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class handles locating and caching all of the active plugins. */ class Plugins_Handler { /** * The transient key for plugin paths. */ const TRANSIENT_KEY = 'jetpack_autoloader_plugin_paths'; /** * The locator for finding plugins in different locations. * * @var Plugin_Locator */ private $plugin_locator; /** * The processor for transforming cached paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Plugin_Locator $plugin_locator The locator for finding active plugins. * @param Path_Processor $path_processor The processor for transforming cached paths. */ public function __construct( $plugin_locator, $path_processor ) { $this->plugin_locator = $plugin_locator; $this->path_processor = $path_processor; } /** * Gets all of the active plugins we can find. * * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. * * @return string[] */ public function get_active_plugins( $include_deactivating, $record_unknown ) { global $jetpack_autoloader_activating_plugins_paths; // We're going to build a unique list of plugins from a few different sources // to find all of our "active" plugins. While we need to return an integer // array, we're going to use an associative array internally to reduce // the amount of time that we're going to spend checking uniqueness // and merging different arrays together to form the output. $active_plugins = array(); // Make sure that plugins which have activated this request are considered as "active" even though // they probably won't be present in any option. if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { $active_plugins[ $path ] = $path; } } // This option contains all of the plugins that have been activated. $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // This option contains all of the multisite plugins that have been activated. if ( is_multisite() ) { $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } } // These actions contain plugins that are being activated/deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // When the current plugin isn't considered "active" there's a problem. // Since we're here, the plugin is active and currently being loaded. // We can support this case (mu-plugins and non-standard activation) // by adding the current plugin to the active list and marking it // as an unknown (activating) plugin. This also has the benefit // of causing a reset because the active plugins list has // been changed since it was saved in the global. $current_plugin = $this->plugin_locator->find_current_plugin(); if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { $active_plugins[ $current_plugin ] = $current_plugin; $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; } // When deactivating plugins aren't desired we should entirely remove them from the active list. if ( ! $include_deactivating ) { // These actions contain plugins that are being deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { unset( $active_plugins[ $path ] ); } } // Transform the array so that we don't have to worry about the keys interacting with other array types later. return array_values( $active_plugins ); } /** * Gets all of the cached plugins if there are any. * * @return string[] */ public function get_cached_plugins() { $cached = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $cached ) || empty( $cached ) ) { return array(); } // We need to expand the tokens to an absolute path for this webserver. return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); } /** * Saves the plugin list to the cache. * * @param array $plugins The plugin list to save to the cache. */ public function cache_plugins( $plugins ) { // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); set_transient( self::TRANSIENT_KEY, $plugins ); } /** * Checks to see whether or not the plugin list given has changed when compared to the * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal * with cases where the active list may change due to filtering.. * * @param string[] $plugins The plugins list to check against the global cache. * * @return bool True if the plugins have changed, otherwise false. */ public function have_plugins_changed( $plugins ) { global $jetpack_autoloader_cached_plugin_paths; if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { $jetpack_autoloader_cached_plugin_paths = $plugins; return true; } return false; } } vendor/jetpack-autoloader/class-path-processor.php 0000644 00000013061 15132754523 0016414 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class handles dealing with paths for the autoloader. */ class Path_Processor { /** * Given a path this will replace any of the path constants with a token to represent it. * * @param string $path The path we want to process. * * @return string The tokenized path. */ public function tokenize_path_constants( $path ) { $path = wp_normalize_path( $path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $len = strlen( $constant_path ); if ( substr( $path, 0, $len ) !== $constant_path ) { continue; } return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); } return $path; } /** * Given a path this will replace any of the path constant tokens with the expanded path. * * @param string $tokenized_path The path we want to process. * * @return string The expanded path. */ public function untokenize_path_constants( $tokenized_path ) { $tokenized_path = wp_normalize_path( $tokenized_path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $constant = '{{' . $constant . '}}'; $len = strlen( $constant ); if ( substr( $tokenized_path, 0, $len ) !== $constant ) { continue; } return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); } return $tokenized_path; } /** * Given a file and an array of places it might be, this will find the absolute path and return it. * * @param string $file The plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|false Returns the absolute path to the directory, otherwise false. */ public function find_directory_with_autoloader( $file, $directories_to_check ) { $file = wp_normalize_path( $file ); if ( ! $this->is_absolute_path( $file ) ) { $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); if ( ! isset( $file ) ) { return false; } } // We need the real path for consistency with __DIR__ paths. $file = $this->get_real_path( $file ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged $directory = @is_file( $file ) ? dirname( $file ) : $file; if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { return false; } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged return $directory; } /** * Fetches an array of normalized paths keyed by the constant they came from. * * @return string[] The normalized paths keyed by the constant. */ private function get_normalized_constants() { $raw_constants = array( // Order the constants from most-specific to least-specific. 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH', ); $constants = array(); foreach ( $raw_constants as $raw ) { if ( ! defined( $raw ) ) { continue; } $path = wp_normalize_path( constant( $raw ) ); if ( isset( $path ) ) { $constants[ $raw ] = $path; } } return $constants; } /** * Indicates whether or not a path is absolute. * * @param string $path The path to check. * * @return bool True if the path is absolute, otherwise false. */ private function is_absolute_path( $path ) { if ( 0 === strlen( $path ) || '.' === $path[0] ) { return false; } // Absolute paths on Windows may begin with a drive letter. if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { return true; } // A path starting with / or \ is absolute; anything else is relative. return ( '/' === $path[0] || '\\' === $path[0] ); } /** * Given a file and a list of directories to check, this method will try to figure out * the absolute path to the file in question. * * @param string $normalized_path The normalized path to the plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|null The absolute path to the plugin file, otherwise null. */ private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { // We're only able to find the absolute path for plugin/theme PHP files. if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { return null; } foreach ( $directories_to_check as $directory ) { $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_file( $normalized_check ) ) { return $normalized_check; } } return null; } /** * Given a path this will figure out the real path that we should be using. * * @param string $path The path to resolve. * * @return string The resolved path. */ private function get_real_path( $path ) { // We want to resolve symbolic links for consistency with __DIR__ paths. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $real_path = @realpath( $path ); if ( false === $real_path ) { // Let the autoloader deal with paths that don't exist. $real_path = $path; } // Using realpath will make it platform-specific so we must normalize it after. if ( $path !== $real_path ) { $real_path = wp_normalize_path( $real_path ); } return $real_path; } } vendor/jetpack-autoloader/class-autoloader.php 0000644 00000010075 15132754523 0015604 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class Autoloader { /** * Checks to see whether or not the autoloader should be initialized and then initializes it if so. * * @param Container|null $container The container we want to use for autoloader initialization. If none is given * then a container will be created automatically. */ public static function init( $container = null ) { // The container holds and manages the lifecycle of our dependencies // to make them easier to work with and increase flexibility. if ( ! isset( $container ) ) { require_once __DIR__ . '/class-container.php'; $container = new Container(); } // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @var Autoloader_Handler $autoloader_handler */ $autoloader_handler = $container->get( Autoloader_Handler::class ); // If the autoloader is already initializing it means that it has included us as the latest. $was_included_by_autoloader = $autoloader_handler->is_initializing(); /** @var Plugin_Locator $plugin_locator */ $plugin_locator = $container->get( Plugin_Locator::class ); /** @var Plugins_Handler $plugins_handler */ $plugins_handler = $container->get( Plugins_Handler::class ); // The current plugin is the one that we are attempting to initialize here. $current_plugin = $plugin_locator->find_current_plugin(); // The active plugins are those that we were able to discover on the site. This list will not // include mu-plugins, those activated by code, or those who are hidden by filtering. We also // want to take care to not consider the current plugin unknown if it was included by an // autoloader. This avoids the case where a plugin will be marked "active" while deactivated // due to it having the latest autoloader. $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. // Note that it's possible this list will include plugins that have since been deactivated, but after a request // the cache should be updated and the deactivated plugins will be removed. $cached_plugins = $plugins_handler->get_cached_plugins(); // We combine the active list and cached list to preemptively load classes for plugins that are // presently unknown but will be loaded during the request. While this may result in us considering packages in // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. $all_plugins = array_merge( $active_plugins, $cached_plugins ); // In particular we also include the current plugin to address the case where it is the latest autoloader // but also unknown (and not cached). We don't want it in the active list because we don't know that it // is active but we need it in the all plugins list so that it is considered by the autoloader. $all_plugins[] = $current_plugin; // We require uniqueness in the array to avoid processing the same plugin more than once. $all_plugins = array_values( array_unique( $all_plugins ) ); /** @var Latest_Autoloader_Guard $guard */ $guard = $container->get( Latest_Autoloader_Guard::class ); if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { return; } // Initialize the autoloader using the handler now that we're ready. $autoloader_handler->activate_autoloader( $all_plugins ); /** @var Hook_Manager $hook_manager */ $hook_manager = $container->get( Hook_Manager::class ); // Register a shutdown handler to clean up the autoloader. $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); // phpcs:enable Generic.Commenting.DocComment.MissingShort } } vendor/jetpack-autoloader/class-version-loader.php 0000644 00000010166 15132754523 0016377 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * This class loads other classes based on given parameters. */ class Version_Loader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * A map of available classes and their version and file path. * * @var array */ private $classmap; /** * A map of PSR-4 namespaces and their version and directory path. * * @var array */ private $psr4_map; /** * A map of all the files that we should load. * * @var array */ private $filemap; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. * @param array $classmap The verioned classmap to load using. * @param array $psr4_map The versioned PSR-4 map to load using. * @param array $filemap The versioned filemap to load. */ public function __construct( $version_selector, $classmap, $psr4_map, $filemap ) { $this->version_selector = $version_selector; $this->classmap = $classmap; $this->psr4_map = $psr4_map; $this->filemap = $filemap; } /** * Finds the file path for the given class. * * @param string $class_name The class to find. * * @return string|null $file_path The path to the file if found, null if no class was found. */ public function find_class_file( $class_name ) { $data = $this->select_newest_file( isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, $this->find_psr4_file( $class_name ) ); if ( ! isset( $data ) ) { return null; } return $data['path']; } /** * Load all of the files in the filemap. */ public function load_filemap() { if ( empty( $this->filemap ) ) { return; } foreach ( $this->filemap as $file_identifier => $file_data ) { if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { require_once $file_data['path']; $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; } } } /** * Compares different class sources and returns the newest. * * @param array|null $classmap_data The classmap class data. * @param array|null $psr4_data The PSR-4 class data. * * @return array|null $data */ private function select_newest_file( $classmap_data, $psr4_data ) { if ( ! isset( $classmap_data ) ) { return $psr4_data; } elseif ( ! isset( $psr4_data ) ) { return $classmap_data; } if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { return $psr4_data; } return $classmap_data; } /** * Finds the file for a given class in a PSR-4 namespace. * * @param string $class_name The class to find. * * @return array|null $data The version and path path to the file if found, null otherwise. */ private function find_psr4_file( $class_name ) { if ( ! isset( $this->psr4_map ) ) { return null; } // Don't bother with classes that have no namespace. $class_index = strrpos( $class_name, '\\' ); if ( ! $class_index ) { return null; } $class_for_path = str_replace( '\\', '/', $class_name ); // Search for the namespace by iteratively cutting off the last segment until // we find a match. This allows us to check the most-specific namespaces // first as well as minimize the amount of time spent looking. for ( $class_namespace = substr( $class_name, 0, $class_index ); ! empty( $class_namespace ); $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) ) { $namespace = $class_namespace . '\\'; if ( ! isset( $this->psr4_map[ $namespace ] ) ) { continue; } $data = $this->psr4_map[ $namespace ]; foreach ( $data['path'] as $path ) { $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; if ( file_exists( $path ) ) { return array( 'version' => $data['version'], 'path' => $path, ); } } } return null; } } vendor/jetpack-autoloader/class-autoloader-handler.php 0000644 00000010767 15132754523 0017227 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class selects the package version for the autoloader. */ class Autoloader_Handler { /** * The PHP_Autoloader instance. * * @var PHP_Autoloader */ private $php_autoloader; /** * The Hook_Manager instance. * * @var Hook_Manager */ private $hook_manager; /** * The Manifest_Reader instance. * * @var Manifest_Reader */ private $manifest_reader; /** * The Version_Selector instance. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param PHP_Autoloader $php_autoloader The PHP_Autoloader instance. * @param Hook_Manager $hook_manager The Hook_Manager instance. * @param Manifest_Reader $manifest_reader The Manifest_Reader instance. * @param Version_Selector $version_selector The Version_Selector instance. */ public function __construct( $php_autoloader, $hook_manager, $manifest_reader, $version_selector ) { $this->php_autoloader = $php_autoloader; $this->hook_manager = $hook_manager; $this->manifest_reader = $manifest_reader; $this->version_selector = $version_selector; } /** * Checks to see whether or not an autoloader is currently in the process of initializing. * * @return bool */ public function is_initializing() { // If no version has been set it means that no autoloader has started initializing yet. global $jetpack_autoloader_latest_version; if ( ! isset( $jetpack_autoloader_latest_version ) ) { return false; } // When the version is set but the classmap is not it ALWAYS means that this is the // latest autoloader and is being included by an older one. global $jetpack_packages_classmap; if ( empty( $jetpack_packages_classmap ) ) { return true; } // Version 2.4.0 added a new global and altered the reset semantics. We need to check // the other global as well since it may also point at initialization. // Note: We don't need to check for the class first because every autoloader that // will set the latest version global requires this class in the classmap. $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return true; } } return false; } /** * Activates an autoloader using the given plugins and activates it. * * @param string[] $plugins The plugins to initialize the autoloader for. */ public function activate_autoloader( $plugins ) { global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); $loader = new Version_Loader( $this->version_selector, $jetpack_packages_classmap, $jetpack_packages_psr4, $jetpack_packages_filemap ); $this->php_autoloader->register_autoloader( $loader ); // Now that the autoloader is active we can load the filemap. $loader->load_filemap(); } /** * Resets the active autoloader and all related global state. */ public function reset_autoloader() { $this->php_autoloader->unregister_autoloader(); $this->hook_manager->reset(); // Clear all of the autoloader globals so that older autoloaders don't do anything strange. global $jetpack_autoloader_latest_version; $jetpack_autoloader_latest_version = null; global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! } } vendor/jetpack-autoloader/class-version-selector.php 0000644 00000003467 15132754523 0016757 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore /** * Used to select package versions. */ class Version_Selector { /** * Checks whether the selected package version should be updated. Composer development * package versions ('9999999-dev' or versions that start with 'dev-') are favored * when the JETPACK_AUTOLOAD_DEV constant is set to true. * * @param String $selected_version The currently selected package version. * @param String $compare_version The package version that is being evaluated to * determine if the version needs to be updated. * * @return bool Returns true if the selected package version should be updated, * else false. */ public function is_version_update_required( $selected_version, $compare_version ) { $use_dev_versions = defined( 'JETPACK_AUTOLOAD_DEV' ) && JETPACK_AUTOLOAD_DEV; if ( is_null( $selected_version ) ) { return true; } if ( $use_dev_versions && $this->is_dev_version( $selected_version ) ) { return false; } if ( $this->is_dev_version( $compare_version ) ) { if ( $use_dev_versions ) { return true; } else { return false; } } if ( version_compare( $selected_version, $compare_version, '<' ) ) { return true; } return false; } /** * Checks whether the given package version is a development version. * * @param String $version The package version. * * @return bool True if the version is a dev version, else false. */ public function is_dev_version( $version ) { if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { return true; } return false; } } vendor/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php 0000644 00000013552 15132754523 0022111 0 ustar 00 <?php namespace Pelago\Emogrifier\Utilities; /** * Facilitates building a CSS string by appending rule blocks one at a time, checking whether the media query, * selectors, or declarations block are the same as those from the preceding block and combining blocks in such cases. * * Example: * $concatenator = new CssConcatenator(); * $concatenator->append(['body'], 'color: blue;'); * $concatenator->append(['body'], 'font-size: 16px;'); * $concatenator->append(['p'], 'margin: 1em 0;'); * $concatenator->append(['ul', 'ol'], 'margin: 1em 0;'); * $concatenator->append(['body'], 'font-size: 14px;', '@media screen and (max-width: 400px)'); * $concatenator->append(['ul', 'ol'], 'margin: 0.75em 0;', '@media screen and (max-width: 400px)'); * $css = $concatenator->getCss(); * * `$css` (if unminified) would contain the following CSS: * ` body { * ` color: blue; * ` font-size: 16px; * ` } * ` p, ul, ol { * ` margin: 1em 0; * ` } * ` @media screen and (max-width: 400px) { * ` body { * ` font-size: 14px; * ` } * ` ul, ol { * ` margin: 0.75em 0; * ` } * ` } * * @internal * * @author Jake Hotson <jake.github@qzdesign.co.uk> */ class CssConcatenator { /** * Array of media rules in order. Each element is an object with the following properties: * - string `media` - The media query string, e.g. "@media screen and (max-width:639px)", or an empty string for * rules not within a media query block; * - \stdClass[] `ruleBlocks` - Array of rule blocks in order, where each element is an object with the following * properties: * - mixed[] `selectorsAsKeys` - Array whose keys are selectors for the rule block (values are of no * significance); * - string `declarationsBlock` - The property declarations, e.g. "margin-top: 0.5em; padding: 0". * * @var \stdClass[] */ private $mediaRules = []; /** * Appends a declaration block to the CSS. * * @param string[] $selectors Array of selectors for the rule, e.g. ["ul", "ol", "p:first-child"]. * @param string $declarationsBlock The property declarations, e.g. "margin-top: 0.5em; padding: 0". * @param string $media The media query for the rule, e.g. "@media screen and (max-width:639px)", * or an empty string if none. */ public function append(array $selectors, $declarationsBlock, $media = '') { $selectorsAsKeys = \array_flip($selectors); $mediaRule = $this->getOrCreateMediaRuleToAppendTo($media); $lastRuleBlock = \end($mediaRule->ruleBlocks); $hasSameDeclarationsAsLastRule = $lastRuleBlock !== false && $declarationsBlock === $lastRuleBlock->declarationsBlock; if ($hasSameDeclarationsAsLastRule) { $lastRuleBlock->selectorsAsKeys += $selectorsAsKeys; } else { $hasSameSelectorsAsLastRule = $lastRuleBlock !== false && self::hasEquivalentSelectors($selectorsAsKeys, $lastRuleBlock->selectorsAsKeys); if ($hasSameSelectorsAsLastRule) { $lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';'); $lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock; } else { $mediaRule->ruleBlocks[] = (object)\compact('selectorsAsKeys', 'declarationsBlock'); } } } /** * @return string */ public function getCss() { return \implode('', \array_map([self::class, 'getMediaRuleCss'], $this->mediaRules)); } /** * @param string $media The media query for rules to be appended, e.g. "@media screen and (max-width:639px)", * or an empty string if none. * * @return \stdClass Object with properties as described for elements of `$mediaRules`. */ private function getOrCreateMediaRuleToAppendTo($media) { $lastMediaRule = \end($this->mediaRules); if ($lastMediaRule !== false && $media === $lastMediaRule->media) { return $lastMediaRule; } $newMediaRule = (object)[ 'media' => $media, 'ruleBlocks' => [], ]; $this->mediaRules[] = $newMediaRule; return $newMediaRule; } /** * Tests if two sets of selectors are equivalent (i.e. the same selectors, possibly in a different order). * * @param mixed[] $selectorsAsKeys1 Array in which the selectors are the keys, and the values are of no * significance. * @param mixed[] $selectorsAsKeys2 Another such array. * * @return bool */ private static function hasEquivalentSelectors(array $selectorsAsKeys1, array $selectorsAsKeys2) { return \count($selectorsAsKeys1) === \count($selectorsAsKeys2) && \count($selectorsAsKeys1) === \count($selectorsAsKeys1 + $selectorsAsKeys2); } /** * @param \stdClass $mediaRule Object with properties as described for elements of `$mediaRules`. * * @return string CSS for the media rule. */ private static function getMediaRuleCss(\stdClass $mediaRule) { $css = \implode('', \array_map([self::class, 'getRuleBlockCss'], $mediaRule->ruleBlocks)); if ($mediaRule->media !== '') { $css = $mediaRule->media . '{' . $css . '}'; } return $css; } /** * @param \stdClass $ruleBlock Object with properties as described for elements of the `ruleBlocks` property of * elements of `$mediaRules`. * * @return string CSS for the rule block. */ private static function getRuleBlockCss(\stdClass $ruleBlock) { $selectors = \array_keys($ruleBlock->selectorsAsKeys); return \implode(',', $selectors) . '{' . $ruleBlock->declarationsBlock . '}'; } } vendor/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php 0000644 00000003503 15132754523 0022313 0 ustar 00 <?php namespace Pelago\Emogrifier\Utilities; /** * When computing many array intersections using the same array, it is more efficient to use `array_flip()` first and * then `array_intersect_key()`, than `array_intersect()`. See the discussion at * {@link https://stackoverflow.com/questions/6329211/php-array-intersect-efficiency Stack Overflow} for more * information. * * Of course, this is only possible if the arrays contain integer or string values, and either don't contain duplicates, * or that fact that duplicates will be removed does not matter. * * This class takes care of the detail. * * @internal * * @author Jake Hotson <jake.github@qzdesign.co.uk> */ class ArrayIntersector { /** * the array with which the object was constructed, with all its keys exchanged with their associated values * * @var (int|string)[] */ private $invertedArray; /** * Constructs the object with the array that will be reused for many intersection computations. * * @param (int|string)[] $array */ public function __construct(array $array) { $this->invertedArray = \array_flip($array); } /** * Computes the intersection of `$array` and the array with which this object was constructed. * * @param (int|string)[] $array * * @return (int|string)[] Returns an array containing all of the values in `$array` whose values exist in the array * with which this object was constructed. Note that keys are preserved, order is maintained, but * duplicates are removed. */ public function intersectWith(array $array) { $invertedArray = \array_flip($array); $invertedIntersection = \array_intersect_key($invertedArray, $this->invertedArray); return \array_flip($invertedIntersection); } } vendor/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php 0000644 00000012131 15132754523 0021741 0 ustar 00 <?php namespace Pelago\Emogrifier\HtmlProcessor; use Pelago\Emogrifier\CssInliner; use Pelago\Emogrifier\Utilities\ArrayIntersector; /** * This class can remove things from HTML. * * @author Oliver Klee <github@oliverklee.de> * @author Jake Hotson <jake.github@qzdesign.co.uk> */ class HtmlPruner extends AbstractHtmlProcessor { /** * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase, * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE") * to lowercase. * * @var string */ const DISPLAY_NONE_MATCHER = '//*[@style and contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")' . ' and not(@class and contains(concat(" ", normalize-space(@class), " "), " -emogrifier-keep "))]'; /** * Removes elements that have a "display: none;" style. * * @return self fluent interface */ public function removeElementsWithDisplayNone() { $elementsWithStyleDisplayNone = $this->xPath->query(self::DISPLAY_NONE_MATCHER); if ($elementsWithStyleDisplayNone->length === 0) { return $this; } /** @var \DOMNode $element */ foreach ($elementsWithStyleDisplayNone as $element) { $parentNode = $element->parentNode; if ($parentNode !== null) { $parentNode->removeChild($element); } } return $this; } /** * Removes classes that are no longer required (e.g. because there are no longer any CSS rules that reference them) * from `class` attributes. * * Note that this does not inspect the CSS, but expects to be provided with a list of classes that are still in use. * * This method also has the (presumably beneficial) side-effect of minifying (removing superfluous whitespace from) * `class` attributes. * * @param string[] $classesToKeep names of classes that should not be removed * * @return self fluent interface */ public function removeRedundantClasses(array $classesToKeep = []) { $elementsWithClassAttribute = $this->xPath->query('//*[@class]'); if ($classesToKeep !== []) { $this->removeClassesFromElements($elementsWithClassAttribute, $classesToKeep); } else { // Avoid unnecessary processing if there are no classes to keep. $this->removeClassAttributeFromElements($elementsWithClassAttribute); } return $this; } /** * Removes classes from the `class` attribute of each element in `$elements`, except any in `$classesToKeep`, * removing the `class` attribute itself if the resultant list is empty. * * @param \DOMNodeList $elements * @param string[] $classesToKeep * * @return void */ private function removeClassesFromElements(\DOMNodeList $elements, array $classesToKeep) { $classesToKeepIntersector = new ArrayIntersector($classesToKeep); /** @var \DOMNode $element */ foreach ($elements as $element) { $elementClasses = \preg_split('/\\s++/', \trim($element->getAttribute('class'))); $elementClassesToKeep = $classesToKeepIntersector->intersectWith($elementClasses); if ($elementClassesToKeep !== []) { $element->setAttribute('class', \implode(' ', $elementClassesToKeep)); } else { $element->removeAttribute('class'); } } } /** * Removes the `class` attribute from each element in `$elements`. * * @param \DOMNodeList $elements * * @return void */ private function removeClassAttributeFromElements(\DOMNodeList $elements) { /** @var \DOMNode $element */ foreach ($elements as $element) { $element->removeAttribute('class'); } } /** * After CSS has been inlined, there will likely be some classes in `class` attributes that are no longer referenced * by any remaining (uninlinable) CSS. This method removes such classes. * * Note that it does not inspect the remaining CSS, but uses information readily available from the `CssInliner` * instance about the CSS rules that could not be inlined. * * @param CssInliner $cssInliner object instance that performed the CSS inlining * * @return self fluent interface * * @throws \BadMethodCallException if `inlineCss` has not first been called on `$cssInliner` */ public function removeRedundantClassesAfterCssInlined(CssInliner $cssInliner) { $classesToKeepAsKeys = []; foreach ($cssInliner->getMatchingUninlinableSelectors() as $selector) { \preg_match_all('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches); $classesToKeepAsKeys += \array_fill_keys($matches[1], true); } $this->removeRedundantClasses(\array_keys($classesToKeepAsKeys)); return $this; } } vendor/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php 0000644 00000021522 15132754523 0024135 0 ustar 00 <?php namespace Pelago\Emogrifier\HtmlProcessor; /** * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes. * * The "vanilla" subclass is the HtmlNormalizer. * * @author Oliver Klee <github@oliverklee.de> */ abstract class AbstractHtmlProcessor { /** * @var string */ const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>'; /** * @var string */ const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; /** * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are * self-closing. These are mostly HTML5 elements, but for completeness <command> (obsolete) and <keygen> * (deprecated) are also included. * * @see https://bugs.php.net/bug.php?id=73175 */ const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)'; /** * @var \DOMDocument */ protected $domDocument = null; /** * @var \DOMXPath */ protected $xPath = null; /** * The constructor. * * Please use ::fromHtml instead. */ private function __construct() { } /** * Builds a new instance from the given HTML. * * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty * * @return static * * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string */ public static function fromHtml($unprocessedHtml) { if (!\is_string($unprocessedHtml)) { throw new \InvalidArgumentException('The provided HTML must be a string.', 1515459744); } if ($unprocessedHtml === '') { throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647); } $instance = new static(); $instance->setHtml($unprocessedHtml); return $instance; } /** * Builds a new instance from the given DOM document. * * @param \DOMDocument $document a DOM document returned by getDomDocument() of another instance * * @return static */ public static function fromDomDocument(\DOMDocument $document) { $instance = new static(); $instance->setDomDocument($document); return $instance; } /** * Sets the HTML to process. * * @param string $html the HTML to process, must be UTF-8-encoded * * @return void */ private function setHtml($html) { $this->createUnifiedDomDocument($html); } /** * Provides access to the internal DOMDocument representation of the HTML in its current state. * * @return \DOMDocument */ public function getDomDocument() { return $this->domDocument; } /** * @param \DOMDocument $domDocument * * @return void */ private function setDomDocument(\DOMDocument $domDocument) { $this->domDocument = $domDocument; $this->xPath = new \DOMXPath($this->domDocument); } /** * Renders the normalized and processed HTML. * * @return string */ public function render() { $htmlWithPossibleErroneousClosingTags = $this->domDocument->saveHTML(); return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); } /** * Renders the content of the BODY element of the normalized and processed HTML. * * @return string */ public function renderBodyContent() { $htmlWithPossibleErroneousClosingTags = $this->domDocument->saveHTML($this->getBodyElement()); $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); return \preg_replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml); } /** * Eliminates any invalid closing tags for void elements from the given HTML. * * @param string $html * * @return string */ private function removeSelfClosingTagsClosingTags($html) { return \preg_replace('%</' . static::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html); } /** * Returns the BODY element. * * This method assumes that there always is a BODY element. * * @return \DOMElement */ private function getBodyElement() { return $this->domDocument->getElementsByTagName('body')->item(0); } /** * Creates a DOM document from the given HTML and stores it in $this->domDocument. * * The DOM document will always have a BODY element and a document type. * * @param string $html * * @return void */ private function createUnifiedDomDocument($html) { $this->createRawDomDocument($html); $this->ensureExistenceOfBodyElement(); } /** * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument. * * @param string $html * * @return void */ private function createRawDomDocument($html) { $domDocument = new \DOMDocument(); $domDocument->strictErrorChecking = false; $domDocument->formatOutput = true; $libXmlState = \libxml_use_internal_errors(true); $domDocument->loadHTML($this->prepareHtmlForDomConversion($html)); \libxml_clear_errors(); \libxml_use_internal_errors($libXmlState); $this->setDomDocument($domDocument); } /** * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed, * ensuring that the HTML will be good for creating a DOM document from it. * * @param string $html * * @return string the unified HTML */ private function prepareHtmlForDomConversion($html) { $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html); $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes); return $this->addContentTypeMetaTag($htmlWithDocumentType); } /** * Makes sure that the passed HTML has a document type. * * @param string $html * * @return string HTML with document type */ private function ensureDocumentType($html) { $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false; if ($hasDocumentType) { return $html; } return static::DEFAULT_DOCUMENT_TYPE . $html; } /** * Adds a Content-Type meta tag for the charset. * * This method also ensures that there is a HEAD element. * * @param string $html * * @return string the HTML with the meta tag added */ private function addContentTypeMetaTag($html) { $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false; if ($hasContentTypeMetaTag) { return $html; } // We are trying to insert the meta tag to the right spot in the DOM. // If we just prepended it to the HTML, we would lose attributes set to the HTML tag. $hasHeadTag = \stripos($html, '<head') !== false; $hasHtmlTag = \stripos($html, '<html') !== false; if ($hasHeadTag) { $reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . static::CONTENT_TYPE_META_TAG, $html); } elseif ($hasHtmlTag) { $reworkedHtml = \preg_replace( '/<html(.*?)>/i', '<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>', $html ); } else { $reworkedHtml = static::CONTENT_TYPE_META_TAG . $html; } return $reworkedHtml; } /** * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a * self-closing slash. * * @param string $html * * @return string HTML with problematic tags converted. */ private function ensurePhpUnrecognizedSelfClosingTagsAreXml($html) { return \preg_replace( '%<' . static::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\\b[^>]*+(?<!/)(?=>)%', '$0/', $html ); } /** * Checks that $this->domDocument has a BODY element and adds it if it is missing. * * @return void * * @throws \UnexpectedValueException */ private function ensureExistenceOfBodyElement() { if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) { return; } $htmlElement = $this->domDocument->getElementsByTagName('html')->item(0); if ($htmlElement === null) { throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853); } $htmlElement->appendChild($this->domDocument->createElement('body')); } } vendor/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php 0000644 00000023577 15132754523 0024470 0 ustar 00 <?php namespace Pelago\Emogrifier\HtmlProcessor; /** * This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes, * e.g. it converts style="width: 100px" to width="100". * * It will only add attributes, but leaves the style attribute untouched. * * To trigger the conversion, call the convertCssToVisualAttributes method. * * @author Oliver Klee <github@oliverklee.de> */ class CssToAttributeConverter extends AbstractHtmlProcessor { /** * This multi-level array contains simple mappings of CSS properties to * HTML attributes. If a mapping only applies to certain HTML nodes or * only for certain values, the mapping is an object with a whitelist * of nodes and values. * * @var mixed[][] */ private $cssToHtmlMap = [ 'background-color' => [ 'attribute' => 'bgcolor', ], 'text-align' => [ 'attribute' => 'align', 'nodes' => ['p', 'div', 'td'], 'values' => ['left', 'right', 'center', 'justify'], ], 'float' => [ 'attribute' => 'align', 'nodes' => ['table', 'img'], 'values' => ['left', 'right'], ], 'border-spacing' => [ 'attribute' => 'cellspacing', 'nodes' => ['table'], ], ]; /** * @var string[][] */ private static $parsedCssCache = []; /** * Maps the CSS from the style nodes to visual HTML attributes. * * @return self fluent interface */ public function convertCssToVisualAttributes() { /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style')); $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node); } return $this; } /** * Returns a list with all DOM nodes that have a style attribute. * * @return \DOMNodeList */ private function getAllNodesWithStyleAttribute() { return $this->xPath->query('//*[@style]'); } /** * Parses a CSS declaration block into property name/value pairs. * * Example: * * The declaration block * * "color: #000; font-weight: bold;" * * will be parsed into the following array: * * "color" => "#000" * "font-weight" => "bold" * * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty * * @return string[] * the CSS declarations with the property names as array keys and the property values as array values */ private function parseCssDeclarationsBlock($cssDeclarationsBlock) { if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) { return self::$parsedCssCache[$cssDeclarationsBlock]; } $properties = []; foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) { $matches = []; if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) { continue; } $propertyName = \strtolower($matches[1]); $propertyValue = $matches[2]; $properties[$propertyName] = $propertyValue; } self::$parsedCssCache[$cssDeclarationsBlock] = $properties; return $properties; } /** * Applies $styles to $node. * * This method maps CSS styles to HTML attributes and adds those to the * node. * * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node * @param \DOMElement $node node to apply styles to * * @return void */ private function mapCssToHtmlAttributes(array $styles, \DOMElement $node) { foreach ($styles as $property => $value) { // Strip !important indicator $value = \trim(\str_replace('!important', '', $value)); $this->mapCssToHtmlAttribute($property, $value, $node); } } /** * Tries to apply the CSS style to $node as an attribute. * * This method maps a CSS rule to HTML attributes and adds those to the node. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to * * @return void */ private function mapCssToHtmlAttribute($property, $value, \DOMElement $node) { if (!$this->mapSimpleCssProperty($property, $value, $node)) { $this->mapComplexCssProperty($property, $value, $node); } } /** * Looks up the CSS property in the mapping table and maps it if it matches the conditions. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to * * @return bool true if the property can be mapped using the simple mapping table */ private function mapSimpleCssProperty($property, $value, \DOMElement $node) { if (!isset($this->cssToHtmlMap[$property])) { return false; } $mapping = $this->cssToHtmlMap[$property]; $nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true); $valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true); $canBeMapped = $nodesMatch && $valuesMatch; if ($canBeMapped) { $node->setAttribute($mapping['attribute'], $value); } return $canBeMapped; } /** * Maps CSS properties that need special transformation to an HTML attribute. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to * * @return void */ private function mapComplexCssProperty($property, $value, \DOMElement $node) { switch ($property) { case 'background': $this->mapBackgroundProperty($node, $value); break; case 'width': // intentional fall-through case 'height': $this->mapWidthOrHeightProperty($node, $value, $property); break; case 'margin': $this->mapMarginProperty($node, $value); break; case 'border': $this->mapBorderProperty($node, $value); break; default: } } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map * * @return void */ private function mapBackgroundProperty(\DOMElement $node, $value) { // parse out the color, if any $styles = \explode(' ', $value, 2); $first = $styles[0]; if (\is_numeric($first[0]) || \strncmp($first, 'url', 3) === 0) { return; } // as this is not a position or image, assume it's a color $node->setAttribute('bgcolor', $first); } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map * @param string $property the name of the CSS property to map * * @return void */ private function mapWidthOrHeightProperty(\DOMElement $node, $value, $property) { // only parse values in px and %, but not values like "auto" if (!\preg_match('/^(\\d+)(px|%)$/', $value)) { return; } $number = \preg_replace('/[^0-9.%]/', '', $value); $node->setAttribute($property, $number); } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map * * @return void */ private function mapMarginProperty(\DOMElement $node, $value) { if (!$this->isTableOrImageNode($node)) { return; } $margins = $this->parseCssShorthandValue($value); if ($margins['left'] === 'auto' && $margins['right'] === 'auto') { $node->setAttribute('align', 'center'); } } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map * * @return void */ private function mapBorderProperty(\DOMElement $node, $value) { if (!$this->isTableOrImageNode($node)) { return; } if ($value === 'none' || $value === '0') { $node->setAttribute('border', '0'); } } /** * @param \DOMElement $node * * @return bool */ private function isTableOrImageNode(\DOMElement $node) { return $node->nodeName === 'table' || $node->nodeName === 'img'; } /** * Parses a shorthand CSS value and splits it into individual values * * @param string $value a string of CSS value with 1, 2, 3 or 4 sizes * For example: padding: 0 auto; * '0 auto' is split into top: 0, left: auto, bottom: 0, * right: auto. * * @return string[] an array of values for top, right, bottom and left (using these as associative array keys) */ private function parseCssShorthandValue($value) { /** @var string[] $values */ $values = \preg_split('/\\s+/', $value); $css = []; $css['top'] = $values[0]; $css['right'] = (\count($values) > 1) ? $values[1] : $css['top']; $css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top']; $css['left'] = (\count($values) > 3) ? $values[3] : $css['right']; return $css; } } vendor/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php 0000644 00000000531 15132754523 0022611 0 ustar 00 <?php namespace Pelago\Emogrifier\HtmlProcessor; /** * Normalizes HTML: * - add a document type (HTML5) if missing * - disentangle incorrectly nested tags * - add HEAD and BODY elements (if they are missing) * - reformat the HTML * * @author Oliver Klee <github@oliverklee.de> */ class HtmlNormalizer extends AbstractHtmlProcessor { } vendor/pelago/emogrifier/src/Emogrifier/CssInliner.php 0000644 00000113521 15132754523 0017113 0 ustar 00 <?php namespace Pelago\Emogrifier; use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor; use Pelago\Emogrifier\Utilities\CssConcatenator; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; /** * This class provides functions for converting CSS styles into inline style attributes in your HTML code. * * For Emogrifier 3.0.0, this will be the successor to the \Pelago\Emogrifier class (which then will be deprecated). * * For more information, please see the README.md file. * * @author Cameron Brooks * @author Jaime Prado * @author Oliver Klee <github@oliverklee.de> * @author Roman Ožana <ozana@omdesign.cz> * @author Sander Kruger <s.kruger@invessel.com> * @author Zoli Szabó <zoli.szabo+github@gmail.com> */ class CssInliner extends AbstractHtmlProcessor { /** * @var int */ const CACHE_KEY_CSS = 0; /** * @var int */ const CACHE_KEY_SELECTOR = 1; /** * @var int */ const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 2; /** * @var int */ const CACHE_KEY_COMBINED_STYLES = 3; /** * Regular expression component matching a static pseudo class in a selector, without the preceding ":", * for which the applicable elements can be determined (by converting the selector to an XPath expression). * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead * group, as appropriate for the usage context.) * * @var string */ const PSEUDO_CLASS_MATCHER = 'empty|(?:first|last|nth(?:-last)?+|only)-child|(?:first|last|nth(?:-last)?+)-of-type|not\\([[:ascii:]]*\\)'; /** * @var bool[] */ private $excludedSelectors = []; /** * @var bool[] */ private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true]; /** * @var mixed[] */ private $caches = [ self::CACHE_KEY_CSS => [], self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; /** * @var CssSelectorConverter */ private $cssSelectorConverter = null; /** * the visited nodes with the XPath paths as array keys * * @var \DOMElement[] */ private $visitedNodes = []; /** * the styles to apply to the nodes with the XPath paths as array keys for the outer array * and the attribute names/values as key/value pairs for the inner array * * @var string[][] */ private $styleAttributesForNodes = []; /** * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved. * If set to false, the value of the style attributes will be discarded. * * @var bool */ private $isInlineStyleAttributesParsingEnabled = true; /** * Determines whether the <style> blocks in the HTML passed to this class should be parsed. * * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML * via inline styles. * * If set to false, the <style> blocks will be left as they are in the HTML. * * @var bool */ private $isStyleBlocksParsingEnabled = true; /** * For calculating selector precedence order. * Keys are a regular expression part to match before a CSS name. * Values are a multiplier factor per match to weight specificity. * * @var int[] */ private $selectorPrecedenceMatchers = [ // IDs: worth 10000 '\\#' => 10000, // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100 '(?:\\.|\\[|(?<!:):(?!not\\())' => 100, // elements (not attribute values or `:not`), pseudo-elements: worth 1 '(?:(?<![="\':\\w\\-])|::)' => 1, ]; /** * array of data describing CSS rules which apply to the document but cannot be inlined, in the format returned by * `parseCssRules` * * @var string[][] */ private $matchingUninlinableCssRules = null; /** * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them. * * @var bool */ private $debug = false; /** * Inlines the given CSS into the existing HTML. * * @param string $css the CSS to inline, must be UTF-8-encoded * * @return self fluent interface * * @throws SyntaxErrorException */ public function inlineCss($css = '') { $this->clearAllCaches(); $this->purgeVisitedNodes(); $this->normalizeStyleAttributesOfAllNodes(); $combinedCss = $css; // grab any existing style blocks from the HTML and append them to the existing CSS // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS) if ($this->isStyleBlocksParsingEnabled) { $combinedCss .= $this->getCssFromAllStyleNodes(); } $cssWithoutComments = $this->removeCssComments($combinedCss); list($cssWithoutCommentsCharsetOrImport, $cssImportRules) = $this->extractImportAndCharsetRules($cssWithoutComments); $excludedNodes = $this->getNodesToExclude(); $cssRules = $this->parseCssRules($cssWithoutCommentsCharsetOrImport); $cssSelectorConverter = $this->getCssSelectorConverter(); foreach ($cssRules['inlinable'] as $cssRule) { try { $nodesMatchingCssSelectors = $this->xPath->query($cssSelectorConverter->toXPath($cssRule['selector'])); } catch (SyntaxErrorException $e) { if ($this->debug) { throw $e; } continue; } /** @var \DOMElement $node */ foreach ($nodesMatchingCssSelectors as $node) { if (\in_array($node, $excludedNodes, true)) { continue; } $this->copyInlinableCssToStyleAttribute($node, $cssRule); } } if ($this->isInlineStyleAttributesParsingEnabled) { $this->fillStyleAttributesWithMergedStyles(); } $this->removeImportantAnnotationFromAllInlineStyles(); $this->determineMatchingUninlinableCssRules($cssRules['uninlinable']); $this->copyUninlinableCssToStyleNode($cssImportRules); return $this; } /** * Disables the parsing of inline styles. * * @return void */ public function disableInlineStyleAttributesParsing() { $this->isInlineStyleAttributesParsingEnabled = false; } /** * Disables the parsing of <style> blocks. * * @return void */ public function disableStyleBlocksParsing() { $this->isStyleBlocksParsingEnabled = false; } /** * Marks a media query type to keep. * * @param string $mediaName the media type name, e.g., "braille" * * @return void */ public function addAllowedMediaType($mediaName) { $this->allowedMediaTypes[$mediaName] = true; } /** * Drops a media query type from the allowed list. * * @param string $mediaName the tag name, e.g., "braille" * * @return void */ public function removeAllowedMediaType($mediaName) { if (isset($this->allowedMediaTypes[$mediaName])) { unset($this->allowedMediaTypes[$mediaName]); } } /** * Adds a selector to exclude nodes from emogrification. * * Any nodes that match the selector will not have their style altered. * * @param string $selector the selector to exclude, e.g., ".editor" * * @return void */ public function addExcludedSelector($selector) { $this->excludedSelectors[$selector] = true; } /** * No longer excludes the nodes matching this selector from emogrification. * * @param string $selector the selector to no longer exclude, e.g., ".editor" * * @return void */ public function removeExcludedSelector($selector) { if (isset($this->excludedSelectors[$selector])) { unset($this->excludedSelectors[$selector]); } } /** * Sets the debug mode. * * @param bool $debug set to true to enable debug mode * * @return void */ public function setDebug($debug) { $this->debug = $debug; } /** * Gets the array of selectors present in the CSS provided to `inlineCss()` for which the declarations could not be * applied as inline styles, but which may affect elements in the HTML. The relevant CSS will have been placed in a * `<style>` element. The selectors may include those used within `@media` rules or those involving dynamic * pseudo-classes (such as `:hover`) or pseudo-elements (such as `::after`). * * @return string[] * * @throws \BadMethodCallException if `inlineCss` has not been called first */ public function getMatchingUninlinableSelectors() { if ($this->matchingUninlinableCssRules === null) { throw new \BadMethodCallException('inlineCss must be called first', 1568385221); } return \array_column($this->matchingUninlinableCssRules, 'selector'); } /** * Clears all caches. * * @return void */ private function clearAllCaches() { $this->caches = [ self::CACHE_KEY_CSS => [], self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; } /** * Purges the visited nodes. * * @return void */ private function purgeVisitedNodes() { $this->visitedNodes = []; $this->styleAttributesForNodes = []; } /** * Parses the document and normalizes all existing CSS attributes. * This changes 'DISPLAY: none' to 'display: none'. * We wouldn't have to do this if DOMXPath supported XPath 2.0. * Also stores a reference of nodes with existing inline styles so we don't overwrite them. * * @return void */ private function normalizeStyleAttributesOfAllNodes() { /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { if ($this->isInlineStyleAttributesParsingEnabled) { $this->normalizeStyleAttributes($node); } // Remove style attribute in every case, so we can add them back (if inline style attributes // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules; // else original inline style rules may remain at the beginning of the final inline style definition // of a node, which may give not the desired results $node->removeAttribute('style'); } } /** * Returns a list with all DOM nodes that have a style attribute. * * @return \DOMNodeList */ private function getAllNodesWithStyleAttribute() { return $this->xPath->query('//*[@style]'); } /** * Normalizes the value of the "style" attribute and saves it. * * @param \DOMElement $node * * @return void */ private function normalizeStyleAttributes(\DOMElement $node) { $normalizedOriginalStyle = \preg_replace_callback( '/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S', static function (array $m) { return \strtolower($m[0]); }, $node->getAttribute('style') ); // in order to not overwrite existing style attributes in the HTML, we // have to save the original HTML styles $nodePath = $node->getNodePath(); if (!isset($this->styleAttributesForNodes[$nodePath])) { $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle); $this->visitedNodes[$nodePath] = $node; } $node->setAttribute('style', $normalizedOriginalStyle); } /** * Parses a CSS declaration block into property name/value pairs. * * Example: * * The declaration block * * "color: #000; font-weight: bold;" * * will be parsed into the following array: * * "color" => "#000" * "font-weight" => "bold" * * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty * * @return string[] * the CSS declarations with the property names as array keys and the property values as array values */ private function parseCssDeclarationsBlock($cssDeclarationsBlock) { if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) { return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock]; } $properties = []; foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) { $matches = []; if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) { continue; } $propertyName = \strtolower($matches[1]); $propertyValue = $matches[2]; $properties[$propertyName] = $propertyValue; } $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties; return $properties; } /** * Returns CSS content. * * @return string */ private function getCssFromAllStyleNodes() { $styleNodes = $this->xPath->query('//style'); if ($styleNodes === false) { return ''; } $css = ''; /** @var \DOMNode $styleNode */ foreach ($styleNodes as $styleNode) { $css .= "\n\n" . $styleNode->nodeValue; $styleNode->parentNode->removeChild($styleNode); } return $css; } /** * Removes comments from the supplied CSS. * * @param string $css * * @return string CSS with the comments removed */ private function removeCssComments($css) { return \preg_replace('%/\\*[^*]*+(?:\\*(?!/)[^*]*+)*+\\*/%', '', $css); } /** * Extracts `@import` and `@charset` rules from the supplied CSS. These rules must not be preceded by any other * rules, or they will be ignored. (From the CSS 2.1 specification: "CSS 2.1 user agents must ignore any '@import' * rule that occurs inside a block or after any non-ignored statement other than an @charset or an @import rule." * Note also that `@charset` is case sensitive whereas `@import` is not.) * * @param string $css CSS with comments removed * * @return string[] The first element is the CSS with the valid `@import` and `@charset` rules removed. The second * element contains a concatenation of the valid `@import` rules, each followed by whatever whitespace followed it * in the original CSS (so that either unminified or minified formatting is preserved); if there were no `@import` * rules, it will be an empty string. The (valid) `@charset` rules are discarded. */ private function extractImportAndCharsetRules($css) { $possiblyModifiedCss = $css; $importRules = ''; while ( \preg_match( '/^\\s*+(@((?i)import(?-i)|charset)\\s[^;]++;\\s*+)/', $possiblyModifiedCss, $matches ) ) { list($fullMatch, $atRuleAndFollowingWhitespace, $atRuleName) = $matches; if (\strtolower($atRuleName) === 'import') { $importRules .= $atRuleAndFollowingWhitespace; } $possiblyModifiedCss = \substr($possiblyModifiedCss, \strlen($fullMatch)); } return [$possiblyModifiedCss, $importRules]; } /** * Find the nodes that are not to be emogrified. * * @return \DOMElement[] * * @throws SyntaxErrorException */ private function getNodesToExclude() { $excludedNodes = []; foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) { try { $matchingNodes = $this->xPath->query($this->getCssSelectorConverter()->toXPath($selectorToExclude)); } catch (SyntaxErrorException $e) { if ($this->debug) { throw $e; } continue; } foreach ($matchingNodes as $node) { $excludedNodes[] = $node; } } return $excludedNodes; } /** * @return CssSelectorConverter */ private function getCssSelectorConverter() { if ($this->cssSelectorConverter === null) { $this->cssSelectorConverter = new CssSelectorConverter(); } return $this->cssSelectorConverter; } /** * Extracts and parses the individual rules from a CSS string. * * @param string $css a string of raw CSS code with comments removed * * @return string[][][] A 2-entry array with the key "inlinable" containing rules which can be inlined as `style` * attributes and the key "uninlinable" containing rules which cannot. Each value is an array of string * sub-arrays with the keys * "media" (the media query string, e.g. "@media screen and (max-width: 480px)", * or an empty string if not from a `@media` rule), * "selector" (the CSS selector, e.g., "*" or "header h1"), * "hasUnmatchablePseudo" (true if that selector contains pseudo-elements or dynamic pseudo-classes * such that the declarations cannot be applied inline), * "declarationsBlock" (the semicolon-separated CSS declarations for that selector, * e.g., "color: red; height: 4px;"), * and "line" (the line number e.g. 42) */ private function parseCssRules($css) { $cssKey = \md5($css); if (isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) { return $this->caches[self::CACHE_KEY_CSS][$cssKey]; } $matches = $this->getCssRuleMatches($css); $cssRules = [ 'inlinable' => [], 'uninlinable' => [], ]; /** @var string[][] $matches */ /** @var string[] $cssRule */ foreach ($matches as $key => $cssRule) { $cssDeclaration = \trim($cssRule['declarations']); if ($cssDeclaration === '') { continue; } foreach (\explode(',', $cssRule['selectors']) as $selector) { // don't process pseudo-elements and behavioral (dynamic) pseudo-classes; // only allow structural pseudo-classes $hasPseudoElement = \strpos($selector, '::') !== false; $hasUnsupportedPseudoClass = (bool)\preg_match( '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector ); $hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass; $parsedCssRule = [ 'media' => $cssRule['media'], 'selector' => \trim($selector), 'hasUnmatchablePseudo' => $hasUnmatchablePseudo, 'declarationsBlock' => $cssDeclaration, // keep track of where it appears in the file, since order is important 'line' => $key, ]; $ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlinable' : 'uninlinable'; $cssRules[$ruleType][] = $parsedCssRule; } } \usort($cssRules['inlinable'], [$this, 'sortBySelectorPrecedence']); $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules; return $cssRules; } /** * @param string[] $a * @param string[] $b * * @return int */ private function sortBySelectorPrecedence(array $a, array $b) { $precedenceA = $this->getCssSelectorPrecedence($a['selector']); $precedenceB = $this->getCssSelectorPrecedence($b['selector']); // We want these sorted in ascending order so selectors with lesser precedence get processed first and // selectors with greater precedence get sorted last. $precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1); $precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1); return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals; } /** * @param string $selector * * @return int */ private function getCssSelectorPrecedence($selector) { $selectorKey = \md5($selector); if (isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) { return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey]; } $precedence = 0; foreach ($this->selectorPrecedenceMatchers as $matcher => $value) { if (\trim($selector) === '') { break; } $number = 0; $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number); $precedence += ($value * $number); } $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence; return $precedence; } /** * Parses a string of CSS into the media query, selectors and declarations for each ruleset in order. * * @param string $css CSS with comments removed * * @return string[][] Array of string sub-arrays with the keys * "media" (the media query string, e.g. "@media screen and (max-width: 480px)", * or an empty string if not from an `@media` rule), * "selectors" (the CSS selector(s), e.g., "*" or "h1, h2"), * "declarations" (the semicolon-separated CSS declarations for that/those selector(s), * e.g., "color: red; height: 4px;"), */ private function getCssRuleMatches($css) { $splitCss = $this->splitCssAndMediaQuery($css); $ruleMatches = []; foreach ($splitCss as $cssPart) { // process each part for selectors and definitions \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER); /** @var string[][] $matches */ foreach ($matches as $cssRule) { $ruleMatches[] = [ 'media' => $cssPart['media'], 'selectors' => $cssRule[1], 'declarations' => $cssRule[2], ]; } } return $ruleMatches; } /** * Splits input CSS code into an array of parts for different media queries, in order. * Each part is an array where: * * - key "css" will contain clean CSS code (for @media rules this will be the group rule body within "{...}") * - key "media" will contain "@media " followed by the media query list, for all allowed media queries, * or an empty string for CSS not within a media query * * Example: * * The CSS code * * "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}" * * will be parsed into the following array: * * 0 => [ * "css" => "h1 { color:red; }", * "media" => "" * ], * 1 => [ * "css" => " h1 {}", * "media" => "@media " * ] * * @param string $css * * @return string[][] */ private function splitCssAndMediaQuery($css) { $mediaTypesExpression = ''; if (!empty($this->allowedMediaTypes)) { $mediaTypesExpression = '|' . \implode('|', \array_keys($this->allowedMediaTypes)); } $mediaRuleBodyMatcher = '[^{]*+{(?:[^{}]*+{.*})?\\s*+}\\s*+'; $cssSplitForAllowedMediaTypes = \preg_split( '#(@media\\s++(?:only\\s++)?+(?:(?=[{(])' . $mediaTypesExpression . ')' . $mediaRuleBodyMatcher . ')#misU', $css, -1, PREG_SPLIT_DELIM_CAPTURE ); // filter the CSS outside/between allowed @media rules $cssCleaningMatchers = [ 'import/charset directives' => '/\\s*+@(?:import|charset)\\s[^;]++;/i', 'remaining media enclosures' => '/\\s*+@media\\s' . $mediaRuleBodyMatcher . '/isU', ]; $splitCss = []; foreach ($cssSplitForAllowedMediaTypes as $index => $cssPart) { $isMediaRule = $index % 2 !== 0; if ($isMediaRule) { \preg_match('/^([^{]*+){(.*)}[^}]*+$/s', $cssPart, $matches); $splitCss[] = [ 'css' => $matches[2], 'media' => $matches[1], ]; } else { $cleanedCss = \trim(\preg_replace($cssCleaningMatchers, '', $cssPart)); if ($cleanedCss !== '') { $splitCss[] = [ 'css' => $cleanedCss, 'media' => '', ]; } } } return $splitCss; } /** * Copies $cssRule into the style attribute of $node. * * Note: This method does not check whether $cssRule matches $node. * * @param \DOMElement $node * @param string[][] $cssRule * * @return void */ private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule) { $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']); if ($newStyleDeclarations === []) { return; } // if it has a style attribute, get it, process it, and append (overwrite) new stuff if ($node->hasAttribute('style')) { // break it up into an associative array $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style')); } else { $oldStyleDeclarations = []; } $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations) ); } /** * This method merges old or existing name/value array with new name/value array * and then generates a string of the combined style suitable for placing inline. * This becomes the single point for CSS string generation allowing for consistent * CSS output no matter where the CSS originally came from. * * @param string[] $oldStyles * @param string[] $newStyles * * @return string */ private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles) { $cacheKey = \serialize([$oldStyles, $newStyles]); if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) { return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey]; } // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed foreach ($oldStyles as $attributeName => $attributeValue) { if (!isset($newStyles[$attributeName])) { continue; } $newAttributeValue = $newStyles[$attributeName]; if ( $this->attributeValueIsImportant($attributeValue) && !$this->attributeValueIsImportant($newAttributeValue) ) { unset($newStyles[$attributeName]); } else { unset($oldStyles[$attributeName]); } } $combinedStyles = \array_merge($oldStyles, $newStyles); $style = ''; foreach ($combinedStyles as $attributeName => $attributeValue) { $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; '; } $trimmedStyle = \rtrim($style); $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle; return $trimmedStyle; } /** * Checks whether $attributeValue is marked as !important. * * @param string $attributeValue * * @return bool */ private function attributeValueIsImportant($attributeValue) { return \strtolower(\substr(\trim($attributeValue), -10)) === '!important'; } /** * Merges styles from styles attributes and style nodes and applies them to the attribute nodes * * @return void */ private function fillStyleAttributesWithMergedStyles() { foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) { $node = $this->visitedNodes[$nodePath]; $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style')); $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $currentStyleAttributes, $styleAttributesForNode ) ); } } /** * Searches for all nodes with a style attribute and removes the "!important" annotations out of * the inline style declarations, eventually by rearranging declarations. * * @return void */ private function removeImportantAnnotationFromAllInlineStyles() { foreach ($this->getAllNodesWithStyleAttribute() as $node) { $this->removeImportantAnnotationFromNodeInlineStyle($node); } } /** * Removes the "!important" annotations out of the inline style declarations, * eventually by rearranging declarations. * Rearranging needed when !important shorthand properties are followed by some of their * not !important expanded-version properties. * For example "font: 12px serif !important; font-size: 13px;" must be reordered * to "font-size: 13px; font: 12px serif;" in order to remain correct. * * @param \DOMElement $node * * @return void */ private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node) { $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style')); $regularStyleDeclarations = []; $importantStyleDeclarations = []; foreach ($inlineStyleDeclarations as $property => $value) { if ($this->attributeValueIsImportant($value)) { $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value)); } else { $regularStyleDeclarations[$property] = $value; } } $inlineStyleDeclarationsInNewOrder = \array_merge( $regularStyleDeclarations, $importantStyleDeclarations ); $node->setAttribute( 'style', $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder) ); } /** * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array. * * @param string[] $styleDeclarations * * @return string */ private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations) { return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations); } /** * Determines which of `$cssRules` actually apply to `$this->domDocument`, and sets them in * `$this->matchingUninlinableCssRules`. * * @param string[][] $cssRules the "uninlinable" array of CSS rules returned by `parseCssRules` * * @return void */ private function determineMatchingUninlinableCssRules(array $cssRules) { $this->matchingUninlinableCssRules = \array_filter($cssRules, [$this, 'existsMatchForSelectorInCssRule']); } /** * Checks whether there is at least one matching element for the CSS selector contained in the `selector` element * of the provided CSS rule. * * Any dynamic pseudo-classes will be assumed to apply. If the selector matches a pseudo-element, * it will test for a match with its originating element. * * @param string[] $cssRule * * @return bool * * @throws SyntaxErrorException */ private function existsMatchForSelectorInCssRule(array $cssRule) { $selector = $cssRule['selector']; if ($cssRule['hasUnmatchablePseudo']) { $selector = $this->removeUnmatchablePseudoComponents($selector); } return $this->existsMatchForCssSelector($selector); } /** * Checks whether there is at least one matching element for $cssSelector. * When not in debug mode, it returns true also for invalid selectors (because they may be valid, * just not implemented/recognized yet by Emogrifier). * * @param string $cssSelector * * @return bool * * @throws SyntaxErrorException */ private function existsMatchForCssSelector($cssSelector) { try { $nodesMatchingSelector = $this->xPath->query($this->getCssSelectorConverter()->toXPath($cssSelector)); } catch (SyntaxErrorException $e) { if ($this->debug) { throw $e; } return true; } return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0; } /** * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary. * If such a pseudo-component is within the argument of `:not`, the entire `:not` component is removed or replaced. * * @param string $selector * * @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply, * or in the case of pseudo-elements will match their originating element. */ private function removeUnmatchablePseudoComponents($selector) { // The regex allows nested brackets via `(?2)`. // A space is temporarily prepended because the callback can't determine if the match was at the very start. $selectorWithoutNots = \ltrim(\preg_replace_callback( '/(\\s?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i', [$this, 'replaceUnmatchableNotComponent'], ' ' . $selector )); $pseudoComponentMatcher = ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+'; return \preg_replace( ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'], ['$1*', ''], $selectorWithoutNots ); } /** * Helps `removeUnmatchablePseudoComponents()` replace or remove a selector `:not(...)` component if its argument * contains pseudo-elements or dynamic pseudo-classes. * * @param string[] $matches array of elements matched by the regular expression * * @return string the full match if there were no unmatchable pseudo components within; otherwise, any preceding * whitespace followed by "*", or an empty string if there was no preceding whitespace */ private function replaceUnmatchableNotComponent(array $matches) { list($notComponentWithAnyPrecedingWhitespace, $anyPrecedingWhitespace, $notArgumentInBrackets) = $matches; $hasUnmatchablePseudo = \preg_match( '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-:]/i', $notArgumentInBrackets ); if ($hasUnmatchablePseudo) { return $anyPrecedingWhitespace !== '' ? $anyPrecedingWhitespace . '*' : ''; } return $notComponentWithAnyPrecedingWhitespace; } /** * Applies `$this->matchingUninlinableCssRules` to `$this->domDocument` by placing them as CSS in a `<style>` * element. * * @param string $cssImportRules This may contain any `@import` rules that should precede the CSS placed in the * `<style>` element. If there are no unlinlinable CSS rules to copy there, a `<style>` element will be * created containing just `$cssImportRules`. `$cssImportRules` may be an empty string; if it is, and there * are no unlinlinable CSS rules, an empty `<style>` element will not be created. * * @return void */ private function copyUninlinableCssToStyleNode($cssImportRules) { $css = $cssImportRules; // avoid including unneeded class dependency if there are no rules if ($this->matchingUninlinableCssRules !== []) { $cssConcatenator = new CssConcatenator(); foreach ($this->matchingUninlinableCssRules as $cssRule) { $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']); } $css .= $cssConcatenator->getCss(); } // avoid adding empty style element if ($css !== '') { $this->addStyleElementToDocument($css); } } /** * Adds a style element with $css to $this->domDocument. * * This method is protected to allow overriding. * * @see https://github.com/MyIntervals/emogrifier/issues/103 * * @param string $css * * @return void */ protected function addStyleElementToDocument($css) { $styleElement = $this->domDocument->createElement('style', $css); $styleAttribute = $this->domDocument->createAttribute('type'); $styleAttribute->value = 'text/css'; $styleElement->appendChild($styleAttribute); $headElement = $this->getHeadElement(); $headElement->appendChild($styleElement); } /** * Returns the HEAD element. * * This method assumes that there always is a HEAD element. * * @return \DOMElement */ private function getHeadElement() { return $this->domDocument->getElementsByTagName('head')->item(0); } } vendor/pelago/emogrifier/src/Emogrifier.php 0000644 00000171006 15132754523 0015044 0 ustar 00 <?php namespace Pelago; use Pelago\Emogrifier\Utilities\CssConcatenator; /** * This class provides functions for converting CSS styles into inline style attributes in your HTML code. * * For more information, please see the README.md file. * * @deprecated Will be removed for version 4.0.0. Please use the CssInliner class instead. * * @author Cameron Brooks * @author Jaime Prado * @author Oliver Klee <github@oliverklee.de> * @author Roman Ožana <ozana@omdesign.cz> * @author Sander Kruger <s.kruger@invessel.com> * @author Zoli Szabó <zoli.szabo+github@gmail.com> */ class Emogrifier { /** * @var int */ const CACHE_KEY_CSS = 0; /** * @var int */ const CACHE_KEY_SELECTOR = 1; /** * @var int */ const CACHE_KEY_XPATH = 2; /** * @var int */ const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 3; /** * @var int */ const CACHE_KEY_COMBINED_STYLES = 4; /** * for calculating nth-of-type and nth-child selectors * * @var int */ const INDEX = 0; /** * for calculating nth-of-type and nth-child selectors * * @var int */ const MULTIPLIER = 1; /** * @var string */ const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/'; /** * @var string */ const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/'; /** * Regular expression component matching a static pseudo class in a selector, without the preceding ":", * for which the applicable elements can be determined (by converting the selector to an XPath expression). * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead * group, as appropriate for the usage context.) * * @var string */ const PSEUDO_CLASS_MATCHER = '(?:first|last|nth)-child|nth-of-type|not\\([[:ascii:]]*\\)'; /** * @var string */ const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; /** * @var string */ const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>'; /** * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are * self-closing. These are mostly HTML5 elements, but for completeness <command> (obsolete) and <keygen> * (deprecated) are also included. * * @see https://bugs.php.net/bug.php?id=73175 */ const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)'; /** * @var \DOMDocument */ protected $domDocument = null; /** * @var \DOMXPath */ protected $xPath = null; /** * @var string */ private $css = ''; /** * @var bool[] */ private $excludedSelectors = []; /** * @var string[] */ private $unprocessableHtmlTags = ['wbr']; /** * @var bool[] */ private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true]; /** * @var mixed[] */ private $caches = [ self::CACHE_KEY_CSS => [], self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_XPATH => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; /** * the visited nodes with the XPath paths as array keys * * @var \DOMElement[] */ private $visitedNodes = []; /** * the styles to apply to the nodes with the XPath paths as array keys for the outer array * and the attribute names/values as key/value pairs for the inner array * * @var string[][] */ private $styleAttributesForNodes = []; /** * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved. * If set to false, the value of the style attributes will be discarded. * * @var bool */ private $isInlineStyleAttributesParsingEnabled = true; /** * Determines whether the <style> blocks in the HTML passed to this class should be parsed. * * If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML * via inline styles. * * If set to false, the <style> blocks will be left as they are in the HTML. * * @var bool */ private $isStyleBlocksParsingEnabled = true; /** * For calculating selector precedence order. * Keys are a regular expression part to match before a CSS name. * Values are a multiplier factor per match to weight specificity. * * @var int[] */ private $selectorPrecedenceMatchers = [ // IDs: worth 10000 '\\#' => 10000, // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100 '(?:\\.|\\[|(?<!:):(?!not\\())' => 100, // elements (not attribute values or `:not`), pseudo-elements: worth 1 '(?:(?<![="\':\\w\\-])|::)' => 1, ]; /** * @var string[] */ private $xPathRules = [ // attribute presence '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/' => '*[@\\1]', // type and attribute exact value '/(\\w)\\[(\\w+)\\=[\'"]?([\\w\\s]+)[\'"]?\\]/' => '\\1[@\\2="\\3"]', // type and attribute value with ~ (one word within a whitespace-separated list of words) '/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/' => '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]', // type and attribute value with | (either exact value match or prefix followed by a hyphen) '/([\\w\\*]+)\\[(\\w+)[\\s]*\\|\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/' => '\\1[@\\2="\\3" or starts-with(@\\2, concat("\\3", "-"))]', // type and attribute value with ^ (prefix match) '/([\\w\\*]+)\\[(\\w+)[\\s]*\\^\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/' => '\\1[starts-with(@\\2, "\\3")]', // type and attribute value with * (substring match) '/([\\w\\*]+)\\[(\\w+)[\\s]*\\*\\=[\\s]*[\'"]?([\\w\\-_\\s\\/:;]+)[\'"]?\\]/' => '\\1[contains(@\\2, "\\3")]', // adjacent sibling '/\\s*\\+\\s*/' => '/following-sibling::*[1]/self::', // child '/\\s*>\\s*/' => '/', // descendant (don't match spaces within already translated XPath predicates) '/\\s+(?![^\\[\\]]*+\\])/' => '//', // type and :first-child '/([^\\/]+):first-child/i' => '*[1]/self::\\1', // type and :last-child '/([^\\/]+):last-child/i' => '*[last()]/self::\\1', // The following matcher will break things if it is placed before the adjacent matcher. // So one of the matchers matches either too much or not enough. // type and attribute value with $ (suffix match) '/([\\w\\*]+)\\[(\\w+)[\\s]*\\$\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/' => '\\1[substring(@\\2, string-length(@\\2) - string-length("\\3") + 1) = "\\3"]', ]; /** * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them. * * @var bool */ private $debug = false; /** * @param string $unprocessedHtml the HTML to process, must be UTF-8-encoded * @param string $css the CSS to merge, must be UTF-8-encoded */ public function __construct($unprocessedHtml = '', $css = '') { if ($unprocessedHtml !== '') { $this->setHtml($unprocessedHtml); } $this->setCss($css); } /** * Sets the HTML to process. * * @param string $html the HTML to process, must be UTF-encoded, must not be empty * * @return void * * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string */ public function setHtml($html) { if (!\is_string($html)) { throw new \InvalidArgumentException('The provided HTML must be a string.', 1540403913); } if ($html === '') { throw new \InvalidArgumentException('The provided HTML must not be empty.', 1540403910); } $this->createUnifiedDomDocument($html); } /** * Provides access to the internal DOMDocument representation of the HTML in its current state. * * @return \DOMDocument */ public function getDomDocument() { return $this->domDocument; } /** * Sets the CSS to merge with the HTML. * * @param string $css the CSS to merge, must be UTF-8-encoded * * @return void */ public function setCss($css) { $this->css = $css; } /** * Renders the normalized and processed HTML. * * @return string */ protected function render() { $htmlWithPossibleErroneousClosingTags = $this->domDocument->saveHTML(); return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); } /** * Renders the content of the BODY element of the normalized and processed HTML. * * @return string */ protected function renderBodyContent() { $htmlWithPossibleErroneousClosingTags = $this->domDocument->saveHTML($this->getBodyElement()); $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); return \preg_replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml); } /** * Eliminates any invalid closing tags for void elements from the given HTML. * * @param string $html * * @return string */ private function removeSelfClosingTagsClosingTags($html) { return \preg_replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html); } /** * Returns the BODY element. * * This method assumes that there always is a BODY element. * * @return \DOMElement */ private function getBodyElement() { return $this->domDocument->getElementsByTagName('body')->item(0); } /** * Returns the HEAD element. * * This method assumes that there always is a HEAD element. * * @return \DOMElement */ private function getHeadElement() { return $this->domDocument->getElementsByTagName('head')->item(0); } /** * Applies $this->css to the given HTML and returns the HTML with the CSS * applied. * * This method places the CSS inline. * * @return string * * @throws \BadMethodCallException */ public function emogrify() { $this->assertExistenceOfHtml(); $this->process(); return $this->render(); } /** * Applies $this->css to the given HTML and returns only the HTML content * within the <body> tag. * * This method places the CSS inline. * * @return string * * @throws \BadMethodCallException */ public function emogrifyBodyContent() { $this->assertExistenceOfHtml(); $this->process(); return $this->renderBodyContent(); } /** * Checks that some HTML has been set, and throws an exception otherwise. * * @return void * * @throws \BadMethodCallException */ private function assertExistenceOfHtml() { if ($this->domDocument === null) { throw new \BadMethodCallException('Please set some HTML first.', 1390393096); } } /** * Creates a DOM document from the given HTML and stores it in $this->domDocument. * * The DOM document will always have a BODY element. * * @param string $html * * @return void */ private function createUnifiedDomDocument($html) { $this->createRawDomDocument($html); $this->ensureExistenceOfBodyElement(); } /** * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument. * * @param string $html * * @return void */ private function createRawDomDocument($html) { $domDocument = new \DOMDocument(); $domDocument->strictErrorChecking = false; $domDocument->formatOutput = true; $libXmlState = \libxml_use_internal_errors(true); $domDocument->loadHTML($this->prepareHtmlForDomConversion($html)); \libxml_clear_errors(); \libxml_use_internal_errors($libXmlState); $this->domDocument = $domDocument; $this->xPath = new \DOMXPath($this->domDocument); } /** * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed, * ensuring that the HTML will be good for creating a DOM document from it. * * @param string $html * * @return string the unified HTML */ private function prepareHtmlForDomConversion($html) { $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html); $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes); return $this->addContentTypeMetaTag($htmlWithDocumentType); } /** * Applies $this->css to $this->domDocument. * * This method places the CSS inline. * * @return void * * @throws \InvalidArgumentException */ protected function process() { $this->clearAllCaches(); $this->purgeVisitedNodes(); \set_error_handler([$this, 'handleXpathQueryWarnings'], E_WARNING); $this->removeUnprocessableTags(); $this->normalizeStyleAttributesOfAllNodes(); // grab any existing style blocks from the html and append them to the existing CSS // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS) $allCss = $this->css; if ($this->isStyleBlocksParsingEnabled) { $allCss .= $this->getCssFromAllStyleNodes(); } $cssWithoutComments = $this->removeCssComments($allCss); list($cssWithoutCommentsCharsetOrImport, $cssImportRules) = $this->extractImportAndCharsetRules($cssWithoutComments); $excludedNodes = $this->getNodesToExclude(); $cssRules = $this->parseCssRules($cssWithoutCommentsCharsetOrImport); foreach ($cssRules['inlinable'] as $cssRule) { // There's no real way to test "PHP Warning" output generated by the following XPath query unless PHPUnit // converts it to an exception. Unfortunately, this would only apply to tests and not work for production // executions, which can still flood logs/output unnecessarily. Instead, Emogrifier's error handler should // always throw an exception and it must be caught here and only rethrown if in debug mode. try { // \DOMXPath::query will always return a DOMNodeList or throw an exception when errors are caught. $nodesMatchingCssSelectors = $this->xPath->query($this->translateCssToXpath($cssRule['selector'])); } catch (\InvalidArgumentException $e) { if ($this->debug) { throw $e; } continue; } /** @var \DOMElement $node */ foreach ($nodesMatchingCssSelectors as $node) { if (\in_array($node, $excludedNodes, true)) { continue; } $this->copyInlinableCssToStyleAttribute($node, $cssRule); } } if ($this->isInlineStyleAttributesParsingEnabled) { $this->fillStyleAttributesWithMergedStyles(); } $this->removeImportantAnnotationFromAllInlineStyles(); $this->copyUninlinableCssToStyleNode($cssRules['uninlinable'], $cssImportRules); \restore_error_handler(); } /** * Searches for all nodes with a style attribute and removes the "!important" annotations out of * the inline style declarations, eventually by rearranging declarations. * * @return void */ private function removeImportantAnnotationFromAllInlineStyles() { foreach ($this->getAllNodesWithStyleAttribute() as $node) { $this->removeImportantAnnotationFromNodeInlineStyle($node); } } /** * Removes the "!important" annotations out of the inline style declarations, * eventually by rearranging declarations. * Rearranging needed when !important shorthand properties are followed by some of their * not !important expanded-version properties. * For example "font: 12px serif !important; font-size: 13px;" must be reordered * to "font-size: 13px; font: 12px serif;" in order to remain correct. * * @param \DOMElement $node * * @return void */ private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node) { $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style')); $regularStyleDeclarations = []; $importantStyleDeclarations = []; foreach ($inlineStyleDeclarations as $property => $value) { if ($this->attributeValueIsImportant($value)) { $importantStyleDeclarations[$property] = \trim(\str_replace('!important', '', $value)); } else { $regularStyleDeclarations[$property] = $value; } } $inlineStyleDeclarationsInNewOrder = \array_merge( $regularStyleDeclarations, $importantStyleDeclarations ); $node->setAttribute( 'style', $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder) ); } /** * Returns a list with all DOM nodes that have a style attribute. * * @return \DOMNodeList */ private function getAllNodesWithStyleAttribute() { return $this->xPath->query('//*[@style]'); } /** * Extracts and parses the individual rules from a CSS string. * * @param string $css a string of raw CSS code with comments removed * * @return string[][][] A 2-entry array with the key "inlinable" containing rules which can be inlined as `style` * attributes and the key "uninlinable" containing rules which cannot. Each value is an array of string * sub-arrays with the keys * "media" (the media query string, e.g. "@media screen and (max-width: 480px)", * or an empty string if not from a `@media` rule), * "selector" (the CSS selector, e.g., "*" or "header h1"), * "hasUnmatchablePseudo" (true if that selector contains pseudo-elements or dynamic pseudo-classes * such that the declarations cannot be applied inline), * "declarationsBlock" (the semicolon-separated CSS declarations for that selector, * e.g., "color: red; height: 4px;"), * and "line" (the line number e.g. 42) */ private function parseCssRules($css) { $cssKey = \md5($css); if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) { $matches = $this->getCssRuleMatches($css); $cssRules = [ 'inlinable' => [], 'uninlinable' => [], ]; /** @var string[][] $matches */ /** @var string[] $cssRule */ foreach ($matches as $key => $cssRule) { $cssDeclaration = \trim($cssRule['declarations']); if ($cssDeclaration === '') { continue; } foreach (\explode(',', $cssRule['selectors']) as $selector) { // don't process pseudo-elements and behavioral (dynamic) pseudo-classes; // only allow structural pseudo-classes $hasPseudoElement = \strpos($selector, '::') !== false; $hasUnsupportedPseudoClass = (bool)\preg_match( '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector ); $hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass; $parsedCssRule = [ 'media' => $cssRule['media'], 'selector' => \trim($selector), 'hasUnmatchablePseudo' => $hasUnmatchablePseudo, 'declarationsBlock' => $cssDeclaration, // keep track of where it appears in the file, since order is important 'line' => $key, ]; $ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlinable' : 'uninlinable'; $cssRules[$ruleType][] = $parsedCssRule; } } \usort($cssRules['inlinable'], [$this, 'sortBySelectorPrecedence']); $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules; } return $this->caches[self::CACHE_KEY_CSS][$cssKey]; } /** * Parses a string of CSS into the media query, selectors and declarations for each ruleset in order. * * @param string $css CSS with comments removed * * @return string[][] Array of string sub-arrays with the keys * "media" (the media query string, e.g. "@media screen and (max-width: 480px)", * or an empty string if not from an `@media` rule), * "selectors" (the CSS selector(s), e.g., "*" or "h1, h2"), * "declarations" (the semicolon-separated CSS declarations for that/those selector(s), * e.g., "color: red; height: 4px;"), */ private function getCssRuleMatches($css) { $splitCss = $this->splitCssAndMediaQuery($css); $ruleMatches = []; foreach ($splitCss as $cssPart) { // process each part for selectors and definitions \preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $cssPart['css'], $matches, PREG_SET_ORDER); /** @var string[][] $matches */ foreach ($matches as $cssRule) { $ruleMatches[] = [ 'media' => $cssPart['media'], 'selectors' => $cssRule[1], 'declarations' => $cssRule[2], ]; } } return $ruleMatches; } /** * Disables the parsing of inline styles. * * @return void */ public function disableInlineStyleAttributesParsing() { $this->isInlineStyleAttributesParsingEnabled = false; } /** * Disables the parsing of <style> blocks. * * @return void */ public function disableStyleBlocksParsing() { $this->isStyleBlocksParsingEnabled = false; } /** * Clears all caches. * * @return void */ private function clearAllCaches() { $this->caches = [ self::CACHE_KEY_CSS => [], self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_XPATH => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; } /** * Purges the visited nodes. * * @return void */ private function purgeVisitedNodes() { $this->visitedNodes = []; $this->styleAttributesForNodes = []; } /** * Marks a tag for removal. * * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them. * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document. * * Note: The tags will not be removed if they have any content. * * @param string $tagName the tag name, e.g., "p" * * @return void */ public function addUnprocessableHtmlTag($tagName) { $this->unprocessableHtmlTags[] = $tagName; } /** * Drops a tag from the removal list. * * @param string $tagName the tag name, e.g., "p" * * @return void */ public function removeUnprocessableHtmlTag($tagName) { $key = \array_search($tagName, $this->unprocessableHtmlTags, true); if ($key !== false) { /** @var int|string $key */ unset($this->unprocessableHtmlTags[$key]); } } /** * Marks a media query type to keep. * * @param string $mediaName the media type name, e.g., "braille" * * @return void */ public function addAllowedMediaType($mediaName) { $this->allowedMediaTypes[$mediaName] = true; } /** * Drops a media query type from the allowed list. * * @param string $mediaName the tag name, e.g., "braille" * * @return void */ public function removeAllowedMediaType($mediaName) { if (isset($this->allowedMediaTypes[$mediaName])) { unset($this->allowedMediaTypes[$mediaName]); } } /** * Adds a selector to exclude nodes from emogrification. * * Any nodes that match the selector will not have their style altered. * * @param string $selector the selector to exclude, e.g., ".editor" * * @return void */ public function addExcludedSelector($selector) { $this->excludedSelectors[$selector] = true; } /** * No longer excludes the nodes matching this selector from emogrification. * * @param string $selector the selector to no longer exclude, e.g., ".editor" * * @return void */ public function removeExcludedSelector($selector) { if (isset($this->excludedSelectors[$selector])) { unset($this->excludedSelectors[$selector]); } } /** * Parses the document and normalizes all existing CSS attributes. * This changes 'DISPLAY: none' to 'display: none'. * We wouldn't have to do this if DOMXPath supported XPath 2.0. * Also stores a reference of nodes with existing inline styles so we don't overwrite them. * * @return void */ private function normalizeStyleAttributesOfAllNodes() { /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { if ($this->isInlineStyleAttributesParsingEnabled) { $this->normalizeStyleAttributes($node); } // Remove style attribute in every case, so we can add them back (if inline style attributes // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules; // else original inline style rules may remain at the beginning of the final inline style definition // of a node, which may give not the desired results $node->removeAttribute('style'); } } /** * Normalizes the value of the "style" attribute and saves it. * * @param \DOMElement $node * * @return void */ private function normalizeStyleAttributes(\DOMElement $node) { $normalizedOriginalStyle = \preg_replace_callback( '/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S', static function (array $m) { return \strtolower($m[0]); }, $node->getAttribute('style') ); // in order to not overwrite existing style attributes in the HTML, we // have to save the original HTML styles $nodePath = $node->getNodePath(); if (!isset($this->styleAttributesForNodes[$nodePath])) { $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle); $this->visitedNodes[$nodePath] = $node; } $node->setAttribute('style', $normalizedOriginalStyle); } /** * Merges styles from styles attributes and style nodes and applies them to the attribute nodes * * @return void */ private function fillStyleAttributesWithMergedStyles() { foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) { $node = $this->visitedNodes[$nodePath]; $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style')); $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $currentStyleAttributes, $styleAttributesForNode ) ); } } /** * This method merges old or existing name/value array with new name/value array * and then generates a string of the combined style suitable for placing inline. * This becomes the single point for CSS string generation allowing for consistent * CSS output no matter where the CSS originally came from. * * @param string[] $oldStyles * @param string[] $newStyles * * @return string */ private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles) { $cacheKey = \serialize([$oldStyles, $newStyles]); if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) { return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey]; } // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed foreach ($oldStyles as $attributeName => $attributeValue) { if (!isset($newStyles[$attributeName])) { continue; } $newAttributeValue = $newStyles[$attributeName]; if ( $this->attributeValueIsImportant($attributeValue) && !$this->attributeValueIsImportant($newAttributeValue) ) { unset($newStyles[$attributeName]); } else { unset($oldStyles[$attributeName]); } } $combinedStyles = \array_merge($oldStyles, $newStyles); $style = ''; foreach ($combinedStyles as $attributeName => $attributeValue) { $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; '; } $trimmedStyle = \rtrim($style); $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle; return $trimmedStyle; } /** * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array. * * @param string[] $styleDeclarations * * @return string */ private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations) { return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations); } /** * Checks whether $attributeValue is marked as !important. * * @param string $attributeValue * * @return bool */ private function attributeValueIsImportant($attributeValue) { return \strtolower(\substr(\trim($attributeValue), -10)) === '!important'; } /** * Copies $cssRule into the style attribute of $node. * * Note: This method does not check whether $cssRule matches $node. * * @param \DOMElement $node * @param string[][] $cssRule * * @return void */ private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule) { $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']); if ($newStyleDeclarations === []) { return; } // if it has a style attribute, get it, process it, and append (overwrite) new stuff if ($node->hasAttribute('style')) { // break it up into an associative array $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style')); } else { $oldStyleDeclarations = []; } $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations) ); } /** * Applies $cssRules to $this->domDocument, limited to the rules that actually apply to the document, by placing * them as CSS in a `<style>` element. * * @param string[][] $cssRules the "uninlinable" array of CSS rules returned by `parseCssRules` * @param string $cssImportRules This may contain any `@import` rules that should precede the CSS placed in the * `<style>` element. If there are no unlinlinable CSS rules to copy there, a `<style>` element will be * created containing just `$cssImportRules`. `$cssImportRules` may be an empty string; if it is, and there * are no unlinlinable CSS rules, an empty `<style>` element will not be created. * * @return void */ private function copyUninlinableCssToStyleNode(array $cssRules, $cssImportRules) { $css = $cssImportRules; $cssRulesRelevantForDocument = \array_filter($cssRules, [$this, 'existsMatchForSelectorInCssRule']); // avoid including unneeded class dependency if there are no rules if ($cssRulesRelevantForDocument !== []) { // support use without autoload if (!\class_exists(CssConcatenator::class)) { require_once __DIR__ . '/Emogrifier/Utilities/CssConcatenator.php'; } $cssConcatenator = new CssConcatenator(); foreach ($cssRulesRelevantForDocument as $cssRule) { $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']); } $css .= $cssConcatenator->getCss(); } // avoid adding empty style element if ($css !== '') { $this->addStyleElementToDocument($css); } } /** * Checks whether there is at least one matching element for the CSS selector contained in the `selector` element * of the provided CSS rule. * * Any dynamic pseudo-classes will be assumed to apply. If the selector matches a pseudo-element, * it will test for a match with its originating element. * * @param string[] $cssRule * * @return bool * * @throws \InvalidArgumentException */ private function existsMatchForSelectorInCssRule(array $cssRule) { $selector = $cssRule['selector']; if ($cssRule['hasUnmatchablePseudo']) { $selector = $this->removeUnmatchablePseudoComponents($selector); } return $this->existsMatchForCssSelector($selector); } /** * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary. * If such a pseudo-component is within the argument of `:not`, the entire `:not` component is removed or replaced. * * @param string $selector * * @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply, * or in the case of pseudo-elements will match their originating element. */ private function removeUnmatchablePseudoComponents($selector) { // The regex allows nested brackets via `(?2)`. // A space is temporarily prepended because the callback can't determine if the match was at the very start. $selectorWithoutNots = \ltrim(\preg_replace_callback( '/(\\s?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i', [$this, 'replaceUnmatchableNotComponent'], ' ' . $selector )); $pseudoComponentMatcher = ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+'; return \preg_replace( ['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'], ['$1*', ''], $selectorWithoutNots ); } /** * Helps `removeUnmatchablePseudoComponents()` replace or remove a selector `:not(...)` component if its argument * contains pseudo-elements or dynamic pseudo-classes. * * @param string[] $matches array of elements matched by the regular expression * * @return string the full match if there were no unmatchable pseudo components within; otherwise, any preceding * whitespace followed by "*", or an empty string if there was no preceding whitespace */ private function replaceUnmatchableNotComponent(array $matches) { list($notComponentWithAnyPrecedingWhitespace, $anyPrecedingWhitespace, $notArgumentInBrackets) = $matches; $hasUnmatchablePseudo = \preg_match( '/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-:]/i', $notArgumentInBrackets ); if ($hasUnmatchablePseudo) { return $anyPrecedingWhitespace !== '' ? $anyPrecedingWhitespace . '*' : ''; } return $notComponentWithAnyPrecedingWhitespace; } /** * Checks whether there is at least one matching element for $cssSelector. * When not in debug mode, it returns true also for invalid selectors (because they may be valid, * just not implemented/recognized yet by Emogrifier). * * @param string $cssSelector * * @return bool * * @throws \InvalidArgumentException */ private function existsMatchForCssSelector($cssSelector) { try { $nodesMatchingSelector = $this->xPath->query($this->translateCssToXpath($cssSelector)); } catch (\InvalidArgumentException $e) { if ($this->debug) { throw $e; } return true; } return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0; } /** * Returns CSS content. * * @return string */ private function getCssFromAllStyleNodes() { $styleNodes = $this->xPath->query('//style'); if ($styleNodes === false) { return ''; } $css = ''; /** @var \DOMNode $styleNode */ foreach ($styleNodes as $styleNode) { $css .= "\n\n" . $styleNode->nodeValue; $styleNode->parentNode->removeChild($styleNode); } return $css; } /** * Adds a style element with $css to $this->domDocument. * * This method is protected to allow overriding. * * @see https://github.com/MyIntervals/emogrifier/issues/103 * * @param string $css * * @return void */ protected function addStyleElementToDocument($css) { $styleElement = $this->domDocument->createElement('style', $css); $styleAttribute = $this->domDocument->createAttribute('type'); $styleAttribute->value = 'text/css'; $styleElement->appendChild($styleAttribute); $headElement = $this->getHeadElement(); $headElement->appendChild($styleElement); } /** * Checks that $this->domDocument has a BODY element and adds it if it is missing. * * @return void * * @throws \UnexpectedValueException */ private function ensureExistenceOfBodyElement() { if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) { return; } $htmlElement = $this->domDocument->getElementsByTagName('html')->item(0); if ($htmlElement === null) { throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930874); } $htmlElement->appendChild($this->domDocument->createElement('body')); } /** * Removes comments from the supplied CSS. * * @param string $css * * @return string CSS with the comments removed */ private function removeCssComments($css) { return \preg_replace('%/\\*[^*]*+(?:\\*(?!/)[^*]*+)*+\\*/%', '', $css); } /** * Extracts `@import` and `@charset` rules from the supplied CSS. These rules must not be preceded by any other * rules, or they will be ignored. (From the CSS 2.1 specification: "CSS 2.1 user agents must ignore any '@import' * rule that occurs inside a block or after any non-ignored statement other than an @charset or an @import rule." * Note also that `@charset` is case sensitive whereas `@import` is not.) * * @param string $css CSS with comments removed * * @return string[] The first element is the CSS with the valid `@import` and `@charset` rules removed. The second * element contains a concatenation of the valid `@import` rules, each followed by whatever whitespace followed it * in the original CSS (so that either unminified or minified formatting is preserved); if there were no `@import` * rules, it will be an empty string. The (valid) `@charset` rules are discarded. */ private function extractImportAndCharsetRules($css) { $possiblyModifiedCss = $css; $importRules = ''; while ( \preg_match( '/^\\s*+(@((?i)import(?-i)|charset)\\s[^;]++;\\s*+)/', $possiblyModifiedCss, $matches ) ) { list($fullMatch, $atRuleAndFollowingWhitespace, $atRuleName) = $matches; if (\strtolower($atRuleName) === 'import') { $importRules .= $atRuleAndFollowingWhitespace; } $possiblyModifiedCss = \substr($possiblyModifiedCss, \strlen($fullMatch)); } return [$possiblyModifiedCss, $importRules]; } /** * Splits input CSS code into an array of parts for different media queries, in order. * Each part is an array where: * * - key "css" will contain clean CSS code (for @media rules this will be the group rule body within "{...}") * - key "media" will contain "@media " followed by the media query list, for all allowed media queries, * or an empty string for CSS not within a media query * * Example: * * The CSS code * * "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}" * * will be parsed into the following array: * * 0 => [ * "css" => "h1 { color:red; }", * "media" => "" * ], * 1 => [ * "css" => " h1 {}", * "media" => "@media " * ] * * @param string $css * * @return string[][] */ private function splitCssAndMediaQuery($css) { $mediaTypesExpression = ''; if (!empty($this->allowedMediaTypes)) { $mediaTypesExpression = '|' . \implode('|', \array_keys($this->allowedMediaTypes)); } $mediaRuleBodyMatcher = '[^{]*+{(?:[^{}]*+{.*})?\\s*+}\\s*+'; $cssSplitForAllowedMediaTypes = \preg_split( '#(@media\\s++(?:only\\s++)?+(?:(?=[{(])' . $mediaTypesExpression . ')' . $mediaRuleBodyMatcher . ')#misU', $css, -1, PREG_SPLIT_DELIM_CAPTURE ); // filter the CSS outside/between allowed @media rules $cssCleaningMatchers = [ 'import/charset directives' => '/\\s*+@(?:import|charset)\\s[^;]++;/i', 'remaining media enclosures' => '/\\s*+@media\\s' . $mediaRuleBodyMatcher . '/isU', ]; $splitCss = []; foreach ($cssSplitForAllowedMediaTypes as $index => $cssPart) { $isMediaRule = $index % 2 !== 0; if ($isMediaRule) { \preg_match('/^([^{]*+){(.*)}[^}]*+$/s', $cssPart, $matches); $splitCss[] = [ 'css' => $matches[2], 'media' => $matches[1], ]; } else { $cleanedCss = \trim(\preg_replace($cssCleaningMatchers, '', $cssPart)); if ($cleanedCss !== '') { $splitCss[] = [ 'css' => $cleanedCss, 'media' => '', ]; } } } return $splitCss; } /** * Removes empty unprocessable tags from the DOM document. * * @return void */ private function removeUnprocessableTags() { foreach ($this->unprocessableHtmlTags as $tagName) { // Deleting nodes from a 'live' NodeList invalidates iteration on it, so a copy must be made to iterate. $nodes = []; foreach ($this->domDocument->getElementsByTagName($tagName) as $node) { $nodes[] = $node; } /** @var \DOMNode $node */ foreach ($nodes as $node) { if (!$node->hasChildNodes()) { $node->parentNode->removeChild($node); } } } } /** * Makes sure that the passed HTML has a document type. * * @param string $html * * @return string HTML with document type */ private function ensureDocumentType($html) { $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false; if ($hasDocumentType) { return $html; } return self::DEFAULT_DOCUMENT_TYPE . $html; } /** * Adds a Content-Type meta tag for the charset. * * This method also ensures that there is a HEAD element. * * @param string $html * * @return string the HTML with the meta tag added */ private function addContentTypeMetaTag($html) { $hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false; if ($hasContentTypeMetaTag) { return $html; } // We are trying to insert the meta tag to the right spot in the DOM. // If we just prepended it to the HTML, we would lose attributes set to the HTML tag. $hasHeadTag = \stripos($html, '<head') !== false; $hasHtmlTag = \stripos($html, '<html') !== false; if ($hasHeadTag) { $reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . self::CONTENT_TYPE_META_TAG, $html); } elseif ($hasHtmlTag) { $reworkedHtml = \preg_replace( '/<html(.*?)>/i', '<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>', $html ); } else { $reworkedHtml = self::CONTENT_TYPE_META_TAG . $html; } return $reworkedHtml; } /** * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a * self-closing slash. * * @param string $html * * @return string HTML with problematic tags converted. */ private function ensurePhpUnrecognizedSelfClosingTagsAreXml($html) { return \preg_replace( '%<' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\\b[^>]*+(?<!/)(?=>)%', '$0/', $html ); } /** * @param string[] $a * @param string[] $b * * @return int */ private function sortBySelectorPrecedence(array $a, array $b) { $precedenceA = $this->getCssSelectorPrecedence($a['selector']); $precedenceB = $this->getCssSelectorPrecedence($b['selector']); // We want these sorted in ascending order so selectors with lesser precedence get processed first and // selectors with greater precedence get sorted last. $precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1); $precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1); return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals; } /** * @param string $selector * * @return int */ private function getCssSelectorPrecedence($selector) { $selectorKey = \md5($selector); if (!isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) { $precedence = 0; foreach ($this->selectorPrecedenceMatchers as $matcher => $value) { if (\trim($selector) === '') { break; } $number = 0; $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number); $precedence += ($value * $number); } $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence; } return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey]; } /** * Maps a CSS selector to an XPath query string. * * @see http://plasmasturm.org/log/444/ * * @param string $cssSelector a CSS selector * * @return string the corresponding XPath selector */ private function translateCssToXpath($cssSelector) { $paddedSelector = ' ' . $cssSelector . ' '; $lowercasePaddedSelector = \preg_replace_callback( '/\\s+\\w+\\s+/', static function (array $matches) { return \strtolower($matches[0]); }, $paddedSelector ); $trimmedLowercaseSelector = \trim($lowercasePaddedSelector); $xPathKey = \md5($trimmedLowercaseSelector); if (isset($this->caches[self::CACHE_KEY_XPATH][$xPathKey])) { return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey]; } $hasNotSelector = (bool)\preg_match( '/^([^:]+):not\\(\\s*([[:ascii:]]+)\\s*\\)$/', $trimmedLowercaseSelector, $matches ); if ($hasNotSelector) { /** @var string[] $matches */ list(, $partBeforeNot, $notContents) = $matches; $xPath = '//' . $this->translateCssToXpathPass($partBeforeNot) . '[not(' . $this->translateCssToXpathPassInline($notContents) . ')]'; } else { $xPath = '//' . $this->translateCssToXpathPass($trimmedLowercaseSelector); } $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey] = $xPath; return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey]; } /** * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector. * * @param string $trimmedLowercaseSelector * * @return string */ private function translateCssToXpathPass($trimmedLowercaseSelector) { return $this->translateCssToXpathPassWithMatchClassAttributesCallback( $trimmedLowercaseSelector, [$this, 'matchClassAttributes'] ); } /** * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector for inline usage. * * @param string $trimmedLowercaseSelector * * @return string */ private function translateCssToXpathPassInline($trimmedLowercaseSelector) { return $this->translateCssToXpathPassWithMatchClassAttributesCallback( $trimmedLowercaseSelector, [$this, 'matchClassAttributesInline'] ); } /** * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector while using * $matchClassAttributesCallback as to match the class attributes. * * @param string $trimmedLowercaseSelector * @param callable $matchClassAttributesCallback * * @return string */ private function translateCssToXpathPassWithMatchClassAttributesCallback( $trimmedLowercaseSelector, callable $matchClassAttributesCallback ) { $roughXpath = \preg_replace(\array_keys($this->xPathRules), $this->xPathRules, $trimmedLowercaseSelector); $xPathWithIdAttributeMatchers = \preg_replace_callback( self::ID_ATTRIBUTE_MATCHER, [$this, 'matchIdAttributes'], $roughXpath ); $xPathWithIdAttributeAndClassMatchers = \preg_replace_callback( self::CLASS_ATTRIBUTE_MATCHER, $matchClassAttributesCallback, $xPathWithIdAttributeMatchers ); // Advanced selectors are going to require a bit more advanced emogrification. $xPathWithIdAttributeAndClassMatchers = \preg_replace_callback( '/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', [$this, 'translateNthChild'], $xPathWithIdAttributeAndClassMatchers ); $finalXpath = \preg_replace_callback( '/([^\\/]+):nth-of-type\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', [$this, 'translateNthOfType'], $xPathWithIdAttributeAndClassMatchers ); return $finalXpath; } /** * @param string[] $match * * @return string */ private function matchIdAttributes(array $match) { return ($match[1] !== '' ? $match[1] : '*') . '[@id="' . $match[2] . '"]'; } /** * @param string[] $match * * @return string xPath class attribute query wrapped in element selector */ private function matchClassAttributes(array $match) { return ($match[1] !== '' ? $match[1] : '*') . '[' . $this->matchClassAttributesInline($match) . ']'; } /** * @param string[] $match * * @return string xPath class attribute query */ private function matchClassAttributesInline(array $match) { return 'contains(concat(" ",@class," "),concat(" ","' . \str_replace('.', '"," "))][contains(concat(" ",@class," "),concat(" ","', \substr($match[2], 1)) . '"," "))'; } /** * @param string[] $match * * @return string */ private function translateNthChild(array $match) { $parseResult = $this->parseNth($match); if (isset($parseResult[self::MULTIPLIER])) { if ($parseResult[self::MULTIPLIER] < 0) { $parseResult[self::MULTIPLIER] = \abs($parseResult[self::MULTIPLIER]); $xPathExpression = \sprintf( '*[(last() - position()) mod %1%u = %2$u]/self::%3$s', $parseResult[self::MULTIPLIER], $parseResult[self::INDEX], $match[1] ); } else { $xPathExpression = \sprintf( '*[position() mod %1$u = %2$u]/self::%3$s', $parseResult[self::MULTIPLIER], $parseResult[self::INDEX], $match[1] ); } } else { $xPathExpression = \sprintf('*[%1$u]/self::%2$s', $parseResult[self::INDEX], $match[1]); } return $xPathExpression; } /** * @param string[] $match * * @return string */ private function translateNthOfType(array $match) { $parseResult = $this->parseNth($match); if (isset($parseResult[self::MULTIPLIER])) { if ($parseResult[self::MULTIPLIER] < 0) { $parseResult[self::MULTIPLIER] = \abs($parseResult[self::MULTIPLIER]); $xPathExpression = \sprintf( '%1$s[(last() - position()) mod %2$u = %3$u]', $match[1], $parseResult[self::MULTIPLIER], $parseResult[self::INDEX] ); } else { $xPathExpression = \sprintf( '%1$s[position() mod %2$u = %3$u]', $match[1], $parseResult[self::MULTIPLIER], $parseResult[self::INDEX] ); } } else { $xPathExpression = \sprintf('%1$s[%2$u]', $match[1], $parseResult[self::INDEX]); } return $xPathExpression; } /** * @param string[] $match * * @return int[] */ private function parseNth(array $match) { if (\in_array(\strtolower($match[2]), ['even', 'odd'], true)) { // we have "even" or "odd" $index = \strtolower($match[2]) === 'even' ? 0 : 1; return [self::MULTIPLIER => 2, self::INDEX => $index]; } if (\stripos($match[2], 'n') === false) { // if there is a multiplier $index = (int)\str_replace(' ', '', $match[2]); return [self::INDEX => $index]; } if (isset($match[3])) { $multipleTerm = \str_replace($match[3], '', $match[2]); $index = (int)\str_replace(' ', '', $match[3]); } else { $multipleTerm = $match[2]; $index = 0; } $multiplier = \str_ireplace('n', '', $multipleTerm); if ($multiplier === '') { $multiplier = 1; } elseif ($multiplier === '0') { return [self::INDEX => $index]; } else { $multiplier = (int)$multiplier; } while ($index < 0) { $index += \abs($multiplier); } return [self::MULTIPLIER => $multiplier, self::INDEX => $index]; } /** * Parses a CSS declaration block into property name/value pairs. * * Example: * * The declaration block * * "color: #000; font-weight: bold;" * * will be parsed into the following array: * * "color" => "#000" * "font-weight" => "bold" * * @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty * * @return string[] * the CSS declarations with the property names as array keys and the property values as array values */ private function parseCssDeclarationsBlock($cssDeclarationsBlock) { if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) { return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock]; } $properties = []; foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) { $matches = []; if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) { continue; } $propertyName = \strtolower($matches[1]); $propertyValue = $matches[2]; $properties[$propertyName] = $propertyValue; } $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties; return $properties; } /** * Find the nodes that are not to be emogrified. * * @return \DOMElement[] * * @throws \InvalidArgumentException */ private function getNodesToExclude() { $excludedNodes = []; foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) { try { $matchingNodes = $this->xPath->query($this->translateCssToXpath($selectorToExclude)); } catch (\InvalidArgumentException $e) { if ($this->debug) { throw $e; } continue; } foreach ($matchingNodes as $node) { $excludedNodes[] = $node; } } return $excludedNodes; } /** * Handles invalid xPath expression warnings, generated during the process() method, * during querying \DOMDocument and trigger an \InvalidArgumentException with an invalid selector * or \RuntimeException, depending on the source of the warning. * * @param int $type * @param string $message * @param string $file * @param int $line * @param array $context * * @return bool always false * * @throws \InvalidArgumentException * @throws \RuntimeException */ public function handleXpathQueryWarnings(// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter $type, $message, $file, $line, array $context ) { $selector = ''; if (isset($context['cssRule']['selector'])) { // warnings generated by invalid/unrecognized selectors in method process() $selector = $context['cssRule']['selector']; } elseif (isset($context['selectorToExclude'])) { // warnings generated by invalid/unrecognized selectors in method getNodesToExclude() $selector = $context['selectorToExclude']; } elseif (isset($context['cssSelector'])) { // warnings generated by invalid/unrecognized selectors in method existsMatchForCssSelector() $selector = $context['cssSelector']; } if ($selector !== '') { throw new \InvalidArgumentException( \sprintf('%1$s in selector >> %2$s << in %3$s on line %4$u', $message, $selector, $file, $line), 1509279985 ); } // Catches eventual warnings generated by method getAllNodesWithStyleAttribute() if (isset($context['xPath'])) { throw new \RuntimeException( \sprintf('%1$s in %2$s on line %3$u', $message, $file, $line), 1509280067 ); } // the normal error handling continues when handler return false return false; } /** * Sets the debug mode. * * @param bool $debug set to true to enable debug mode * * @return void */ public function setDebug($debug) { $this->debug = $debug; } } vendor/pelago/emogrifier/LICENSE 0000644 00000002054 15132754523 0012455 0 ustar 00 MIT License Copyright (c) 2008-2018 Pelago Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/automattic/jetpack-constants/src/class-constants.php 0000644 00000006452 15132754523 0020303 0 ustar 00 <?php /** * A constants manager for Jetpack. * * @package automattic/jetpack-constants */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Constants * * Testing constants is hard. Once you define a constant, it's defined. Constants Manager is an * abstraction layer so that unit tests can set "constants" for tests. * * To test your code, you'll need to swap out `defined( 'CONSTANT' )` with `Automattic\Jetpack\Constants::is_defined( 'CONSTANT' )` * and replace `CONSTANT` with `Automattic\Jetpack\Constants::get_constant( 'CONSTANT' )`. Then in the unit test, you can set the * constant with `Automattic\Jetpack\Constants::set_constant( 'CONSTANT', $value )` and then clean up after each test with something like * this: * * function tearDown() { * Automattic\Jetpack\Constants::clear_constants(); * } */ class Constants { /** * A container for all defined constants. * * @access public * @static * * @var array. */ public static $set_constants = array(); /** * Checks if a "constant" has been set in constants Manager * and has the value of true * * @param string $name The name of the constant. * * @return bool */ public static function is_true( $name ) { return self::is_defined( $name ) && self::get_constant( $name ); } /** * Checks if a "constant" has been set in constants Manager, and if not, * checks if the constant was defined with define( 'name', 'value ). * * @param string $name The name of the constant. * * @return bool */ public static function is_defined( $name ) { return array_key_exists( $name, self::$set_constants ) ? true : defined( $name ); } /** * Attempts to retrieve the "constant" from constants Manager, and if it hasn't been set, * then attempts to get the constant with the constant() function. If that also hasn't * been set, attempts to get a value from filters. * * @param string $name The name of the constant. * * @return mixed null if the constant does not exist or the value of the constant. */ public static function get_constant( $name ) { if ( array_key_exists( $name, self::$set_constants ) ) { return self::$set_constants[ $name ]; } if ( defined( $name ) ) { return constant( $name ); } /** * Filters the value of the constant. * * @since 8.5.0 * * @param null The constant value to be filtered. The default is null. * @param String $name The constant name. */ return apply_filters( 'jetpack_constant_default_value', null, $name ); } /** * Sets the value of the "constant" within constants Manager. * * @param string $name The name of the constant. * @param string $value The value of the constant. */ public static function set_constant( $name, $value ) { self::$set_constants[ $name ] = $value; } /** * Will unset a "constant" from constants Manager if the constant exists. * * @param string $name The name of the constant. * * @return bool Whether the constant was removed. */ public static function clear_single_constant( $name ) { if ( ! array_key_exists( $name, self::$set_constants ) ) { return false; } unset( self::$set_constants[ $name ] ); return true; } /** * Resets all of the constants within constants Manager. */ public static function clear_constants() { self::$set_constants = array(); } } vendor/automattic/jetpack-autoloader/src/class-version-loader.php 0000644 00000007664 15132754523 0021351 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class loads other classes based on given parameters. */ class Version_Loader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * A map of available classes and their version and file path. * * @var array */ private $classmap; /** * A map of PSR-4 namespaces and their version and directory path. * * @var array */ private $psr4_map; /** * A map of all the files that we should load. * * @var array */ private $filemap; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. * @param array $classmap The verioned classmap to load using. * @param array $psr4_map The versioned PSR-4 map to load using. * @param array $filemap The versioned filemap to load. */ public function __construct( $version_selector, $classmap, $psr4_map, $filemap ) { $this->version_selector = $version_selector; $this->classmap = $classmap; $this->psr4_map = $psr4_map; $this->filemap = $filemap; } /** * Finds the file path for the given class. * * @param string $class_name The class to find. * * @return string|null $file_path The path to the file if found, null if no class was found. */ public function find_class_file( $class_name ) { $data = $this->select_newest_file( isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, $this->find_psr4_file( $class_name ) ); if ( ! isset( $data ) ) { return null; } return $data['path']; } /** * Load all of the files in the filemap. */ public function load_filemap() { if ( empty( $this->filemap ) ) { return; } foreach ( $this->filemap as $file_identifier => $file_data ) { if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { require_once $file_data['path']; $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; } } } /** * Compares different class sources and returns the newest. * * @param array|null $classmap_data The classmap class data. * @param array|null $psr4_data The PSR-4 class data. * * @return array|null $data */ private function select_newest_file( $classmap_data, $psr4_data ) { if ( ! isset( $classmap_data ) ) { return $psr4_data; } elseif ( ! isset( $psr4_data ) ) { return $classmap_data; } if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { return $psr4_data; } return $classmap_data; } /** * Finds the file for a given class in a PSR-4 namespace. * * @param string $class_name The class to find. * * @return array|null $data The version and path path to the file if found, null otherwise. */ private function find_psr4_file( $class_name ) { if ( ! isset( $this->psr4_map ) ) { return null; } // Don't bother with classes that have no namespace. $class_index = strrpos( $class_name, '\\' ); if ( ! $class_index ) { return null; } $class_for_path = str_replace( '\\', '/', $class_name ); // Search for the namespace by iteratively cutting off the last segment until // we find a match. This allows us to check the most-specific namespaces // first as well as minimize the amount of time spent looking. for ( $class_namespace = substr( $class_name, 0, $class_index ); ! empty( $class_namespace ); $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) ) { $namespace = $class_namespace . '\\'; if ( ! isset( $this->psr4_map[ $namespace ] ) ) { continue; } $data = $this->psr4_map[ $namespace ]; foreach ( $data['path'] as $path ) { $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; if ( file_exists( $path ) ) { return array( 'version' => $data['version'], 'path' => $path, ); } } } return null; } } vendor/automattic/jetpack-autoloader/src/class-autoloader-locator.php 0000644 00000003616 15132754523 0022211 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class locates autoloaders. */ class Autoloader_Locator { /** * The object for comparing autoloader versions. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The version selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Finds the path to the plugin with the latest autoloader. * * @param array $plugin_paths An array of plugin paths. * @param string $latest_version The latest version reference. * * @return string|null */ public function find_latest_autoloader( $plugin_paths, &$latest_version ) { $latest_plugin = null; foreach ( $plugin_paths as $plugin_path ) { $version = $this->get_autoloader_version( $plugin_path ); if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { continue; } $latest_version = $version; $latest_plugin = $plugin_path; } return $latest_plugin; } /** * Gets the path to the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string */ public function get_autoloader_path( $plugin_path ) { return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; } /** * Gets the version for the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string|null */ public function get_autoloader_version( $plugin_path ) { $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; if ( ! file_exists( $classmap ) ) { return null; } $classmap = require $classmap; if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { return $classmap[ AutoloadGenerator::class ]['version']; } return null; } } vendor/automattic/jetpack-autoloader/src/class-path-processor.php 0000644 00000012557 15132754523 0021366 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles dealing with paths for the autoloader. */ class Path_Processor { /** * Given a path this will replace any of the path constants with a token to represent it. * * @param string $path The path we want to process. * * @return string The tokenized path. */ public function tokenize_path_constants( $path ) { $path = wp_normalize_path( $path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $len = strlen( $constant_path ); if ( substr( $path, 0, $len ) !== $constant_path ) { continue; } return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); } return $path; } /** * Given a path this will replace any of the path constant tokens with the expanded path. * * @param string $tokenized_path The path we want to process. * * @return string The expanded path. */ public function untokenize_path_constants( $tokenized_path ) { $tokenized_path = wp_normalize_path( $tokenized_path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $constant = '{{' . $constant . '}}'; $len = strlen( $constant ); if ( substr( $tokenized_path, 0, $len ) !== $constant ) { continue; } return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); } return $tokenized_path; } /** * Given a file and an array of places it might be, this will find the absolute path and return it. * * @param string $file The plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|false Returns the absolute path to the directory, otherwise false. */ public function find_directory_with_autoloader( $file, $directories_to_check ) { $file = wp_normalize_path( $file ); if ( ! $this->is_absolute_path( $file ) ) { $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); if ( ! isset( $file ) ) { return false; } } // We need the real path for consistency with __DIR__ paths. $file = $this->get_real_path( $file ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged $directory = @is_file( $file ) ? dirname( $file ) : $file; if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { return false; } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged return $directory; } /** * Fetches an array of normalized paths keyed by the constant they came from. * * @return string[] The normalized paths keyed by the constant. */ private function get_normalized_constants() { $raw_constants = array( // Order the constants from most-specific to least-specific. 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH', ); $constants = array(); foreach ( $raw_constants as $raw ) { if ( ! defined( $raw ) ) { continue; } $path = wp_normalize_path( constant( $raw ) ); if ( isset( $path ) ) { $constants[ $raw ] = $path; } } return $constants; } /** * Indicates whether or not a path is absolute. * * @param string $path The path to check. * * @return bool True if the path is absolute, otherwise false. */ private function is_absolute_path( $path ) { if ( 0 === strlen( $path ) || '.' === $path[0] ) { return false; } // Absolute paths on Windows may begin with a drive letter. if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { return true; } // A path starting with / or \ is absolute; anything else is relative. return ( '/' === $path[0] || '\\' === $path[0] ); } /** * Given a file and a list of directories to check, this method will try to figure out * the absolute path to the file in question. * * @param string $normalized_path The normalized path to the plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|null The absolute path to the plugin file, otherwise null. */ private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { // We're only able to find the absolute path for plugin/theme PHP files. if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { return null; } foreach ( $directories_to_check as $directory ) { $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_file( $normalized_check ) ) { return $normalized_check; } } return null; } /** * Given a path this will figure out the real path that we should be using. * * @param string $path The path to resolve. * * @return string The resolved path. */ private function get_real_path( $path ) { // We want to resolve symbolic links for consistency with __DIR__ paths. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $real_path = @realpath( $path ); if ( false === $real_path ) { // Let the autoloader deal with paths that don't exist. $real_path = $path; } // Using realpath will make it platform-specific so we must normalize it after. if ( $path !== $real_path ) { $real_path = wp_normalize_path( $real_path ); } return $real_path; } } vendor/automattic/jetpack-autoloader/src/class-manifest-reader.php 0000644 00000004673 15132754523 0021463 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class reads autoloader manifest files. */ class Manifest_Reader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Reads all of the manifests in the given plugin paths. * * @param array $plugin_paths The paths to the plugins we're loading the manifest in. * @param string $manifest_path The path that we're loading the manifest from in each plugin. * @param array $path_map The path map to add the contents of the manifests to. * * @return array $path_map The path map we've built using the manifests in each plugin. */ public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { $file_paths = array_map( function ( $path ) use ( $manifest_path ) { return trailingslashit( $path ) . $manifest_path; }, $plugin_paths ); foreach ( $file_paths as $path ) { $this->register_manifest( $path, $path_map ); } return $path_map; } /** * Registers a plugin's manifest file with the path map. * * @param string $manifest_path The absolute path to the manifest that we're loading. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_manifest( $manifest_path, &$path_map ) { if ( ! is_readable( $manifest_path ) ) { return; } $manifest = require $manifest_path; if ( ! is_array( $manifest ) ) { return; } foreach ( $manifest as $key => $data ) { $this->register_record( $key, $data, $path_map ); } } /** * Registers an entry from the manifest in the path map. * * @param string $key The identifier for the entry we're registering. * @param array $data The data for the entry we're registering. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_record( $key, $data, &$path_map ) { if ( isset( $path_map[ $key ]['version'] ) ) { $selected_version = $path_map[ $key ]['version']; } else { $selected_version = null; } if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { $path_map[ $key ] = array( 'version' => $data['version'], 'path' => $data['path'], ); } } } vendor/automattic/jetpack-autoloader/src/class-autoloader-handler.php 0000644 00000010465 15132754523 0022163 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class selects the package version for the autoloader. */ class Autoloader_Handler { /** * The PHP_Autoloader instance. * * @var PHP_Autoloader */ private $php_autoloader; /** * The Hook_Manager instance. * * @var Hook_Manager */ private $hook_manager; /** * The Manifest_Reader instance. * * @var Manifest_Reader */ private $manifest_reader; /** * The Version_Selector instance. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param PHP_Autoloader $php_autoloader The PHP_Autoloader instance. * @param Hook_Manager $hook_manager The Hook_Manager instance. * @param Manifest_Reader $manifest_reader The Manifest_Reader instance. * @param Version_Selector $version_selector The Version_Selector instance. */ public function __construct( $php_autoloader, $hook_manager, $manifest_reader, $version_selector ) { $this->php_autoloader = $php_autoloader; $this->hook_manager = $hook_manager; $this->manifest_reader = $manifest_reader; $this->version_selector = $version_selector; } /** * Checks to see whether or not an autoloader is currently in the process of initializing. * * @return bool */ public function is_initializing() { // If no version has been set it means that no autoloader has started initializing yet. global $jetpack_autoloader_latest_version; if ( ! isset( $jetpack_autoloader_latest_version ) ) { return false; } // When the version is set but the classmap is not it ALWAYS means that this is the // latest autoloader and is being included by an older one. global $jetpack_packages_classmap; if ( empty( $jetpack_packages_classmap ) ) { return true; } // Version 2.4.0 added a new global and altered the reset semantics. We need to check // the other global as well since it may also point at initialization. // Note: We don't need to check for the class first because every autoloader that // will set the latest version global requires this class in the classmap. $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return true; } } return false; } /** * Activates an autoloader using the given plugins and activates it. * * @param string[] $plugins The plugins to initialize the autoloader for. */ public function activate_autoloader( $plugins ) { global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); $loader = new Version_Loader( $this->version_selector, $jetpack_packages_classmap, $jetpack_packages_psr4, $jetpack_packages_filemap ); $this->php_autoloader->register_autoloader( $loader ); // Now that the autoloader is active we can load the filemap. $loader->load_filemap(); } /** * Resets the active autoloader and all related global state. */ public function reset_autoloader() { $this->php_autoloader->unregister_autoloader(); $this->hook_manager->reset(); // Clear all of the autoloader globals so that older autoloaders don't do anything strange. global $jetpack_autoloader_latest_version; $jetpack_autoloader_latest_version = null; global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! } } vendor/automattic/jetpack-autoloader/src/class-plugin-locator.php 0000644 00000010174 15132754523 0021345 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class scans the WordPress installation to find active plugins. */ class Plugin_Locator { /** * The path processor for finding plugin paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Path_Processor $path_processor The Path_Processor instance. */ public function __construct( $path_processor ) { $this->path_processor = $path_processor; } /** * Finds the path to the current plugin. * * @return string $path The path to the current plugin. * * @throws \RuntimeException If the current plugin does not have an autoloader. */ public function find_current_plugin() { // Escape from `vendor/__DIR__` to root plugin directory. $plugin_directory = dirname( dirname( __DIR__ ) ); // Use the path processor to ensure that this is an autoloader we're referencing. $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); if ( false === $path ) { throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); } return $path; } /** * Checks a given option for plugin paths. * * @param string $option_name The option that we want to check for plugin information. * @param bool $site_option Indicates whether or not we want to check the site option. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_option( $option_name, $site_option = false ) { $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); if ( false === $raw ) { return array(); } return $this->convert_plugins_to_paths( $raw ); } /** * Checks for plugins in the `action` request parameter. * * @param string[] $allowed_actions The actions that we're allowed to return plugins for. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_request_action( $allowed_actions ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended /** * Note: we're not actually checking the nonce here because it's too early * in the execution. The pluggable functions are not yet loaded to give * plugins a chance to plug their versions. Therefore we're doing the bare * minimum: checking whether the nonce exists and it's in the right place. * The request will fail later if the nonce doesn't pass the check. */ if ( empty( $_REQUEST['_wpnonce'] ) ) { return array(); } $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; if ( ! in_array( $action, $allowed_actions, true ) ) { return array(); } $plugin_slugs = array(); switch ( $action ) { case 'activate': case 'deactivate': if ( empty( $_REQUEST['plugin'] ) ) { break; } $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); break; case 'activate-selected': case 'deactivate-selected': if ( empty( $_REQUEST['checked'] ) ) { break; } $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); break; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $this->convert_plugins_to_paths( $plugin_slugs ); } /** * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter * out the plugins that are not directory plugins. Note that array keys will also be included * if they are plugin paths! * * @param string[] $plugins Plugin paths or slugs to filter. * * @return string[] */ private function convert_plugins_to_paths( $plugins ) { if ( ! is_array( $plugins ) || empty( $plugins ) ) { return array(); } // We're going to look for plugins in the standard directories. $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); $plugin_paths = array(); foreach ( $plugins as $key => $value ) { $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } } return $plugin_paths; } } vendor/automattic/jetpack-autoloader/src/class-hook-manager.php 0000644 00000003643 15132754523 0020761 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Allows the latest autoloader to register hooks that can be removed when the autoloader is reset. */ class Hook_Manager { /** * An array containing all of the hooks that we've registered. * * @var array */ private $registered_hooks; /** * The constructor. */ public function __construct() { $this->registered_hooks = array(); } /** * Adds an action to WordPress and registers it internally. * * @param string $tag The name of the action which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the action. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_action( $tag, $callable, $priority, $accepted_args ); } /** * Adds a filter to WordPress and registers it internally. * * @param string $tag The name of the filter which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the filter. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_filter( $tag, $callable, $priority, $accepted_args ); } /** * Removes all of the registered hooks. */ public function reset() { foreach ( $this->registered_hooks as $tag => $hooks ) { foreach ( $hooks as $hook ) { remove_filter( $tag, $hook['callable'], $hook['priority'] ); } } $this->registered_hooks = array(); } } vendor/automattic/jetpack-autoloader/src/AutoloadProcessor.php 0000644 00000012357 15132754523 0020760 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoload Processor. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase namespace Automattic\Jetpack\Autoloader; /** * Class AutoloadProcessor. */ class AutoloadProcessor { /** * A callable for scanning a directory for all of its classes. * * @var callable */ private $classmapScanner; /** * A callable for transforming a path into one to be used in code. * * @var callable */ private $pathCodeTransformer; /** * The constructor. * * @param callable $classmapScanner A callable for scanning a directory for all of its classes. * @param callable $pathCodeTransformer A callable for transforming a path into one to be used in code. */ public function __construct( $classmapScanner, $pathCodeTransformer ) { $this->classmapScanner = $classmapScanner; $this->pathCodeTransformer = $pathCodeTransformer; } /** * Processes the classmap autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array $processed */ public function processClassmap( $autoloads, $scanPsrPackages ) { // We can't scan PSR packages if we don't actually have any. if ( empty( $autoloads['psr-4'] ) ) { $scanPsrPackages = false; } if ( empty( $autoloads['classmap'] ) && ! $scanPsrPackages ) { return null; } $excludedClasses = null; if ( ! empty( $autoloads['exclude-from-classmap'] ) ) { $excludedClasses = '{(' . implode( '|', $autoloads['exclude-from-classmap'] ) . ')}'; } $processed = array(); if ( $scanPsrPackages ) { foreach ( $autoloads['psr-4'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } /* * PSR-0 namespaces are converted to classmaps for both optimized and unoptimized autoloaders because any new * development should use classmap or PSR-4 autoloading. */ if ( ! empty( $autoloads['psr-0'] ) ) { foreach ( $autoloads['psr-0'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } if ( ! empty( $autoloads['classmap'] ) ) { foreach ( $autoloads['classmap'] as $package ) { $classmap = call_user_func( $this->classmapScanner, $package['path'], $excludedClasses, null ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } return $processed; } /** * Processes the PSR-4 autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array $processed */ public function processPsr4Packages( $autoloads, $scanPsrPackages ) { if ( $scanPsrPackages || empty( $autoloads['psr-4'] ) ) { return null; } $processed = array(); foreach ( $autoloads['psr-4'] as $namespace => $packages ) { $namespace = empty( $namespace ) ? null : $namespace; $paths = array(); foreach ( $packages as $package ) { $paths[] = call_user_func( $this->pathCodeTransformer, $package['path'] ); } $processed[ $namespace ] = array( 'version' => $package['version'], 'path' => $paths, ); } return $processed; } /** * Processes the file autoloads into a relative format including the version for each file. * * @param array $autoloads The autoloads we are processing. * * @return array|null $processed */ public function processFiles( $autoloads ) { if ( empty( $autoloads['files'] ) ) { return null; } $processed = array(); foreach ( $autoloads['files'] as $file_id => $package ) { $processed[ $file_id ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $package['path'] ), ); } return $processed; } } vendor/automattic/jetpack-autoloader/src/AutoloadGenerator.php 0000644 00000033623 15132754523 0020726 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoloader Generator. * * @package automattic/jetpack-autoloader */ // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound // phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound // phpcs:disable PHPCompatibility.FunctionDeclarations.NewClosure.Found // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_dirFound // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase namespace Automattic\Jetpack\Autoloader; use Composer\Autoload\ClassMapGenerator; use Composer\Composer; use Composer\Config; use Composer\Installer\InstallationManager; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\PackageSorter; /** * Class AutoloadGenerator. */ class AutoloadGenerator { /** * The filesystem utility. * * @var Filesystem */ private $filesystem; /** * Instantiate an AutoloadGenerator object. * * @param IOInterface $io IO object. */ public function __construct( IOInterface $io = null ) { $this->io = $io; $this->filesystem = new Filesystem(); } /** * Dump the Jetpack autoloader files. * * @param Composer $composer The Composer object. * @param Config $config Config object. * @param InstalledRepositoryInterface $localRepo Installed Repository object. * @param PackageInterface $mainPackage Main Package object. * @param InstallationManager $installationManager Manager for installing packages. * @param string $targetDir Path to the current target directory. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * @param string $suffix The autoloader suffix. */ public function dump( Composer $composer, Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = null ) { $this->filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) ); $packageMap = $composer->getAutoloadGenerator()->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() ); $autoloads = $this->parseAutoloads( $packageMap, $mainPackage ); // Convert the autoloads into a format that the manifest generator can consume more easily. $basePath = $this->filesystem->normalizePath( realpath( getcwd() ) ); $vendorPath = $this->filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) ); $processedAutoloads = $this->processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ); unset( $packageMap, $autoloads ); // Make sure none of the legacy files remain that can lead to problems with the autoloader. $this->removeLegacyFiles( $vendorPath ); // Write all of the files now that we're done. $this->writeAutoloaderFiles( $vendorPath . '/jetpack-autoloader/', $suffix ); $this->writeManifests( $vendorPath . '/' . $targetDir, $processedAutoloads ); if ( ! $scanPsrPackages ) { $this->io->writeError( '<warning>You are generating an unoptimized autoloader. If this is a production build, consider using the -o option.</warning>' ); } } /** * Compiles an ordered list of namespace => path mappings * * @param array $packageMap Array of array(package, installDir-relative-to-composer.json). * @param PackageInterface $mainPackage Main package instance. * * @return array The list of path mappings. */ public function parseAutoloads( array $packageMap, PackageInterface $mainPackage ) { $rootPackageMap = array_shift( $packageMap ); $sortedPackageMap = $this->sortPackageMap( $packageMap ); $sortedPackageMap[] = $rootPackageMap; array_unshift( $packageMap, $rootPackageMap ); $psr0 = $this->parseAutoloadsType( $packageMap, 'psr-0', $mainPackage ); $psr4 = $this->parseAutoloadsType( $packageMap, 'psr-4', $mainPackage ); $classmap = $this->parseAutoloadsType( array_reverse( $sortedPackageMap ), 'classmap', $mainPackage ); $files = $this->parseAutoloadsType( $sortedPackageMap, 'files', $mainPackage ); krsort( $psr0 ); krsort( $psr4 ); return array( 'psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files, ); } /** * Sorts packages by dependency weight * * Packages of equal weight retain the original order * * @param array $packageMap The package map. * * @return array */ protected function sortPackageMap( array $packageMap ) { $packages = array(); $paths = array(); foreach ( $packageMap as $item ) { list( $package, $path ) = $item; $name = $package->getName(); $packages[ $name ] = $package; $paths[ $name ] = $path; } $sortedPackages = PackageSorter::sortPackages( $packages ); $sortedPackageMap = array(); foreach ( $sortedPackages as $package ) { $name = $package->getName(); $sortedPackageMap[] = array( $packages[ $name ], $paths[ $name ] ); } return $sortedPackageMap; } /** * Returns the file identifier. * * @param PackageInterface $package The package instance. * @param string $path The path. */ protected function getFileIdentifier( PackageInterface $package, $path ) { return md5( $package->getName() . ':' . $path ); } /** * Returns the path code for the given path. * * @param Filesystem $filesystem The filesystem instance. * @param string $basePath The base path. * @param string $vendorPath The vendor path. * @param string $path The path. * * @return string The path code. */ protected function getPathCode( Filesystem $filesystem, $basePath, $vendorPath, $path ) { if ( ! $filesystem->isAbsolutePath( $path ) ) { $path = $basePath . '/' . $path; } $path = $filesystem->normalizePath( $path ); $baseDir = ''; if ( 0 === strpos( $path . '/', $vendorPath . '/' ) ) { $path = substr( $path, strlen( $vendorPath ) ); $baseDir = '$vendorDir'; if ( false !== $path ) { $baseDir .= ' . '; } } else { $path = $filesystem->normalizePath( $filesystem->findShortestPath( $basePath, $path, true ) ); if ( ! $filesystem->isAbsolutePath( $path ) ) { $baseDir = '$baseDir . '; $path = '/' . $path; } } if ( strpos( $path, '.phar' ) !== false ) { $baseDir = "'phar://' . " . $baseDir; } return $baseDir . ( ( false !== $path ) ? var_export( $path, true ) : '' ); } /** * This function differs from the composer parseAutoloadsType in that beside returning the path. * It also return the path and the version of a package. * * Supports PSR-4, PSR-0, and classmap parsing. * * @param array $packageMap Map of all the packages. * @param string $type Type of autoloader to use. * @param PackageInterface $mainPackage Instance of the Package Object. * * @return array */ protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) { $autoloads = array(); foreach ( $packageMap as $item ) { list($package, $installPath) = $item; $autoload = $package->getAutoload(); if ( $package === $mainPackage ) { $autoload = array_merge_recursive( $autoload, $package->getDevAutoload() ); } if ( null !== $package->getTargetDir() && $package !== $mainPackage ) { $installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) ); } if ( in_array( $type, array( 'psr-4', 'psr-0' ), true ) && isset( $autoload[ $type ] ) && is_array( $autoload[ $type ] ) ) { foreach ( $autoload[ $type ] as $namespace => $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $namespace ][] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? ); } } } if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) { foreach ( $autoload['classmap'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? ); } } } if ( 'files' === $type && isset( $autoload['files'] ) && is_array( $autoload['files'] ) ) { foreach ( $autoload['files'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $this->getFileIdentifier( $package, $path ) ] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the file comes from the package - should we try to parse it? ); } } } } return $autoloads; } /** * Given Composer's autoloads this will convert them to a version that we can use to generate the manifests. * * When the $scanPsrPackages argument is true, PSR-4 namespaces are converted to classmaps. When $scanPsrPackages * is false, PSR-4 namespaces are not converted to classmaps. * * PSR-0 namespaces are always converted to classmaps. * * @param array $autoloads The autoloads we want to process. * @param bool $scanPsrPackages Whether or not PSR-4 packages should be converted to a classmap. * @param string $vendorPath The path to the vendor directory. * @param string $basePath The path to the current directory. * * @return array $processedAutoloads */ private function processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ) { $processor = new AutoloadProcessor( function ( $path, $excludedClasses, $namespace ) use ( $basePath ) { $dir = $this->filesystem->normalizePath( $this->filesystem->isAbsolutePath( $path ) ? $path : $basePath . '/' . $path ); return ClassMapGenerator::createMap( $dir, $excludedClasses, null, // Don't pass the IOInterface since the normal autoload generation will have reported already. empty( $namespace ) ? null : $namespace ); }, function ( $path ) use ( $basePath, $vendorPath ) { return $this->getPathCode( $this->filesystem, $basePath, $vendorPath, $path ); } ); return array( 'psr-4' => $processor->processPsr4Packages( $autoloads, $scanPsrPackages ), 'classmap' => $processor->processClassmap( $autoloads, $scanPsrPackages ), 'files' => $processor->processFiles( $autoloads ), ); } /** * Removes all of the legacy autoloader files so they don't cause any problems. * * @param string $outDir The directory legacy files are written to. */ private function removeLegacyFiles( $outDir ) { $files = array( 'autoload_functions.php', 'class-autoloader-handler.php', 'class-classes-handler.php', 'class-files-handler.php', 'class-plugins-handler.php', 'class-version-selector.php', ); foreach ( $files as $file ) { $this->filesystem->remove( $outDir . '/' . $file ); } } /** * Writes all of the autoloader files to disk. * * @param string $outDir The directory to write to. * @param string $suffix The unique autoloader suffix. */ private function writeAutoloaderFiles( $outDir, $suffix ) { $this->io->writeError( "<info>Generating jetpack autoloader ($outDir)</info>" ); // We will remove all autoloader files to generate this again. $this->filesystem->emptyDirectory( $outDir ); // Write the autoloader files. AutoloadFileWriter::copyAutoloaderFiles( $this->io, $outDir, $suffix ); } /** * Writes all of the manifest files to disk. * * @param string $outDir The directory to write to. * @param array $processedAutoloads The processed autoloads. */ private function writeManifests( $outDir, $processedAutoloads ) { $this->io->writeError( "<info>Generating jetpack autoloader manifests ($outDir)</info>" ); $manifestFiles = array( 'classmap' => 'jetpack_autoload_classmap.php', 'psr-4' => 'jetpack_autoload_psr4.php', 'files' => 'jetpack_autoload_filemap.php', ); foreach ( $manifestFiles as $key => $file ) { // Make sure the file doesn't exist so it isn't there if we don't write it. $this->filesystem->remove( $outDir . '/' . $file ); if ( empty( $processedAutoloads[ $key ] ) ) { continue; } $content = ManifestGenerator::buildManifest( $key, $file, $processedAutoloads[ $key ] ); if ( empty( $content ) ) { continue; } if ( file_put_contents( $outDir . '/' . $file, $content ) ) { $this->io->writeError( " <info>Generated: $file</info>" ); } else { $this->io->writeError( " <error>Error: $file</error>" ); } } } } vendor/automattic/jetpack-autoloader/src/ManifestGenerator.php 0000644 00000007132 15132754523 0020720 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Manifest Generator. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export namespace Automattic\Jetpack\Autoloader; /** * Class ManifestGenerator. */ class ManifestGenerator { /** * Builds a manifest file for the given autoloader type. * * @param string $autoloaderType The type of autoloader to build a manifest for. * @param string $fileName The filename of the manifest. * @param array $content The manifest content to generate using. * * @return string|null $manifestFile * @throws \InvalidArgumentException When an invalid autoloader type is given. */ public static function buildManifest( $autoloaderType, $fileName, $content ) { if ( empty( $content ) ) { return null; } switch ( $autoloaderType ) { case 'classmap': case 'files': return self::buildStandardManifest( $fileName, $content ); case 'psr-4': return self::buildPsr4Manifest( $fileName, $content ); } throw new \InvalidArgumentException( 'An invalid manifest type of ' . $autoloaderType . ' was passed!' ); } /** * Builds the contents for the standard manifest file. * * @param string $fileName The filename we are building. * @param array $manifestData The formatted data for the manifest. * * @return string|null $manifestFile */ private static function buildStandardManifest( $fileName, $manifestData ) { $fileContent = PHP_EOL; foreach ( $manifestData as $key => $data ) { $key = var_export( $key, true ); $versionCode = var_export( $data['version'], true ); $fileContent .= <<<MANIFEST_CODE $key => array( 'version' => $versionCode, 'path' => {$data['path']} ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Builds the contents for the PSR-4 manifest file. * * @param string $fileName The filename we are building. * @param array $namespaces The formatted PSR-4 data for the manifest. * * @return string|null $manifestFile */ private static function buildPsr4Manifest( $fileName, $namespaces ) { $fileContent = PHP_EOL; foreach ( $namespaces as $namespace => $data ) { $namespaceCode = var_export( $namespace, true ); $versionCode = var_export( $data['version'], true ); $pathCode = 'array( ' . implode( ', ', $data['path'] ) . ' )'; $fileContent .= <<<MANIFEST_CODE $namespaceCode => array( 'version' => $versionCode, 'path' => $pathCode ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Generate the PHP that will be used in the file. * * @param string $fileName The filename we are building. * @param string $content The content to be written into the file. * * @return string $fileContent */ private static function buildFile( $fileName, $content ) { return <<<INCLUDE_FILE <?php // This file `$fileName` was auto generated by automattic/jetpack-autoloader. \$vendorDir = dirname(__DIR__); \$baseDir = dirname(\$vendorDir); return array($content); INCLUDE_FILE; } } vendor/automattic/jetpack-autoloader/src/class-shutdown-handler.php 0000644 00000005203 15132754523 0021671 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles the shutdown of the autoloader. */ class Shutdown_Handler { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The plugins cached by this autoloader. * * @var string[] */ private $cached_plugins; /** * Indicates whether or not this autoloader was included by another. * * @var bool */ private $was_included_by_autoloader; /** * Constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance to use. * @param string[] $cached_plugins The plugins cached by the autoloaer. * @param bool $was_included_by_autoloader Indicates whether or not the autoloader was included by another. */ public function __construct( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) { $this->plugins_handler = $plugins_handler; $this->cached_plugins = $cached_plugins; $this->was_included_by_autoloader = $was_included_by_autoloader; } /** * Handles the shutdown of the autoloader. */ public function __invoke() { // Don't save a broken cache if an error happens during some plugin's initialization. if ( ! did_action( 'plugins_loaded' ) ) { // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // Load the active plugins fresh since the list we pulled earlier might not contain // plugins that were activated but did not reset the autoloader. This happens // when a plugin is in the cache but not "active" when the autoloader loads. // We also want to make sure that plugins which are deactivating are not // considered "active" so that they will be removed from the cache now. try { $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); } catch ( \Exception $ex ) { // When the package is deleted before shutdown it will throw an exception. // In the event this happens we should erase the cache. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // The paths should be sorted for easy comparisons with those loaded from the cache. // Note we don't need to sort the cached entries because they're already sorted. sort( $active_plugins ); // We don't want to waste time saving a cache that hasn't changed. if ( $this->cached_plugins === $active_plugins ) { return; } $this->plugins_handler->cache_plugins( $active_plugins ); } } vendor/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php 0000644 00000014057 15132754523 0021760 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase /** * Custom Autoloader Composer Plugin, hooks into composer events to generate the custom autoloader. * * @package automattic/jetpack-autoloader */ // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound // phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound // phpcs:disable WordPress.Files.FileName.NotHyphenatedLowercase // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase namespace Automattic\Jetpack\Autoloader; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; use Composer\Script\Event; use Composer\Script\ScriptEvents; /** * Class CustomAutoloaderPlugin. * * @package automattic/jetpack-autoloader */ class CustomAutoloaderPlugin implements PluginInterface, EventSubscriberInterface { /** * IO object. * * @var IOInterface IO object. */ private $io; /** * Composer object. * * @var Composer Composer object. */ private $composer; /** * Do nothing. * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function activate( Composer $composer, IOInterface $io ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->composer = $composer; $this->io = $io; } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function deactivate( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function uninstall( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Tell composer to listen for events and do something with them. * * @return array List of subscribed events. */ public static function getSubscribedEvents() { return array( ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', ); } /** * Generate the custom autolaoder. * * @param Event $event Script event object. */ public function postAutoloadDump( Event $event ) { // When the autoloader is not required by the root package we don't want to execute it. // This prevents unwanted transitive execution that generates unused autoloaders or // at worst throws fatal executions. if ( ! $this->isRequiredByRoot() ) { return; } $config = $this->composer->getConfig(); if ( 'vendor' !== $config->raw()['config']['vendor-dir'] ) { $this->io->writeError( "\n<error>An error occurred while generating the autoloader files:", true ); $this->io->writeError( 'The project\'s composer.json or composer environment set a non-default vendor directory.', true ); $this->io->writeError( 'The default composer vendor directory must be used.</error>', true ); exit(); } $installationManager = $this->composer->getInstallationManager(); $repoManager = $this->composer->getRepositoryManager(); $localRepo = $repoManager->getLocalRepository(); $package = $this->composer->getPackage(); $optimize = $event->getFlags()['optimize']; $suffix = $this->determineSuffix(); $generator = new AutoloadGenerator( $this->io ); $generator->dump( $this->composer, $config, $localRepo, $package, $installationManager, 'composer', $optimize, $suffix ); $this->generated = true; } /** * Determine the suffix for the autoloader class. * * Reuses an existing suffix from vendor/autoload_packages.php or vendor/autoload.php if possible. * * @return string Suffix. */ private function determineSuffix() { $config = $this->composer->getConfig(); $vendorPath = $config->get( 'vendor-dir' ); // Command line. $suffix = $config->get( 'autoloader-suffix' ); if ( $suffix ) { return $suffix; } // Reuse our own suffix, if any. if ( is_readable( $vendorPath . '/autoload_packages.php' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $vendorPath . '/autoload_packages.php' ); if ( preg_match( '/^namespace Automattic\\\\Jetpack\\\\Autoloader\\\\jp([^;\s]+);/m', $content, $match ) ) { return $match[1]; } } // Reuse Composer's suffix, if any. if ( is_readable( $vendorPath . '/autoload.php' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $vendorPath . '/autoload.php' ); if ( preg_match( '{ComposerAutoloaderInit([^:\s]+)::}', $content, $match ) ) { return $match[1]; } } // Generate a random suffix. return md5( uniqid( '', true ) ); } /** * Checks to see whether or not the root package is the one that required the autoloader. * * @return bool */ private function isRequiredByRoot() { $package = $this->composer->getPackage(); $requires = $package->getRequires(); if ( ! is_array( $requires ) ) { $requires = array(); } $devRequires = $package->getDevRequires(); if ( ! is_array( $devRequires ) ) { $devRequires = array(); } $requires = array_merge( $requires, $devRequires ); if ( empty( $requires ) ) { $this->io->writeError( "\n<error>The package is not required and this should never happen?</error>", true ); exit(); } foreach ( $requires as $require ) { if ( 'automattic/jetpack-autoloader' === $require->getTarget() ) { return true; } } return false; } } vendor/automattic/jetpack-autoloader/src/class-autoloader.php 0000644 00000007573 15132754523 0020556 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class Autoloader { /** * Checks to see whether or not the autoloader should be initialized and then initializes it if so. * * @param Container|null $container The container we want to use for autoloader initialization. If none is given * then a container will be created automatically. */ public static function init( $container = null ) { // The container holds and manages the lifecycle of our dependencies // to make them easier to work with and increase flexibility. if ( ! isset( $container ) ) { require_once __DIR__ . '/class-container.php'; $container = new Container(); } // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @var Autoloader_Handler $autoloader_handler */ $autoloader_handler = $container->get( Autoloader_Handler::class ); // If the autoloader is already initializing it means that it has included us as the latest. $was_included_by_autoloader = $autoloader_handler->is_initializing(); /** @var Plugin_Locator $plugin_locator */ $plugin_locator = $container->get( Plugin_Locator::class ); /** @var Plugins_Handler $plugins_handler */ $plugins_handler = $container->get( Plugins_Handler::class ); // The current plugin is the one that we are attempting to initialize here. $current_plugin = $plugin_locator->find_current_plugin(); // The active plugins are those that we were able to discover on the site. This list will not // include mu-plugins, those activated by code, or those who are hidden by filtering. We also // want to take care to not consider the current plugin unknown if it was included by an // autoloader. This avoids the case where a plugin will be marked "active" while deactivated // due to it having the latest autoloader. $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. // Note that it's possible this list will include plugins that have since been deactivated, but after a request // the cache should be updated and the deactivated plugins will be removed. $cached_plugins = $plugins_handler->get_cached_plugins(); // We combine the active list and cached list to preemptively load classes for plugins that are // presently unknown but will be loaded during the request. While this may result in us considering packages in // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. $all_plugins = array_merge( $active_plugins, $cached_plugins ); // In particular we also include the current plugin to address the case where it is the latest autoloader // but also unknown (and not cached). We don't want it in the active list because we don't know that it // is active but we need it in the all plugins list so that it is considered by the autoloader. $all_plugins[] = $current_plugin; // We require uniqueness in the array to avoid processing the same plugin more than once. $all_plugins = array_values( array_unique( $all_plugins ) ); /** @var Latest_Autoloader_Guard $guard */ $guard = $container->get( Latest_Autoloader_Guard::class ); if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { return; } // Initialize the autoloader using the handler now that we're ready. $autoloader_handler->activate_autoloader( $all_plugins ); /** @var Hook_Manager $hook_manager */ $hook_manager = $container->get( Hook_Manager::class ); // Register a shutdown handler to clean up the autoloader. $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); // phpcs:enable Generic.Commenting.DocComment.MissingShort } } vendor/automattic/jetpack-autoloader/src/class-php-autoloader.php 0000644 00000005071 15132754523 0021332 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class PHP_Autoloader { /** * Registers the autoloader with PHP so that it can begin autoloading classes. * * @param Version_Loader $version_loader The class loader to use in the autoloader. */ public function register_autoloader( $version_loader ) { // Make sure no other autoloaders are registered. $this->unregister_autoloader(); // Set the global so that it can be used to load classes. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = $version_loader; // Ensure that the autoloader is first to avoid contention with others. spl_autoload_register( array( self::class, 'load_class' ), true, true ); } /** * Unregisters the active autoloader so that it will no longer autoload classes. */ public function unregister_autoloader() { // Remove any v2 autoloader that we've already registered. $autoload_chain = spl_autoload_functions(); foreach ( $autoload_chain as $autoloader ) { // We can identify a v2 autoloader using the namespace. $namespace_check = null; // Functions are recorded as strings. if ( is_string( $autoloader ) ) { $namespace_check = $autoloader; } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { // Static method calls have the class as the first array element. $namespace_check = $autoloader[0]; } else { // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. continue; } // Check for the namespace without the generated suffix. if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { spl_autoload_unregister( $autoloader ); } } // Clear the global now that the autoloader has been unregistered. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = null; } /** * Loads a class file if one could be found. * * Note: This function is static so that the autoloader can be easily unregistered. If * it was a class method we would have to unwrap the object to check the namespace. * * @param string $class_name The name of the class to autoload. * * @return bool Indicates whether or not a class file was loaded. */ public static function load_class( $class_name ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return; } $file = $jetpack_autoloader_loader->find_class_file( $class_name ); if ( ! isset( $file ) ) { return false; } require $file; return true; } } vendor/automattic/jetpack-autoloader/src/autoload.php 0000644 00000000173 15132754523 0017111 0 ustar 00 <?php /* HEADER */ // phpcs:ignore require_once __DIR__ . '/jetpack-autoloader/class-autoloader.php'; Autoloader::init(); vendor/automattic/jetpack-autoloader/src/class-latest-autoloader-guard.php 0000644 00000005062 15132754523 0023137 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class ensures that we're only executing the latest autoloader. */ class Latest_Autoloader_Guard { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The Autoloader_Handler instance. * * @var Autoloader_Handler */ private $autoloader_handler; /** * The Autoloader_locator instance. * * @var Autoloader_Locator */ private $autoloader_locator; /** * The constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance. * @param Autoloader_Handler $autoloader_handler The Autoloader_Handler instance. * @param Autoloader_Locator $autoloader_locator The Autoloader_Locator instance. */ public function __construct( $plugins_handler, $autoloader_handler, $autoloader_locator ) { $this->plugins_handler = $plugins_handler; $this->autoloader_handler = $autoloader_handler; $this->autoloader_locator = $autoloader_locator; } /** * Indicates whether or not the autoloader should be initialized. Note that this function * has the side-effect of actually loading the latest autoloader in the event that this * is not it. * * @param string $current_plugin The current plugin we're checking. * @param string[] $plugins The active plugins to check for autoloaders in. * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. * * @return bool True if we should stop initialization, otherwise false. */ public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { global $jetpack_autoloader_latest_version; // We need to reset the autoloader when the plugins change because // that means the autoloader was generated with a different list. if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { $this->autoloader_handler->reset_autoloader(); } // When the latest autoloader has already been found we don't need to search for it again. // We should take care however because this will also trigger if the autoloader has been // included by an older one. if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { return true; } $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); return true; } return false; } } vendor/automattic/jetpack-autoloader/src/class-plugins-handler.php 0000644 00000013071 15132754523 0021501 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles locating and caching all of the active plugins. */ class Plugins_Handler { /** * The transient key for plugin paths. */ const TRANSIENT_KEY = 'jetpack_autoloader_plugin_paths'; /** * The locator for finding plugins in different locations. * * @var Plugin_Locator */ private $plugin_locator; /** * The processor for transforming cached paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Plugin_Locator $plugin_locator The locator for finding active plugins. * @param Path_Processor $path_processor The processor for transforming cached paths. */ public function __construct( $plugin_locator, $path_processor ) { $this->plugin_locator = $plugin_locator; $this->path_processor = $path_processor; } /** * Gets all of the active plugins we can find. * * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. * * @return string[] */ public function get_active_plugins( $include_deactivating, $record_unknown ) { global $jetpack_autoloader_activating_plugins_paths; // We're going to build a unique list of plugins from a few different sources // to find all of our "active" plugins. While we need to return an integer // array, we're going to use an associative array internally to reduce // the amount of time that we're going to spend checking uniqueness // and merging different arrays together to form the output. $active_plugins = array(); // Make sure that plugins which have activated this request are considered as "active" even though // they probably won't be present in any option. if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { $active_plugins[ $path ] = $path; } } // This option contains all of the plugins that have been activated. $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // This option contains all of the multisite plugins that have been activated. if ( is_multisite() ) { $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } } // These actions contain plugins that are being activated/deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // When the current plugin isn't considered "active" there's a problem. // Since we're here, the plugin is active and currently being loaded. // We can support this case (mu-plugins and non-standard activation) // by adding the current plugin to the active list and marking it // as an unknown (activating) plugin. This also has the benefit // of causing a reset because the active plugins list has // been changed since it was saved in the global. $current_plugin = $this->plugin_locator->find_current_plugin(); if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { $active_plugins[ $current_plugin ] = $current_plugin; $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; } // When deactivating plugins aren't desired we should entirely remove them from the active list. if ( ! $include_deactivating ) { // These actions contain plugins that are being deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { unset( $active_plugins[ $path ] ); } } // Transform the array so that we don't have to worry about the keys interacting with other array types later. return array_values( $active_plugins ); } /** * Gets all of the cached plugins if there are any. * * @return string[] */ public function get_cached_plugins() { $cached = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $cached ) || empty( $cached ) ) { return array(); } // We need to expand the tokens to an absolute path for this webserver. return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); } /** * Saves the plugin list to the cache. * * @param array $plugins The plugin list to save to the cache. */ public function cache_plugins( $plugins ) { // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); set_transient( self::TRANSIENT_KEY, $plugins ); } /** * Checks to see whether or not the plugin list given has changed when compared to the * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal * with cases where the active list may change due to filtering.. * * @param string[] $plugins The plugins list to check against the global cache. * * @return bool True if the plugins have changed, otherwise false. */ public function have_plugins_changed( $plugins ) { global $jetpack_autoloader_cached_plugin_paths; if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { $jetpack_autoloader_cached_plugin_paths = $plugins; return true; } return false; } } vendor/automattic/jetpack-autoloader/src/class-container.php 0000644 00000011157 15132754523 0020372 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class manages the files and dependencies of the autoloader. */ class Container { /** * Since each autoloader's class files exist within their own namespace we need a map to * convert between the local class and a shared key. Note that no version checking is * performed on these dependencies and the first autoloader to register will be the * one that is utilized. */ const SHARED_DEPENDENCY_KEYS = array( Hook_Manager::class => 'Hook_Manager', ); /** * A map of all the dependencies we've registered with the container and created. * * @var array */ protected $dependencies; /** * The constructor. */ public function __construct() { $this->dependencies = array(); $this->register_shared_dependencies(); $this->register_dependencies(); $this->initialize_globals(); } /** * Gets a dependency out of the container. * * @param string $class The class to fetch. * * @return mixed * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. */ public function get( $class ) { if ( ! isset( $this->dependencies[ $class ] ) ) { throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); } return $this->dependencies[ $class ]; } /** * Registers all of the dependencies that are shared between all instances of the autoloader. */ private function register_shared_dependencies() { global $jetpack_autoloader_container_shared; if ( ! isset( $jetpack_autoloader_container_shared ) ) { $jetpack_autoloader_container_shared = array(); } $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { require_once __DIR__ . '/class-hook-manager.php'; $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); } $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; } /** * Registers all of the dependencies with the container. */ private function register_dependencies() { require_once __DIR__ . '/class-path-processor.php'; $this->dependencies[ Path_Processor::class ] = new Path_Processor(); require_once __DIR__ . '/class-plugin-locator.php'; $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-version-selector.php'; $this->dependencies[ Version_Selector::class ] = new Version_Selector(); require_once __DIR__ . '/class-autoloader-locator.php'; $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-php-autoloader.php'; $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); require_once __DIR__ . '/class-manifest-reader.php'; $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-plugins-handler.php'; $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( $this->get( Plugin_Locator::class ), $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-autoloader-handler.php'; $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( $this->get( PHP_Autoloader::class ), $this->get( Hook_Manager::class ), $this->get( Manifest_Reader::class ), $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-latest-autoloader-guard.php'; $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( $this->get( Plugins_Handler::class ), $this->get( Autoloader_Handler::class ), $this->get( Autoloader_Locator::class ) ); // Register any classes that we will use elsewhere. require_once __DIR__ . '/class-version-loader.php'; require_once __DIR__ . '/class-shutdown-handler.php'; } /** * Initializes any of the globals needed by the autoloader. */ private function initialize_globals() { /* * This global was retired in version 2.9. The value is set to 'false' to maintain * compatibility with older versions of the autoloader. */ global $jetpack_autoloader_including_latest; $jetpack_autoloader_including_latest = false; // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader // but was not discoverable, we will record them in this array to track them as "active". global $jetpack_autoloader_activating_plugins_paths; if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { $jetpack_autoloader_activating_plugins_paths = array(); } } } vendor/automattic/jetpack-autoloader/src/class-version-selector.php 0000644 00000003165 15132754523 0021713 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Used to select package versions. */ class Version_Selector { /** * Checks whether the selected package version should be updated. Composer development * package versions ('9999999-dev' or versions that start with 'dev-') are favored * when the JETPACK_AUTOLOAD_DEV constant is set to true. * * @param String $selected_version The currently selected package version. * @param String $compare_version The package version that is being evaluated to * determine if the version needs to be updated. * * @return bool Returns true if the selected package version should be updated, * else false. */ public function is_version_update_required( $selected_version, $compare_version ) { $use_dev_versions = defined( 'JETPACK_AUTOLOAD_DEV' ) && JETPACK_AUTOLOAD_DEV; if ( is_null( $selected_version ) ) { return true; } if ( $use_dev_versions && $this->is_dev_version( $selected_version ) ) { return false; } if ( $this->is_dev_version( $compare_version ) ) { if ( $use_dev_versions ) { return true; } else { return false; } } if ( version_compare( $selected_version, $compare_version, '<' ) ) { return true; } return false; } /** * Checks whether the given package version is a development version. * * @param String $version The package version. * * @return bool True if the version is a dev version, else false. */ public function is_dev_version( $version ) { if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { return true; } return false; } } vendor/automattic/jetpack-autoloader/src/AutoloadFileWriter.php 0000644 00000006211 15132754523 0021045 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoloader file writer. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite namespace Automattic\Jetpack\Autoloader; /** * Class AutoloadFileWriter. */ class AutoloadFileWriter { /** * The file comment to use. */ const COMMENT = <<<AUTOLOADER_COMMENT /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ AUTOLOADER_COMMENT; /** * Copies autoloader files and replaces any placeholders in them. * * @param IOInterface|null $io An IO for writing to. * @param string $outDir The directory to place the autoloader files in. * @param string $suffix The suffix to use in the autoloader's namespace. */ public static function copyAutoloaderFiles( $io, $outDir, $suffix ) { $renameList = array( 'autoload.php' => '../autoload_packages.php', ); $ignoreList = array( 'AutoloadGenerator.php', 'AutoloadProcessor.php', 'CustomAutoloaderPlugin.php', 'ManifestGenerator.php', 'AutoloadFileWriter.php', ); // Copy all of the autoloader files. $files = scandir( __DIR__ ); foreach ( $files as $file ) { // Only PHP files will be copied. if ( substr( $file, -4 ) !== '.php' ) { continue; } if ( in_array( $file, $ignoreList, true ) ) { continue; } $newFile = isset( $renameList[ $file ] ) ? $renameList[ $file ] : $file; $content = self::prepareAutoloaderFile( $file, $suffix ); $written = file_put_contents( $outDir . '/' . $newFile, $content ); if ( $io ) { if ( $written ) { $io->writeError( " <info>Generated: $newFile</info>" ); } else { $io->writeError( " <error>Error: $newFile</error>" ); } } } } /** * Prepares an autoloader file to be written to the destination. * * @param String $filename a file to prepare. * @param String $suffix Unique suffix used in the namespace. * * @return string */ private static function prepareAutoloaderFile( $filename, $suffix ) { $header = self::COMMENT; $header .= PHP_EOL; $header .= 'namespace Automattic\Jetpack\Autoloader\jp' . $suffix . ';'; $header .= PHP_EOL . PHP_EOL; $sourceLoader = fopen( __DIR__ . '/' . $filename, 'r' ); $file_contents = stream_get_contents( $sourceLoader ); return str_replace( '/* HEADER */', $header, $file_contents ); } } vendor/autoload.php 0000644 00000000262 15132754523 0010371 0 ustar 00 <?php // autoload.php @generated by Composer require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit7fa27687a59114a5aec1ac3080434897::getLoader(); vendor/maxmind-db/reader/autoload.php 0000644 00000002575 15132754523 0013664 0 ustar 00 <?php /** * PSR-4 autoloader implementation for the MaxMind\DB namespace. * First we define the 'mmdb_autoload' function, and then we register * it with 'spl_autoload_register' so that PHP knows to use it. * * @param mixed $class */ /** * Automatically include the file that defines <code>class</code>. * * @param string $class * the name of the class to load */ function mmdb_autoload($class) { /* * A project-specific mapping between the namespaces and where * they're located. By convention, we include the trailing * slashes. The one-element array here simply makes things easy * to extend in the future if (for example) the test classes * begin to use one another. */ $namespace_map = ['MaxMind\\Db\\' => __DIR__ . '/src/MaxMind/Db/']; foreach ($namespace_map as $prefix => $dir) { /* First swap out the namespace prefix with a directory... */ $path = str_replace($prefix, $dir, $class); /* replace the namespace separator with a directory separator... */ $path = str_replace('\\', '/', $path); /* and finally, add the PHP file extension to the result. */ $path = $path . '.php'; /* $path should now contain the path to a PHP file defining $class */ if (file_exists($path)) { include $path; } } } spl_autoload_register('mmdb_autoload'); vendor/maxmind-db/reader/LICENSE 0000644 00000026136 15132754523 0012347 0 ustar 00 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php 0000644 00000000300 15132754523 0022601 0 ustar 00 <?php namespace MaxMind\Db\Reader; use Exception; /** * This class should be thrown when unexpected data is found in the database. */ class InvalidDatabaseException extends Exception { } vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php 0000644 00000001276 15132754523 0016641 0 ustar 00 <?php namespace MaxMind\Db\Reader; class Util { public static function read($stream, $offset, $numberOfBytes) { if ($numberOfBytes === 0) { return ''; } if (fseek($stream, $offset) === 0) { $value = fread($stream, $numberOfBytes); // We check that the number of bytes read is equal to the number // asked for. We use ftell as getting the length of $value is // much slower. if (ftell($stream) - $offset === $numberOfBytes) { return $value; } } throw new InvalidDatabaseException( 'The MaxMind DB file contains bad data' ); } } vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php 0000644 00000010645 15132754523 0017444 0 ustar 00 <?php namespace MaxMind\Db\Reader; /** * This class provides the metadata for the MaxMind DB file. * * @property int $nodeCount This is an unsigned 32-bit * integer indicating the number of * nodes in the search tree. * @property int $recordSize This is an unsigned 16-bit * integer. It indicates the number * of bits in a record in the search * tree. Note that each node * consists of two records. * @property int $ipVersion This is an unsigned 16-bit * integer which is always 4 or 6. * It indicates whether the database * contains IPv4 or IPv6 address * data. * @property string $databaseType This is a string that indicates * the structure of each data record * associated with an IP address. * The actual definition of these * structures is left up to the * database creator. * @property array $languages An array of strings, each of * which is a language code. A given * record may contain data items * that have been localized to some * or all of these languages. This * may be undefined. * @property int $binaryFormatMajorVersion This is an unsigned 16-bit * integer indicating the major * version number for the database's * binary format. * @property int $binaryFormatMinorVersion This is an unsigned 16-bit * integer indicating the minor * version number for the database's * binary format. * @property int $buildEpoch This is an unsigned 64-bit * integer that contains the * database build timestamp as a * Unix epoch value. * @property array $description This key will always point to a * map (associative array). The keys * of that map will be language * codes, and the values will be a * description in that language as a * UTF-8 string. May be undefined * for some databases. */ class Metadata { private $binaryFormatMajorVersion; private $binaryFormatMinorVersion; private $buildEpoch; private $databaseType; private $description; private $ipVersion; private $languages; private $nodeByteSize; private $nodeCount; private $recordSize; private $searchTreeSize; public function __construct($metadata) { $this->binaryFormatMajorVersion = $metadata['binary_format_major_version']; $this->binaryFormatMinorVersion = $metadata['binary_format_minor_version']; $this->buildEpoch = $metadata['build_epoch']; $this->databaseType = $metadata['database_type']; $this->languages = $metadata['languages']; $this->description = $metadata['description']; $this->ipVersion = $metadata['ip_version']; $this->nodeCount = $metadata['node_count']; $this->recordSize = $metadata['record_size']; $this->nodeByteSize = $this->recordSize / 4; $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; } public function __get($var) { return $this->$var; } } vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php 0000644 00000024450 15132754523 0017270 0 ustar 00 <?php namespace MaxMind\Db\Reader; // @codingStandardsIgnoreLine use RuntimeException; /** * @ignore * * We subtract 1 from the log to protect against precision loss. */ \define(__NAMESPACE__ . '\_MM_MAX_INT_BYTES', (log(PHP_INT_MAX, 2) - 1) / 8); class Decoder { private $fileStream; private $pointerBase; private $pointerBaseByteSize; // This is only used for unit testing private $pointerTestHack; private $switchByteOrder; /** @ignore */ const _EXTENDED = 0; /** @ignore */ const _POINTER = 1; /** @ignore */ const _UTF8_STRING = 2; /** @ignore */ const _DOUBLE = 3; /** @ignore */ const _BYTES = 4; /** @ignore */ const _UINT16 = 5; /** @ignore */ const _UINT32 = 6; /** @ignore */ const _MAP = 7; /** @ignore */ const _INT32 = 8; /** @ignore */ const _UINT64 = 9; /** @ignore */ const _UINT128 = 10; /** @ignore */ const _ARRAY = 11; /** @ignore */ const _CONTAINER = 12; /** @ignore */ const _END_MARKER = 13; /** @ignore */ const _BOOLEAN = 14; /** @ignore */ const _FLOAT = 15; public function __construct( $fileStream, $pointerBase = 0, $pointerTestHack = false ) { $this->fileStream = $fileStream; $this->pointerBase = $pointerBase; $this->pointerBaseByteSize = $pointerBase > 0 ? log($pointerBase, 2) / 8 : 0; $this->pointerTestHack = $pointerTestHack; $this->switchByteOrder = $this->isPlatformLittleEndian(); } public function decode($offset) { $ctrlByte = \ord(Util::read($this->fileStream, $offset, 1)); ++$offset; $type = $ctrlByte >> 5; // Pointers are a special case, we don't read the next $size bytes, we // use the size to determine the length of the pointer and then follow // it. if ($type === self::_POINTER) { list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset); // for unit testing if ($this->pointerTestHack) { return [$pointer]; } list($result) = $this->decode($pointer); return [$result, $offset]; } if ($type === self::_EXTENDED) { $nextByte = \ord(Util::read($this->fileStream, $offset, 1)); $type = $nextByte + 7; if ($type < 8) { throw new InvalidDatabaseException( 'Something went horribly wrong in the decoder. An extended type ' . 'resolved to a type number < 8 (' . $type . ')' ); } ++$offset; } list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset); return $this->decodeByType($type, $offset, $size); } private function decodeByType($type, $offset, $size) { switch ($type) { case self::_MAP: return $this->decodeMap($size, $offset); case self::_ARRAY: return $this->decodeArray($size, $offset); case self::_BOOLEAN: return [$this->decodeBoolean($size), $offset]; } $newOffset = $offset + $size; $bytes = Util::read($this->fileStream, $offset, $size); switch ($type) { case self::_BYTES: case self::_UTF8_STRING: return [$bytes, $newOffset]; case self::_DOUBLE: $this->verifySize(8, $size); return [$this->decodeDouble($bytes), $newOffset]; case self::_FLOAT: $this->verifySize(4, $size); return [$this->decodeFloat($bytes), $newOffset]; case self::_INT32: return [$this->decodeInt32($bytes, $size), $newOffset]; case self::_UINT16: case self::_UINT32: case self::_UINT64: case self::_UINT128: return [$this->decodeUint($bytes, $size), $newOffset]; default: throw new InvalidDatabaseException( 'Unknown or unexpected type: ' . $type ); } } private function verifySize($expected, $actual) { if ($expected !== $actual) { throw new InvalidDatabaseException( "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" ); } } private function decodeArray($size, $offset) { $array = []; for ($i = 0; $i < $size; ++$i) { list($value, $offset) = $this->decode($offset); array_push($array, $value); } return [$array, $offset]; } private function decodeBoolean($size) { return $size === 0 ? false : true; } private function decodeDouble($bits) { // This assumes IEEE 754 doubles, but most (all?) modern platforms // use them. // // We are not using the "E" format as that was only added in // 7.0.15 and 7.1.1. As such, we must switch byte order on // little endian machines. list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits)); return $double; } private function decodeFloat($bits) { // This assumes IEEE 754 floats, but most (all?) modern platforms // use them. // // We are not using the "G" format as that was only added in // 7.0.15 and 7.1.1. As such, we must switch byte order on // little endian machines. list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits)); return $float; } private function decodeInt32($bytes, $size) { switch ($size) { case 0: return 0; case 1: case 2: case 3: $bytes = str_pad($bytes, 4, "\x00", STR_PAD_LEFT); break; case 4: break; default: throw new InvalidDatabaseException( "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" ); } list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes)); return $int; } private function decodeMap($size, $offset) { $map = []; for ($i = 0; $i < $size; ++$i) { list($key, $offset) = $this->decode($offset); list($value, $offset) = $this->decode($offset); $map[$key] = $value; } return [$map, $offset]; } private function decodePointer($ctrlByte, $offset) { $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; $buffer = Util::read($this->fileStream, $offset, $pointerSize); $offset = $offset + $pointerSize; switch ($pointerSize) { case 1: $packed = \chr($ctrlByte & 0x7) . $buffer; list(, $pointer) = unpack('n', $packed); $pointer += $this->pointerBase; break; case 2: $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer; list(, $pointer) = unpack('N', $packed); $pointer += $this->pointerBase + 2048; break; case 3: $packed = \chr($ctrlByte & 0x7) . $buffer; // It is safe to use 'N' here, even on 32 bit machines as the // first bit is 0. list(, $pointer) = unpack('N', $packed); $pointer += $this->pointerBase + 526336; break; case 4: // We cannot use unpack here as we might overflow on 32 bit // machines $pointerOffset = $this->decodeUint($buffer, $pointerSize); $byteLength = $pointerSize + $this->pointerBaseByteSize; if ($byteLength <= _MM_MAX_INT_BYTES) { $pointer = $pointerOffset + $this->pointerBase; } elseif (\extension_loaded('gmp')) { $pointer = gmp_strval(gmp_add($pointerOffset, $this->pointerBase)); } elseif (\extension_loaded('bcmath')) { $pointer = bcadd($pointerOffset, $this->pointerBase); } else { throw new RuntimeException( 'The gmp or bcmath extension must be installed to read this database.' ); } } return [$pointer, $offset]; } private function decodeUint($bytes, $byteLength) { if ($byteLength === 0) { return 0; } $integer = 0; for ($i = 0; $i < $byteLength; ++$i) { $part = \ord($bytes[$i]); // We only use gmp or bcmath if the final value is too big if ($byteLength <= _MM_MAX_INT_BYTES) { $integer = ($integer << 8) + $part; } elseif (\extension_loaded('gmp')) { $integer = gmp_strval(gmp_add(gmp_mul($integer, 256), $part)); } elseif (\extension_loaded('bcmath')) { $integer = bcadd(bcmul($integer, 256), $part); } else { throw new RuntimeException( 'The gmp or bcmath extension must be installed to read this database.' ); } } return $integer; } private function sizeFromCtrlByte($ctrlByte, $offset) { $size = $ctrlByte & 0x1f; if ($size < 29) { return [$size, $offset]; } $bytesToRead = $size - 28; $bytes = Util::read($this->fileStream, $offset, $bytesToRead); if ($size === 29) { $size = 29 + \ord($bytes); } elseif ($size === 30) { list(, $adjust) = unpack('n', $bytes); $size = 285 + $adjust; } elseif ($size > 30) { list(, $adjust) = unpack('N', "\x00" . $bytes); $size = $adjust + 65821; } return [$size, $offset + $bytesToRead]; } private function maybeSwitchByteOrder($bytes) { return $this->switchByteOrder ? strrev($bytes) : $bytes; } private function isPlatformLittleEndian() { $testint = 0x00FF; $packed = pack('S', $testint); return $testint === current(unpack('v', $packed)); } } vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php 0000644 00000025204 15132754523 0015721 0 ustar 00 <?php namespace MaxMind\Db; use BadMethodCallException; use Exception; use InvalidArgumentException; use MaxMind\Db\Reader\Decoder; use MaxMind\Db\Reader\InvalidDatabaseException; use MaxMind\Db\Reader\Metadata; use MaxMind\Db\Reader\Util; use UnexpectedValueException; /** * Instances of this class provide a reader for the MaxMind DB format. IP * addresses can be looked up using the get method. */ class Reader { private static $DATA_SECTION_SEPARATOR_SIZE = 16; private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com"; private static $METADATA_START_MARKER_LENGTH = 14; private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB private $decoder; private $fileHandle; private $fileSize; private $ipV4Start; private $metadata; /** * Constructs a Reader for the MaxMind DB format. The file passed to it must * be a valid MaxMind DB file such as a GeoIp2 database file. * * @param string $database * the MaxMind DB file to use * * @throws InvalidArgumentException for invalid database path or unknown arguments * @throws \MaxMind\Db\Reader\InvalidDatabaseException * if the database is invalid or there is an error reading * from it */ public function __construct($database) { if (\func_num_args() !== 1) { throw new InvalidArgumentException( 'The constructor takes exactly one argument.' ); } if (!is_readable($database)) { throw new InvalidArgumentException( "The file \"$database\" does not exist or is not readable." ); } $this->fileHandle = @fopen($database, 'rb'); if ($this->fileHandle === false) { throw new InvalidArgumentException( "Error opening \"$database\"." ); } $this->fileSize = @filesize($database); if ($this->fileSize === false) { throw new UnexpectedValueException( "Error determining the size of \"$database\"." ); } $start = $this->findMetadataStart($database); $metadataDecoder = new Decoder($this->fileHandle, $start); list($metadataArray) = $metadataDecoder->decode($start); $this->metadata = new Metadata($metadataArray); $this->decoder = new Decoder( $this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE ); $this->ipV4Start = $this->ipV4StartNode(); } /** * Retrieves the record for the IP address. * * @param string $ipAddress * the IP address to look up * * @throws BadMethodCallException if this method is called on a closed database * @throws InvalidArgumentException if something other than a single IP address is passed to the method * @throws InvalidDatabaseException * if the database is invalid or there is an error reading * from it * * @return mixed the record for the IP address */ public function get($ipAddress) { if (\func_num_args() !== 1) { throw new InvalidArgumentException( 'Method takes exactly one argument.' ); } list($record) = $this->getWithPrefixLen($ipAddress); return $record; } /** * Retrieves the record for the IP address and its associated network prefix length. * * @param string $ipAddress * the IP address to look up * * @throws BadMethodCallException if this method is called on a closed database * @throws InvalidArgumentException if something other than a single IP address is passed to the method * @throws InvalidDatabaseException * if the database is invalid or there is an error reading * from it * * @return array an array where the first element is the record and the * second the network prefix length for the record */ public function getWithPrefixLen($ipAddress) { if (\func_num_args() !== 1) { throw new InvalidArgumentException( 'Method takes exactly one argument.' ); } if (!\is_resource($this->fileHandle)) { throw new BadMethodCallException( 'Attempt to read from a closed MaxMind DB.' ); } if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) { throw new InvalidArgumentException( "The value \"$ipAddress\" is not a valid IP address." ); } list($pointer, $prefixLen) = $this->findAddressInTree($ipAddress); if ($pointer === 0) { return [null, $prefixLen]; } return [$this->resolveDataPointer($pointer), $prefixLen]; } private function findAddressInTree($ipAddress) { $rawAddress = unpack('C*', inet_pton($ipAddress)); $bitCount = \count($rawAddress) * 8; // The first node of the tree is always node 0, at the beginning of the // value $node = 0; $metadata = $this->metadata; // Check if we are looking up an IPv4 address in an IPv6 tree. If this // is the case, we can skip over the first 96 nodes. if ($metadata->ipVersion === 6) { if ($bitCount === 32) { $node = $this->ipV4Start; } } elseif ($metadata->ipVersion === 4 && $bitCount === 128) { throw new InvalidArgumentException( "Error looking up $ipAddress. You attempted to look up an" . ' IPv6 address in an IPv4-only database.' ); } $nodeCount = $metadata->nodeCount; for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) { $tempBit = 0xFF & $rawAddress[($i >> 3) + 1]; $bit = 1 & ($tempBit >> 7 - ($i % 8)); $node = $this->readNode($node, $bit); } if ($node === $nodeCount) { // Record is empty return [0, $i]; } elseif ($node > $nodeCount) { // Record is a data pointer return [$node, $i]; } throw new InvalidDatabaseException('Something bad happened'); } private function ipV4StartNode() { // If we have an IPv4 database, the start node is the first node if ($this->metadata->ipVersion === 4) { return 0; } $node = 0; for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) { $node = $this->readNode($node, 0); } return $node; } private function readNode($nodeNumber, $index) { $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; switch ($this->metadata->recordSize) { case 24: $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); list(, $node) = unpack('N', "\x00" . $bytes); return $node; case 28: $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4); if ($index === 0) { $middle = (0xF0 & \ord($bytes[3])) >> 4; } else { $middle = 0x0F & \ord($bytes[0]); } list(, $node) = unpack('N', \chr($middle) . substr($bytes, $index, 3)); return $node; case 32: $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); list(, $node) = unpack('N', $bytes); return $node; default: throw new InvalidDatabaseException( 'Unknown record size: ' . $this->metadata->recordSize ); } } private function resolveDataPointer($pointer) { $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize; if ($resolved >= $this->fileSize) { throw new InvalidDatabaseException( "The MaxMind DB file's search tree is corrupt" ); } list($data) = $this->decoder->decode($resolved); return $data; } /* * This is an extremely naive but reasonably readable implementation. There * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever * an issue, but I suspect it won't be. */ private function findMetadataStart($filename) { $handle = $this->fileHandle; $fstat = fstat($handle); $fileSize = $fstat['size']; $marker = self::$METADATA_START_MARKER; $markerLength = self::$METADATA_START_MARKER_LENGTH; $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize); for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) { if (fseek($handle, $offset) !== 0) { break; } $value = fread($handle, $markerLength); if ($value === $marker) { return $offset + $markerLength; } } throw new InvalidDatabaseException( "Error opening database file ($filename). " . 'Is this a valid MaxMind DB file?' ); } /** * @throws InvalidArgumentException if arguments are passed to the method * @throws BadMethodCallException if the database has been closed * * @return Metadata object for the database */ public function metadata() { if (\func_num_args()) { throw new InvalidArgumentException( 'Method takes no arguments.' ); } // Not technically required, but this makes it consistent with // C extension and it allows us to change our implementation later. if (!\is_resource($this->fileHandle)) { throw new BadMethodCallException( 'Attempt to read from a closed MaxMind DB.' ); } return $this->metadata; } /** * Closes the MaxMind DB and returns resources to the system. * * @throws Exception * if an I/O error occurs */ public function close() { if (!\is_resource($this->fileHandle)) { throw new BadMethodCallException( 'Attempt to close a closed MaxMind DB.' ); } fclose($this->fileHandle); } } vendor/maxmind-db/reader/ext/php_maxminddb.h 0000644 00000001535 15132754523 0015121 0 ustar 00 /* MaxMind, Inc., licenses this file to you under the Apache License, Version * 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #include <zend_interfaces.h> #ifndef PHP_MAXMINDDB_H #define PHP_MAXMINDDB_H 1 #define PHP_MAXMINDDB_VERSION "1.6.0" #define PHP_MAXMINDDB_EXTNAME "maxminddb" extern zend_module_entry maxminddb_module_entry; #define phpext_maxminddb_ptr &maxminddb_module_entry #endif vendor/maxmind-db/reader/ext/maxminddb.c 0000644 00000055714 15132754523 0014255 0 ustar 00 /* MaxMind, Inc., licenses this file to you under the Apache License, Version * 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #include "php_maxminddb.h" #ifdef HAVE_CONFIG_H #include "config.h" #endif #include <php.h> #include <zend.h> #include "Zend/zend_exceptions.h" #include "ext/standard/info.h" #include <maxminddb.h> #ifdef ZTS #include <TSRM.h> #endif #define __STDC_FORMAT_MACROS #include <inttypes.h> #define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db") #define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader") #define PHP_MAXMINDDB_READER_EX_NS \ ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException") #ifdef ZEND_ENGINE_3 #define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv)) #define _ZVAL_STRING ZVAL_STRING #define _ZVAL_STRINGL ZVAL_STRINGL typedef size_t strsize_t; typedef zend_object free_obj_t; #else #define Z_MAXMINDDB_P(zv) \ (maxminddb_obj *)zend_object_store_get_object(zv TSRMLS_CC) #define _ZVAL_STRING(a, b) ZVAL_STRING(a, b, 1) #define _ZVAL_STRINGL(a, b, c) ZVAL_STRINGL(a, b, c, 1) typedef int strsize_t; typedef void free_obj_t; #endif /* For PHP 8 compatibility */ #ifndef TSRMLS_C #define TSRMLS_C #endif #ifndef TSRMLS_CC #define TSRMLS_CC #endif #ifndef TSRMLS_DC #define TSRMLS_DC #endif #ifndef ZEND_ACC_CTOR #define ZEND_ACC_CTOR 0 #endif #ifdef ZEND_ENGINE_3 typedef struct _maxminddb_obj { MMDB_s *mmdb; zend_object std; } maxminddb_obj; #else typedef struct _maxminddb_obj { zend_object std; MMDB_s *mmdb; } maxminddb_obj; #endif PHP_FUNCTION(maxminddb); static int get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len); static const MMDB_entry_data_list_s * handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static const MMDB_entry_data_list_s * handle_array(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static const MMDB_entry_data_list_s * handle_map(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC); static zend_class_entry *lookup_class(const char *name TSRMLS_DC); #define CHECK_ALLOCATED(val) \ if (!val) { \ zend_error(E_ERROR, "Out of memory"); \ return; \ } #define THROW_EXCEPTION(name, ...) \ { \ zend_class_entry *exception_ce = lookup_class(name TSRMLS_CC); \ zend_throw_exception_ex(exception_ce, 0 TSRMLS_CC, __VA_ARGS__); \ } #if PHP_VERSION_ID < 50399 #define object_properties_init(zo, class_type) \ { \ zval *tmp; \ zend_hash_copy((*zo).properties, \ &class_type->default_properties, \ (copy_ctor_func_t)zval_add_ref, \ (void *)&tmp, \ sizeof(zval *)); \ } #endif static zend_object_handlers maxminddb_obj_handlers; static zend_class_entry *maxminddb_ce; static inline maxminddb_obj * php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) { #ifdef ZEND_ENGINE_3 return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std)); #else return (maxminddb_obj *)obj; #endif } ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_construct, 0, 0, 1) ZEND_ARG_INFO(0, db_file) ZEND_END_ARG_INFO() PHP_METHOD(MaxMind_Db_Reader, __construct) { char *db_file = NULL; strsize_t name_len; zval *_this_zval = NULL; if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os", &_this_zval, maxminddb_ce, &db_file, &name_len) == FAILURE) { THROW_EXCEPTION("InvalidArgumentException", "The constructor takes exactly one argument."); return; } if (0 != php_check_open_basedir(db_file TSRMLS_CC) || 0 != access(db_file, R_OK)) { THROW_EXCEPTION("InvalidArgumentException", "The file \"%s\" does not exist or is not readable.", db_file); return; } MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s)); uint16_t status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb); if (MMDB_SUCCESS != status) { THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, "Error opening database file (%s). Is this a valid " "MaxMind DB file?", db_file); efree(mmdb); return; } maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(getThis()); mmdb_obj->mmdb = mmdb; } ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_get, 0, 0, 1) ZEND_ARG_INFO(0, ip_address) ZEND_END_ARG_INFO() PHP_METHOD(MaxMind_Db_Reader, get) { int prefix_len = 0; get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len); } PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) { zval *record, *z_prefix_len; #ifdef ZEND_ENGINE_3 zval _record, _z_prefix_len; record = &_record; z_prefix_len = &_z_prefix_len; #else ALLOC_INIT_ZVAL(record); ALLOC_INIT_ZVAL(z_prefix_len); #endif int prefix_len = 0; if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, record, &prefix_len)) { return; } array_init(return_value); add_next_index_zval(return_value, record); ZVAL_LONG(z_prefix_len, prefix_len); add_next_index_zval(return_value, z_prefix_len); } static int get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) { char *ip_address = NULL; strsize_t name_len; zval *_this_zval = NULL; if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os", &_this_zval, maxminddb_ce, &ip_address, &name_len) == FAILURE) { THROW_EXCEPTION("InvalidArgumentException", "Method takes exactly one argument."); return 1; } const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); MMDB_s *mmdb = mmdb_obj->mmdb; if (NULL == mmdb) { THROW_EXCEPTION("BadMethodCallException", "Attempt to read from a closed MaxMind DB."); return 1; } struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_flags = AI_NUMERICHOST, // We set ai_socktype so that we only get one result back .ai_socktype = SOCK_STREAM}; struct addrinfo *addresses = NULL; int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses); if (gai_status) { THROW_EXCEPTION("InvalidArgumentException", "The value \"%s\" is not a valid IP address.", ip_address); return 1; } if (!addresses || !addresses->ai_addr) { THROW_EXCEPTION( "InvalidArgumentException", "getaddrinfo was successful but failed to set the addrinfo"); return 1; } int sa_family = addresses->ai_addr->sa_family; int mmdb_error = MMDB_SUCCESS; MMDB_lookup_result_s result = MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error); freeaddrinfo(addresses); if (MMDB_SUCCESS != mmdb_error) { char *exception_name; if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) { exception_name = "InvalidArgumentException"; } else { exception_name = PHP_MAXMINDDB_READER_EX_NS; } THROW_EXCEPTION(exception_name, "Error looking up %s. %s", ip_address, MMDB_strerror(mmdb_error)); return 1; } *prefix_len = result.netmask; if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) { // We return the prefix length given the IPv4 address. If there is // no IPv4 subtree, we return a prefix length of 0. *prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0; } if (!result.found_entry) { ZVAL_NULL(record); return 0; } MMDB_entry_data_list_s *entry_data_list = NULL; int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); if (MMDB_SUCCESS != status) { THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, "Error while looking up data for %s. %s", ip_address, MMDB_strerror(status)); MMDB_free_entry_data_list(entry_data_list); return 1; } else if (NULL == entry_data_list) { THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, "Error while looking up data for %s. Your database may " "be corrupt or you have found a bug in libmaxminddb.", ip_address); return 1; } handle_entry_data_list(entry_data_list, record TSRMLS_CC); MMDB_free_entry_data_list(entry_data_list); return 0; } ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_void, 0, 0, 0) ZEND_END_ARG_INFO() PHP_METHOD(MaxMind_Db_Reader, metadata) { if (ZEND_NUM_ARGS() != 0) { THROW_EXCEPTION("InvalidArgumentException", "Method takes no arguments."); return; } const maxminddb_obj *const mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); if (NULL == mmdb_obj->mmdb) { THROW_EXCEPTION("BadMethodCallException", "Attempt to read from a closed MaxMind DB."); return; } const char *const name = ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata"); zend_class_entry *metadata_ce = lookup_class(name TSRMLS_CC); object_init_ex(return_value, metadata_ce); #ifdef ZEND_ENGINE_3 zval _metadata_array; zval *metadata_array = &_metadata_array; ZVAL_NULL(metadata_array); #else zval *metadata_array; ALLOC_INIT_ZVAL(metadata_array); #endif MMDB_entry_data_list_s *entry_data_list; MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list); handle_entry_data_list(entry_data_list, metadata_array TSRMLS_CC); MMDB_free_entry_data_list(entry_data_list); #if PHP_VERSION_ID >= 80000 zend_call_method_with_1_params(Z_OBJ_P(return_value), metadata_ce, &metadata_ce->constructor, ZEND_CONSTRUCTOR_FUNC_NAME, NULL, metadata_array); zval_ptr_dtor(metadata_array); #elif defined(ZEND_ENGINE_3) zend_call_method_with_1_params(return_value, metadata_ce, &metadata_ce->constructor, ZEND_CONSTRUCTOR_FUNC_NAME, NULL, metadata_array); zval_ptr_dtor(metadata_array); #else zend_call_method_with_1_params(&return_value, metadata_ce, &metadata_ce->constructor, ZEND_CONSTRUCTOR_FUNC_NAME, NULL, metadata_array); zval_ptr_dtor(&metadata_array); #endif } PHP_METHOD(MaxMind_Db_Reader, close) { if (ZEND_NUM_ARGS() != 0) { THROW_EXCEPTION("InvalidArgumentException", "Method takes no arguments."); return; } maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); if (NULL == mmdb_obj->mmdb) { THROW_EXCEPTION("BadMethodCallException", "Attempt to close a closed MaxMind DB."); return; } MMDB_close(mmdb_obj->mmdb); efree(mmdb_obj->mmdb); mmdb_obj->mmdb = NULL; } static const MMDB_entry_data_list_s * handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { switch (entry_data_list->entry_data.type) { case MMDB_DATA_TYPE_MAP: return handle_map(entry_data_list, z_value TSRMLS_CC); case MMDB_DATA_TYPE_ARRAY: return handle_array(entry_data_list, z_value TSRMLS_CC); case MMDB_DATA_TYPE_UTF8_STRING: _ZVAL_STRINGL(z_value, (char *)entry_data_list->entry_data.utf8_string, entry_data_list->entry_data.data_size); break; case MMDB_DATA_TYPE_BYTES: _ZVAL_STRINGL(z_value, (char *)entry_data_list->entry_data.bytes, entry_data_list->entry_data.data_size); break; case MMDB_DATA_TYPE_DOUBLE: ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value); break; case MMDB_DATA_TYPE_FLOAT: ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value); break; case MMDB_DATA_TYPE_UINT16: ZVAL_LONG(z_value, entry_data_list->entry_data.uint16); break; case MMDB_DATA_TYPE_UINT32: handle_uint32(entry_data_list, z_value TSRMLS_CC); break; case MMDB_DATA_TYPE_BOOLEAN: ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean); break; case MMDB_DATA_TYPE_UINT64: handle_uint64(entry_data_list, z_value TSRMLS_CC); break; case MMDB_DATA_TYPE_UINT128: handle_uint128(entry_data_list, z_value TSRMLS_CC); break; case MMDB_DATA_TYPE_INT32: ZVAL_LONG(z_value, entry_data_list->entry_data.int32); break; default: THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, "Invalid data type arguments: %d", entry_data_list->entry_data.type); return NULL; } return entry_data_list; } static const MMDB_entry_data_list_s * handle_map(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { array_init(z_value); const uint32_t map_size = entry_data_list->entry_data.data_size; uint i; for (i = 0; i < map_size && entry_data_list; i++) { entry_data_list = entry_data_list->next; char *key = estrndup((char *)entry_data_list->entry_data.utf8_string, entry_data_list->entry_data.data_size); if (NULL == key) { THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, "Invalid data type arguments"); return NULL; } entry_data_list = entry_data_list->next; #ifdef ZEND_ENGINE_3 zval _new_value; zval *new_value = &_new_value; ZVAL_NULL(new_value); #else zval *new_value; ALLOC_INIT_ZVAL(new_value); #endif entry_data_list = handle_entry_data_list(entry_data_list, new_value TSRMLS_CC); add_assoc_zval(z_value, key, new_value); efree(key); } return entry_data_list; } static const MMDB_entry_data_list_s * handle_array(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { const uint32_t size = entry_data_list->entry_data.data_size; array_init(z_value); uint i; for (i = 0; i < size && entry_data_list; i++) { entry_data_list = entry_data_list->next; #ifdef ZEND_ENGINE_3 zval _new_value; zval *new_value = &_new_value; ZVAL_NULL(new_value); #else zval *new_value; ALLOC_INIT_ZVAL(new_value); #endif entry_data_list = handle_entry_data_list(entry_data_list, new_value TSRMLS_CC); add_next_index_zval(z_value, new_value); } return entry_data_list; } static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { uint64_t high = 0; uint64_t low = 0; #if MMDB_UINT128_IS_BYTE_ARRAY int i; for (i = 0; i < 8; i++) { high = (high << 8) | entry_data_list->entry_data.uint128[i]; } for (i = 8; i < 16; i++) { low = (low << 8) | entry_data_list->entry_data.uint128[i]; } #else high = entry_data_list->entry_data.uint128 >> 64; low = (uint64_t)entry_data_list->entry_data.uint128; #endif char *num_str; spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low); CHECK_ALLOCATED(num_str); _ZVAL_STRING(z_value, num_str); efree(num_str); } static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { uint32_t val = entry_data_list->entry_data.uint32; #if LONG_MAX >= UINT32_MAX ZVAL_LONG(z_value, val); return; #else if (val <= LONG_MAX) { ZVAL_LONG(z_value, val); return; } char *int_str; spprintf(&int_str, 0, "%" PRIu32, val); CHECK_ALLOCATED(int_str); _ZVAL_STRING(z_value, int_str); efree(int_str); #endif } static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, zval *z_value TSRMLS_DC) { uint64_t val = entry_data_list->entry_data.uint64; #if LONG_MAX >= UINT64_MAX ZVAL_LONG(z_value, val); return; #else if (val <= LONG_MAX) { ZVAL_LONG(z_value, val); return; } char *int_str; spprintf(&int_str, 0, "%" PRIu64, val); CHECK_ALLOCATED(int_str); _ZVAL_STRING(z_value, int_str); efree(int_str); #endif } static zend_class_entry *lookup_class(const char *name TSRMLS_DC) { #ifdef ZEND_ENGINE_3 zend_string *n = zend_string_init(name, strlen(name), 0); zend_class_entry *ce = zend_lookup_class(n); zend_string_release(n); if (NULL == ce) { zend_error(E_ERROR, "Class %s not found", name); } return ce; #else zend_class_entry **ce; if (FAILURE == zend_lookup_class(name, strlen(name), &ce TSRMLS_CC)) { zend_error(E_ERROR, "Class %s not found", name); } return *ce; #endif } static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) { maxminddb_obj *obj = php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC); if (obj->mmdb != NULL) { MMDB_close(obj->mmdb); efree(obj->mmdb); } zend_object_std_dtor(&obj->std TSRMLS_CC); #ifndef ZEND_ENGINE_3 efree(object); #endif } #ifdef ZEND_ENGINE_3 static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) { maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj)); zend_object_std_init(&obj->std, type TSRMLS_CC); object_properties_init(&(obj->std), type); obj->std.handlers = &maxminddb_obj_handlers; return &obj->std; } #else static zend_object_value maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) { zend_object_value retval; maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj)); zend_object_std_init(&obj->std, type TSRMLS_CC); object_properties_init(&(obj->std), type); retval.handle = zend_objects_store_put( obj, NULL, maxminddb_free_storage, NULL TSRMLS_CC); retval.handlers = &maxminddb_obj_handlers; return retval; } #endif // clang-format off static zend_function_entry maxminddb_methods[] = { PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxmindbreader_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) PHP_ME(MaxMind_Db_Reader, close, arginfo_maxmindbreader_void, ZEND_ACC_PUBLIC) PHP_ME(MaxMind_Db_Reader, get, arginfo_maxmindbreader_get, ZEND_ACC_PUBLIC) PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxmindbreader_get, ZEND_ACC_PUBLIC) PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxmindbreader_void, ZEND_ACC_PUBLIC) { NULL, NULL, NULL } }; // clang-format on PHP_MINIT_FUNCTION(maxminddb) { zend_class_entry ce; INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods); maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC); maxminddb_ce->create_object = maxminddb_create_handler; memcpy(&maxminddb_obj_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); maxminddb_obj_handlers.clone_obj = NULL; #ifdef ZEND_ENGINE_3 maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std); maxminddb_obj_handlers.free_obj = maxminddb_free_storage; #endif zend_declare_class_constant_string(maxminddb_ce, "MMDB_LIB_VERSION", sizeof("MMDB_LIB_VERSION") - 1, MMDB_lib_version() TSRMLS_CC); return SUCCESS; } static PHP_MINFO_FUNCTION(maxminddb) { php_info_print_table_start(); php_info_print_table_row(2, "MaxMind DB Reader", "enabled"); php_info_print_table_row( 2, "maxminddb extension version", PHP_MAXMINDDB_VERSION); php_info_print_table_row( 2, "libmaxminddb library version", MMDB_lib_version()); php_info_print_table_end(); } zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER, PHP_MAXMINDDB_EXTNAME, NULL, PHP_MINIT(maxminddb), NULL, NULL, NULL, PHP_MINFO(maxminddb), PHP_MAXMINDDB_VERSION, STANDARD_MODULE_PROPERTIES}; #ifdef COMPILE_DL_MAXMINDDB ZEND_GET_MODULE(maxminddb) #endif vendor/maxmind-db/reader/ext/config.m4 0000644 00000003101 15132754523 0013634 0 ustar 00 PHP_ARG_WITH(maxminddb, [Whether to enable the MaxMind DB Reader extension], [ --with-maxminddb Enable MaxMind DB Reader extension support]) PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support, [ --enable-maxminddb-debug Enable enable MaxMind DB deubg support], no, no) if test $PHP_MAXMINDDB != "no"; then AC_PATH_PROG(PKG_CONFIG, pkg-config, no) AC_MSG_CHECKING(for libmaxminddb) if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then dnl retrieve build options from pkg-config if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags` LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs` LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion` AC_MSG_RESULT(found version $LIBMAXMINDDB_VER) else AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0) fi PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD) PHP_EVAL_INCLINE($LIBMAXMINDDB_INC) else AC_MSG_RESULT(pkg-config information missing) AC_MSG_WARN(will use libmaxmxinddb from compiler default path) PHP_CHECK_LIBRARY(maxminddb, MMDB_open) PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD) fi if test $PHP_MAXMINDDB_DEBUG != "no"; then CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror" fi PHP_SUBST(MAXMINDDB_SHARED_LIBADD) PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared) fi vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt 0000644 00000000342 15132754523 0017040 0 ustar 00 --TEST-- openbase_dir is followed --INI-- open_basedir=/--dne-- --FILE-- <?php use MaxMind\Db\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb'); ?> --EXPECTREGEX-- .*open_basedir restriction in effect.* vendor/maxmind-db/reader/ext/tests/002-final.phpt 0000644 00000000411 15132754523 0015555 0 ustar 00 --TEST-- Check that Reader class is not final --SKIPIF-- <?php if (!extension_loaded('maxminddb')) { echo 'skip'; } ?> --FILE-- <?php $reflectionClass = new \ReflectionClass('MaxMind\Db\Reader'); var_dump($reflectionClass->isFinal()); ?> --EXPECT-- bool(false) vendor/maxmind-db/reader/ext/tests/001-load.phpt 0000644 00000000332 15132754523 0015404 0 ustar 00 --TEST-- Check for maxminddb presence --SKIPIF-- <?php if (!extension_loaded('maxminddb')) { echo 'skip'; } ?> --FILE-- <?php echo 'maxminddb extension is available'; ?> --EXPECT-- maxminddb extension is available vendor/composer/autoload_static.php 0000644 00000406424 15132754523 0013601 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInit7fa27687a59114a5aec1ac3080434897 { public static $prefixLengthsPsr4 = array ( 'S' => array ( 'Symfony\\Component\\CssSelector\\' => 30, ), 'P' => array ( 'Psr\\Container\\' => 14, 'Pelago\\' => 7, ), 'M' => array ( 'MaxMind\\Db\\' => 11, ), 'C' => array ( 'Composer\\Installers\\' => 20, ), 'A' => array ( 'Automattic\\WooCommerce\\Vendor\\' => 30, 'Automattic\\WooCommerce\\Tests\\' => 29, 'Automattic\\WooCommerce\\Testing\\Tools\\' => 37, 'Automattic\\WooCommerce\\Blocks\\' => 30, 'Automattic\\WooCommerce\\Admin\\' => 29, 'Automattic\\WooCommerce\\' => 23, 'Automattic\\Jetpack\\Autoloader\\' => 30, ), ); public static $prefixDirsPsr4 = array ( 'Symfony\\Component\\CssSelector\\' => array ( 0 => __DIR__ . '/..' . '/symfony/css-selector', ), 'Psr\\Container\\' => array ( 0 => __DIR__ . '/..' . '/psr/container/src', ), 'Pelago\\' => array ( 0 => __DIR__ . '/..' . '/pelago/emogrifier/src', ), 'MaxMind\\Db\\' => array ( 0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db', ), 'Composer\\Installers\\' => array ( 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers', ), 'Automattic\\WooCommerce\\Vendor\\' => array ( 0 => __DIR__ . '/../..' . '/lib/packages', ), 'Automattic\\WooCommerce\\Tests\\' => array ( 0 => __DIR__ . '/../..' . '/tests/php/src', ), 'Automattic\\WooCommerce\\Testing\\Tools\\' => array ( 0 => __DIR__ . '/../..' . '/tests/Tools', ), 'Automattic\\WooCommerce\\Blocks\\' => array ( 0 => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src', ), 'Automattic\\WooCommerce\\Admin\\' => array ( 0 => __DIR__ . '/../..' . '/packages/woocommerce-admin/src', ), 'Automattic\\WooCommerce\\' => array ( 0 => __DIR__ . '/../..' . '/src', ), 'Automattic\\Jetpack\\Autoloader\\' => array ( 0 => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src', ), ); public static $prefixesPsr0 = array ( 'A' => array ( 'Automattic\\WooCommerce\\Vendor\\' => array ( 0 => __DIR__ . '/../..' . '/lib/packages', ), ), ); public static $classMap = array ( 'Automattic\\Jetpack\\Autoloader\\AutoloadFileWriter' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php', 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php', 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php', 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/ManifestGenerator.php', 'Automattic\\Jetpack\\Constants' => __DIR__ . '/..' . '/automattic/jetpack-constants/src/class-constants.php', 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Coupons.php', 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php', 'Automattic\\WooCommerce\\Admin\\API\\Customers' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Customers.php', 'Automattic\\WooCommerce\\Admin\\API\\Data' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Data.php', 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/DataCountries.php', 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php', 'Automattic\\WooCommerce\\Admin\\API\\Features' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Features.php', 'Automattic\\WooCommerce\\Admin\\API\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Init.php', 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Leaderboards.php', 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Marketing.php', 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/MarketingOverview.php', 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/NavigationFavorites.php', 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/NoteActions.php', 'Automattic\\WooCommerce\\Admin\\API\\Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Notes.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingPayments.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingProfile.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingTasks.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingThemes.php', 'Automattic\\WooCommerce\\Admin\\API\\Options' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Options.php', 'Automattic\\WooCommerce\\Admin\\API\\Orders' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Orders.php', 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Plugins.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductAttributes.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductCategories.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductReviews.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductVariations.php', 'Automattic\\WooCommerce\\Admin\\API\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Products.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Cache.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/SettingOptions.php', 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Taxes.php', 'Automattic\\WooCommerce\\Admin\\API\\Themes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Themes.php', 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/CategoryLookup.php', 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Composer/Package.php', 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php', 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php', 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php', 'Automattic\\WooCommerce\\Admin\\Events' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Events.php', 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/FeaturePlugin.php', 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ActivityPanels.php', 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Analytics.php', 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Coupons.php', 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php', 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php', 'Automattic\\WooCommerce\\Admin\\Features\\Features' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Features.php', 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Homescreen.php', 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php', 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Onboarding.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php', 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Settings.php', 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php', 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php', 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/TransientNotices.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php', 'Automattic\\WooCommerce\\Admin\\Install' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Install.php', 'Automattic\\WooCommerce\\Admin\\Loader' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Loader.php', 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php', 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DataStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DrawAttention.php', 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php', 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FirstProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php', 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php', 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php', 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MobileApp.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Note.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NoteTraits.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Notes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php', 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php', 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php', 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php', 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/TestCheckout.php', 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php', 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/Order.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php', 'Automattic\\WooCommerce\\Admin\\PageController' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PageController.php', 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PaymentPlugins.php', 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsHelper.php', 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsInstaller.php', 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php', 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportCSVEmail.php', 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportCSVExporter.php', 'Automattic\\WooCommerce\\Admin\\ReportExporter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportExporter.php', 'Automattic\\WooCommerce\\Admin\\ReportsSync' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportsSync.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php', 'Automattic\\WooCommerce\\Admin\\Survey' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Survey.php', 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/WCAdminHelper.php', 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php', 'Automattic\\WooCommerce\\Autoloader' => __DIR__ . '/../..' . '/src/Autoloader.php', 'Automattic\\WooCommerce\\Blocks\\Assets' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets.php', 'Automattic\\WooCommerce\\Blocks\\AssetsController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/AssetsController.php', 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets/Api.php', 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypesController.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Package.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php', 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/InboxNotifications.php', 'Automattic\\WooCommerce\\Blocks\\Installer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Installer.php', 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php', 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php', 'Automattic\\WooCommerce\\Blocks\\Library' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Library.php', 'Automattic\\WooCommerce\\Blocks\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Package.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Api.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/Container.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/FactoryType.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/SharedType.php', 'Automattic\\WooCommerce\\Blocks\\RestApi' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/RestApi.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php', 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php', 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php', 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => __DIR__ . '/../..' . '/src/Checkout/Helpers/ReserveStock.php', 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => __DIR__ . '/../..' . '/src/Checkout/Helpers/ReserveStockException.php', 'Automattic\\WooCommerce\\Container' => __DIR__ . '/../..' . '/src/Container.php', 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => __DIR__ . '/../..' . '/src/Internal/AssignDefaultCategory.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/AbstractServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ContainerException.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/Definition.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ExtendedContainer.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => __DIR__ . '/../..' . '/src/Internal/DownloadPermissionsAdjuster.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/DataRegenerator.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/Filterer.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/LookupDataStore.php', 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => __DIR__ . '/../..' . '/src/Internal/RestApiUtil.php', 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => __DIR__ . '/../..' . '/src/Internal/RestockRefundedItemsAdjuster.php', 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => __DIR__ . '/../..' . '/src/Internal/WCCom/ConnectionHelper.php', 'Automattic\\WooCommerce\\Packages' => __DIR__ . '/../..' . '/src/Packages.php', 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => __DIR__ . '/../..' . '/src/Proxies/ActionsProxy.php', 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => __DIR__ . '/../..' . '/src/Proxies/LegacyProxy.php', 'Automattic\\WooCommerce\\RestApi\\Package' => __DIR__ . '/../..' . '/includes/rest-api/Package.php', 'Automattic\\WooCommerce\\RestApi\\Server' => __DIR__ . '/../..' . '/includes/rest-api/Server.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php', 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => __DIR__ . '/../..' . '/includes/rest-api/Utilities/ImageAttachment.php', 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => __DIR__ . '/../..' . '/includes/rest-api/Utilities/SingletonTrait.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/CodeHacker.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/CodeHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => __DIR__ . '/../..' . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php', 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => __DIR__ . '/../..' . '/tests/Tools/FakeQueue.php', 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/AssignDefaultCategoryTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/RestApiUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => __DIR__ . '/../..' . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/LegacyProxyTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/MockableLegacyProxyTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/ArrayUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/NumberUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/StringUtilTest.php', 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => __DIR__ . '/../..' . '/src/Utilities/ArrayUtil.php', 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => __DIR__ . '/../..' . '/src/Utilities/NumberUtil.php', 'Automattic\\WooCommerce\\Utilities\\StringUtil' => __DIR__ . '/../..' . '/src/Utilities/StringUtil.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassName.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassNameInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/RawArgument.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/RawArgumentInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => __DIR__ . '/../..' . '/lib/packages/League/Container/Container.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ContainerAwareInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => __DIR__ . '/../..' . '/lib/packages/League/Container/ContainerAwareTrait.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/Definition.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => __DIR__ . '/../..' . '/lib/packages/League/Container/Exception/ContainerException.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => __DIR__ . '/../..' . '/lib/packages/League/Container/Exception/NotFoundException.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/Inflector.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => __DIR__ . '/../..' . '/lib/packages/League/Container/ReflectionContainer.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php', 'Composer\\Installers\\AglInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AglInstaller.php', 'Composer\\Installers\\AimeosInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AimeosInstaller.php', 'Composer\\Installers\\AnnotateCmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', 'Composer\\Installers\\AsgardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', 'Composer\\Installers\\AttogramInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', 'Composer\\Installers\\BaseInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BaseInstaller.php', 'Composer\\Installers\\BitrixInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', 'Composer\\Installers\\BonefishInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', 'Composer\\Installers\\CakePHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', 'Composer\\Installers\\ChefInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ChefInstaller.php', 'Composer\\Installers\\CiviCrmInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', 'Composer\\Installers\\ClanCatsFrameworkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', 'Composer\\Installers\\CockpitInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', 'Composer\\Installers\\CodeIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', 'Composer\\Installers\\Concrete5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', 'Composer\\Installers\\CraftInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CraftInstaller.php', 'Composer\\Installers\\CroogoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', 'Composer\\Installers\\DecibelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', 'Composer\\Installers\\DframeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DframeInstaller.php', 'Composer\\Installers\\DokuWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', 'Composer\\Installers\\DolibarrInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', 'Composer\\Installers\\DrupalInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', 'Composer\\Installers\\ElggInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ElggInstaller.php', 'Composer\\Installers\\EliasisInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', 'Composer\\Installers\\ExpressionEngineInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', 'Composer\\Installers\\EzPlatformInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', 'Composer\\Installers\\FuelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelInstaller.php', 'Composer\\Installers\\FuelphpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', 'Composer\\Installers\\GravInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/GravInstaller.php', 'Composer\\Installers\\HuradInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/HuradInstaller.php', 'Composer\\Installers\\ImageCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', 'Composer\\Installers\\Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Installer.php', 'Composer\\Installers\\ItopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ItopInstaller.php', 'Composer\\Installers\\JoomlaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php', 'Composer\\Installers\\KanboardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', 'Composer\\Installers\\KirbyInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KirbyInstaller.php', 'Composer\\Installers\\KnownInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KnownInstaller.php', 'Composer\\Installers\\KodiCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', 'Composer\\Installers\\KohanaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', 'Composer\\Installers\\LanManagementSystemInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', 'Composer\\Installers\\LaravelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', 'Composer\\Installers\\LavaLiteInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', 'Composer\\Installers\\LithiumInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', 'Composer\\Installers\\MODULEWorkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', 'Composer\\Installers\\MODXEvoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', 'Composer\\Installers\\MagentoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', 'Composer\\Installers\\MajimaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', 'Composer\\Installers\\MakoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MakoInstaller.php', 'Composer\\Installers\\MantisBTInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', 'Composer\\Installers\\MauticInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MauticInstaller.php', 'Composer\\Installers\\MayaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MayaInstaller.php', 'Composer\\Installers\\MediaWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', 'Composer\\Installers\\MiaoxingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', 'Composer\\Installers\\MicroweberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', 'Composer\\Installers\\ModxInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ModxInstaller.php', 'Composer\\Installers\\MoodleInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', 'Composer\\Installers\\OctoberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', 'Composer\\Installers\\OntoWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', 'Composer\\Installers\\OsclassInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', 'Composer\\Installers\\OxidInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OxidInstaller.php', 'Composer\\Installers\\PPIInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PPIInstaller.php', 'Composer\\Installers\\PantheonInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', 'Composer\\Installers\\PhiftyInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', 'Composer\\Installers\\PhpBBInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', 'Composer\\Installers\\PimcoreInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php', 'Composer\\Installers\\PiwikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', 'Composer\\Installers\\PlentymarketsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', 'Composer\\Installers\\Plugin' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Plugin.php', 'Composer\\Installers\\PortoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PortoInstaller.php', 'Composer\\Installers\\PrestashopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', 'Composer\\Installers\\ProcessWireInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', 'Composer\\Installers\\PuppetInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', 'Composer\\Installers\\PxcmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', 'Composer\\Installers\\RadPHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', 'Composer\\Installers\\ReIndexInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', 'Composer\\Installers\\Redaxo5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', 'Composer\\Installers\\RedaxoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', 'Composer\\Installers\\RoundcubeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', 'Composer\\Installers\\SMFInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SMFInstaller.php', 'Composer\\Installers\\ShopwareInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', 'Composer\\Installers\\SilverStripeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', 'Composer\\Installers\\SiteDirectInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', 'Composer\\Installers\\StarbugInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', 'Composer\\Installers\\SyDESInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', 'Composer\\Installers\\SyliusInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', 'Composer\\Installers\\Symfony1Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Symfony1Installer.php', 'Composer\\Installers\\TYPO3CmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php', 'Composer\\Installers\\TYPO3FlowInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php', 'Composer\\Installers\\TaoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TaoInstaller.php', 'Composer\\Installers\\TastyIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', 'Composer\\Installers\\TheliaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', 'Composer\\Installers\\TuskInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TuskInstaller.php', 'Composer\\Installers\\UserFrostingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', 'Composer\\Installers\\VanillaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', 'Composer\\Installers\\VgmcpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', 'Composer\\Installers\\WHMCSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', 'Composer\\Installers\\WinterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WinterInstaller.php', 'Composer\\Installers\\WolfCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', 'Composer\\Installers\\WordPressInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', 'Composer\\Installers\\YawikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/YawikInstaller.php', 'Composer\\Installers\\ZendInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZendInstaller.php', 'Composer\\Installers\\ZikulaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', 'MaxMind\\Db\\Reader' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader.php', 'MaxMind\\Db\\Reader\\Decoder' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php', 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php', 'MaxMind\\Db\\Reader\\Metadata' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php', 'MaxMind\\Db\\Reader\\Util' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php', 'Pelago\\Emogrifier' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier.php', 'Pelago\\Emogrifier\\CssInliner' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/CssInliner.php', 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php', 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php', 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php', 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php', 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php', 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php', 'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php', 'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php', 'Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/NotFoundExceptionInterface.php', 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => __DIR__ . '/..' . '/symfony/css-selector/CssSelectorConverter.php', 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExceptionInterface.php', 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExpressionErrorException.php', 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/InternalErrorException.php', 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ParseException.php', 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/SyntaxErrorException.php', 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AbstractNode.php', 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AttributeNode.php', 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ClassNode.php', 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/CombinedSelectorNode.php', 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ElementNode.php', 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/FunctionNode.php', 'Symfony\\Component\\CssSelector\\Node\\HashNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/HashNode.php', 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/NegationNode.php', 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => __DIR__ . '/..' . '/symfony/css-selector/Node/NodeInterface.php', 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/PseudoNode.php', 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/SelectorNode.php', 'Symfony\\Component\\CssSelector\\Node\\Specificity' => __DIR__ . '/..' . '/symfony/css-selector/Node/Specificity.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/CommentHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HashHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/NumberHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/StringHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Parser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Parser.php', 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/ParserInterface.php', 'Symfony\\Component\\CssSelector\\Parser\\Reader' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Reader.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/HashParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Token' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Token.php', 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => __DIR__ . '/..' . '/symfony/css-selector/Parser/TokenStream.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/NodeExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Translator' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Translator.php', 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/TranslatorInterface.php', 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => __DIR__ . '/..' . '/symfony/css-selector/XPath/XPathExpr.php', 'WC_REST_CRUD_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php', 'WC_REST_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php', 'WC_REST_Coupons_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php', 'WC_REST_Coupons_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php', 'WC_REST_Coupons_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php', 'WC_REST_Customer_Downloads_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php', 'WC_REST_Customer_Downloads_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php', 'WC_REST_Customer_Downloads_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php', 'WC_REST_Customers_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php', 'WC_REST_Customers_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php', 'WC_REST_Customers_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php', 'WC_REST_Data_Continents_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php', 'WC_REST_Data_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php', 'WC_REST_Data_Countries_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php', 'WC_REST_Data_Currencies_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php', 'WC_REST_Network_Orders_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php', 'WC_REST_Network_Orders_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php', 'WC_REST_Order_Notes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php', 'WC_REST_Order_Notes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php', 'WC_REST_Order_Notes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php', 'WC_REST_Order_Refunds_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php', 'WC_REST_Order_Refunds_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php', 'WC_REST_Order_Refunds_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php', 'WC_REST_Orders_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php', 'WC_REST_Orders_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php', 'WC_REST_Orders_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php', 'WC_REST_Payment_Gateways_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php', 'WC_REST_Payment_Gateways_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php', 'WC_REST_Posts_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php', 'WC_REST_Product_Attribute_Terms_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php', 'WC_REST_Product_Attribute_Terms_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php', 'WC_REST_Product_Attribute_Terms_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php', 'WC_REST_Product_Attributes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php', 'WC_REST_Product_Attributes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php', 'WC_REST_Product_Attributes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php', 'WC_REST_Product_Categories_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php', 'WC_REST_Product_Categories_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php', 'WC_REST_Product_Categories_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php', 'WC_REST_Product_Reviews_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php', 'WC_REST_Product_Reviews_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php', 'WC_REST_Product_Reviews_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php', 'WC_REST_Product_Shipping_Classes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php', 'WC_REST_Product_Shipping_Classes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php', 'WC_REST_Product_Shipping_Classes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php', 'WC_REST_Product_Tags_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php', 'WC_REST_Product_Tags_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php', 'WC_REST_Product_Tags_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php', 'WC_REST_Product_Variations_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php', 'WC_REST_Product_Variations_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php', 'WC_REST_Products_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php', 'WC_REST_Products_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php', 'WC_REST_Products_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php', 'WC_REST_Report_Coupons_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php', 'WC_REST_Report_Customers_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php', 'WC_REST_Report_Orders_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php', 'WC_REST_Report_Products_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php', 'WC_REST_Report_Reviews_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php', 'WC_REST_Report_Sales_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php', 'WC_REST_Report_Sales_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php', 'WC_REST_Report_Sales_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php', 'WC_REST_Report_Top_Sellers_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php', 'WC_REST_Report_Top_Sellers_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php', 'WC_REST_Report_Top_Sellers_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php', 'WC_REST_Reports_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php', 'WC_REST_Reports_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php', 'WC_REST_Reports_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php', 'WC_REST_Setting_Options_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php', 'WC_REST_Setting_Options_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php', 'WC_REST_Settings_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php', 'WC_REST_Settings_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php', 'WC_REST_Shipping_Methods_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php', 'WC_REST_Shipping_Methods_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php', 'WC_REST_Shipping_Zone_Locations_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php', 'WC_REST_Shipping_Zone_Locations_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php', 'WC_REST_Shipping_Zone_Methods_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php', 'WC_REST_Shipping_Zone_Methods_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php', 'WC_REST_Shipping_Zones_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php', 'WC_REST_Shipping_Zones_Controller_Base' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php', 'WC_REST_Shipping_Zones_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php', 'WC_REST_System_Status_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php', 'WC_REST_System_Status_Tools_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php', 'WC_REST_System_Status_Tools_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php', 'WC_REST_System_Status_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php', 'WC_REST_Tax_Classes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php', 'WC_REST_Tax_Classes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php', 'WC_REST_Tax_Classes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php', 'WC_REST_Taxes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php', 'WC_REST_Taxes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php', 'WC_REST_Taxes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php', 'WC_REST_Telemetry_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php', 'WC_REST_Terms_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php', 'WC_REST_Webhook_Deliveries_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php', 'WC_REST_Webhook_Deliveries_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php', 'WC_REST_Webhooks_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php', 'WC_REST_Webhooks_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php', 'WC_REST_Webhooks_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixesPsr0; $loader->classMap = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$classMap; }, null, ClassLoader::class); } } vendor/composer/jetpack_autoload_classmap.php 0000644 00000502305 15132754523 0015611 0 ustar 00 <?php // This file `jetpack_autoload_classmap.php` was auto generated by automattic/jetpack-autoloader. $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Translator' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Translator.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php' ), 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php' ), 'Symfony\\Component\\CssSelector\\Tests\\XPath\\TranslatorTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/XPath/TranslatorTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\CssSelectorConverterTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/CssSelectorConverterTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\HashNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/HashNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\NegationNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/NegationNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\AbstractNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/AbstractNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\SelectorNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/SelectorNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\PseudoNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/PseudoNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\ClassNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/ClassNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\AttributeNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/AttributeNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\FunctionNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/FunctionNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\SpecificityTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/SpecificityTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\CombinedSelectorNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/CombinedSelectorNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Node\\ElementNodeTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/ElementNodeTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\ParserTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/ParserTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\ReaderTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/ReaderTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\TokenStreamTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/TokenStreamTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\HashParserTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/HashParserTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\ClassParserTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/ClassParserTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\ElementParserTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/ElementParserTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\EmptyStringParserTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/EmptyStringParserTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\CommentHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/CommentHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\HashHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/HashHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\AbstractHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/AbstractHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\IdentifierHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/IdentifierHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\WhitespaceHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/WhitespaceHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\NumberHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/NumberHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\StringHandlerTest' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/StringHandlerTest.php' ), 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php' ), 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php' ), 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php' ), 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php' ), 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php' ), 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\HashNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/HashNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php' ), 'Symfony\\Component\\CssSelector\\Node\\Specificity' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/Specificity.php' ), 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php' ), 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php' ), 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Token' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Token.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Reader' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Reader.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php' ), 'Symfony\\Component\\CssSelector\\Parser\\Parser' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/Parser.php' ), 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php' ), 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => array( 'version' => '3.3.6.0', 'path' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php' ), 'Psr\\Container\\NotFoundExceptionInterface' => array( 'version' => '1.0.0.0', 'path' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php' ), 'Psr\\Container\\ContainerExceptionInterface' => array( 'version' => '1.0.0.0', 'path' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php' ), 'Psr\\Container\\ContainerInterface' => array( 'version' => '1.0.0.0', 'path' => $vendorDir . '/psr/container/src/ContainerInterface.php' ), 'Pelago\\Emogrifier' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier.php' ), 'Pelago\\Emogrifier\\CssInliner' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssInliner.php' ), 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php' ), 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php' ), 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php' ), 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php' ), 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php' ), 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => array( 'version' => '3.1.0.0', 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php' ), 'MaxMind\\Db\\Reader\\Decoder' => array( 'version' => '1.6.0.0', 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php' ), 'MaxMind\\Db\\Reader\\Metadata' => array( 'version' => '1.6.0.0', 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php' ), 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => array( 'version' => '1.6.0.0', 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php' ), 'MaxMind\\Db\\Reader\\Util' => array( 'version' => '1.6.0.0', 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php' ), 'MaxMind\\Db\\Reader' => array( 'version' => '1.6.0.0', 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader.php' ), 'Composer\\Installers\\SyliusInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SyliusInstaller.php' ), 'Composer\\Installers\\GravInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/GravInstaller.php' ), 'Composer\\Installers\\CodeIgniterInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php' ), 'Composer\\Installers\\BonefishInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BonefishInstaller.php' ), 'Composer\\Installers\\ItopInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ItopInstaller.php' ), 'Composer\\Installers\\DrupalInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DrupalInstaller.php' ), 'Composer\\Installers\\DokuWikiInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php' ), 'Composer\\Installers\\YawikInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/YawikInstaller.php' ), 'Composer\\Installers\\ZikulaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php' ), 'Composer\\Installers\\BaseInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BaseInstaller.php' ), 'Composer\\Installers\\LanManagementSystemInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php' ), 'Composer\\Installers\\BitrixInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BitrixInstaller.php' ), 'Composer\\Installers\\CraftInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CraftInstaller.php' ), 'Composer\\Installers\\ImageCMSInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php' ), 'Composer\\Installers\\SilverStripeInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php' ), 'Composer\\Installers\\Installer' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Installer.php' ), 'Composer\\Installers\\CakePHPInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php' ), 'Composer\\Installers\\ShopwareInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php' ), 'Composer\\Installers\\ExpressionEngineInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php' ), 'Composer\\Installers\\TastyIgniterInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php' ), 'Composer\\Installers\\PhpBBInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php' ), 'Composer\\Installers\\DecibelInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DecibelInstaller.php' ), 'Composer\\Installers\\OxidInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OxidInstaller.php' ), 'Composer\\Installers\\MediaWikiInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php' ), 'Composer\\Installers\\CroogoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CroogoInstaller.php' ), 'Composer\\Installers\\LavaLiteInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php' ), 'Composer\\Installers\\ClanCatsFrameworkInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php' ), 'Composer\\Installers\\PrestashopInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php' ), 'Composer\\Installers\\KnownInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KnownInstaller.php' ), 'Composer\\Installers\\FuelInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelInstaller.php' ), 'Composer\\Installers\\AnnotateCmsInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php' ), 'Composer\\Installers\\PiwikInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PiwikInstaller.php' ), 'Composer\\Installers\\KirbyInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KirbyInstaller.php' ), 'Composer\\Installers\\OctoberInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OctoberInstaller.php' ), 'Composer\\Installers\\HuradInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/HuradInstaller.php' ), 'Composer\\Installers\\KodiCMSInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php' ), 'Composer\\Installers\\CiviCrmInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php' ), 'Composer\\Installers\\KanboardInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KanboardInstaller.php' ), 'Composer\\Installers\\StarbugInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/StarbugInstaller.php' ), 'Composer\\Installers\\WordPressInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WordPressInstaller.php' ), 'Composer\\Installers\\OsclassInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OsclassInstaller.php' ), 'Composer\\Installers\\EliasisInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/EliasisInstaller.php' ), 'Composer\\Installers\\TYPO3FlowInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php' ), 'Composer\\Installers\\ChefInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ChefInstaller.php' ), 'Composer\\Installers\\Plugin' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Plugin.php' ), 'Composer\\Installers\\JoomlaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php' ), 'Composer\\Installers\\PhiftyInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php' ), 'Composer\\Installers\\MODXEvoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php' ), 'Composer\\Installers\\MODULEWorkInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php' ), 'Composer\\Installers\\CockpitInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CockpitInstaller.php' ), 'Composer\\Installers\\FuelphpInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php' ), 'Composer\\Installers\\MakoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MakoInstaller.php' ), 'Composer\\Installers\\ElggInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ElggInstaller.php' ), 'Composer\\Installers\\WHMCSInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php' ), 'Composer\\Installers\\DolibarrInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php' ), 'Composer\\Installers\\AttogramInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AttogramInstaller.php' ), 'Composer\\Installers\\AglInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php' ), 'Composer\\Installers\\OntoWikiInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php' ), 'Composer\\Installers\\MauticInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MauticInstaller.php' ), 'Composer\\Installers\\Concrete5Installer' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Concrete5Installer.php' ), 'Composer\\Installers\\SiteDirectInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php' ), 'Composer\\Installers\\ZendInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ZendInstaller.php' ), 'Composer\\Installers\\LithiumInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LithiumInstaller.php' ), 'Composer\\Installers\\KohanaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KohanaInstaller.php' ), 'Composer\\Installers\\Symfony1Installer' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Symfony1Installer.php' ), 'Composer\\Installers\\ModxInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ModxInstaller.php' ), 'Composer\\Installers\\MajimaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MajimaInstaller.php' ), 'Composer\\Installers\\PPIInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PPIInstaller.php' ), 'Composer\\Installers\\WolfCMSInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php' ), 'Composer\\Installers\\MantisBTInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php' ), 'Composer\\Installers\\ProcessWireInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php' ), 'Composer\\Installers\\PantheonInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PantheonInstaller.php' ), 'Composer\\Installers\\WinterInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WinterInstaller.php' ), 'Composer\\Installers\\TheliaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TheliaInstaller.php' ), 'Composer\\Installers\\DframeInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DframeInstaller.php' ), 'Composer\\Installers\\RedaxoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php' ), 'Composer\\Installers\\PlentymarketsInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php' ), 'Composer\\Installers\\MoodleInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MoodleInstaller.php' ), 'Composer\\Installers\\MicroweberInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php' ), 'Composer\\Installers\\RoundcubeInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php' ), 'Composer\\Installers\\RadPHPInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php' ), 'Composer\\Installers\\PuppetInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PuppetInstaller.php' ), 'Composer\\Installers\\MiaoxingInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php' ), 'Composer\\Installers\\PimcoreInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php' ), 'Composer\\Installers\\TaoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TaoInstaller.php' ), 'Composer\\Installers\\SyDESInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SyDESInstaller.php' ), 'Composer\\Installers\\ReIndexInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php' ), 'Composer\\Installers\\MagentoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MagentoInstaller.php' ), 'Composer\\Installers\\TYPO3CmsInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php' ), 'Composer\\Installers\\AsgardInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AsgardInstaller.php' ), 'Composer\\Installers\\EzPlatformInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php' ), 'Composer\\Installers\\Redaxo5Installer' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php' ), 'Composer\\Installers\\PortoInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PortoInstaller.php' ), 'Composer\\Installers\\LaravelInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LaravelInstaller.php' ), 'Composer\\Installers\\MayaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MayaInstaller.php' ), 'Composer\\Installers\\SMFInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SMFInstaller.php' ), 'Composer\\Installers\\TuskInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TuskInstaller.php' ), 'Composer\\Installers\\PxcmsInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php' ), 'Composer\\Installers\\VgmcpInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php' ), 'Composer\\Installers\\VanillaInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/VanillaInstaller.php' ), 'Composer\\Installers\\UserFrostingInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php' ), 'Composer\\Installers\\AimeosInstaller' => array( 'version' => '1.12.0.0', 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AimeosInstaller.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Container.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassName.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/RawArgument.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/RawArgumentInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Inflector/Inflector.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregate.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Exception/ContainerException.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Exception/NotFoundException.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ContainerAwareTrait.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregate.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/Definition/Definition.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ContainerAwareInterface.php' ), 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/lib/packages/League/Container/ReflectionContainer.php' ), 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Proxies/MockableLegacyProxyTest.php' ), 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php' ), 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php' ), 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Proxies/LegacyProxyTest.php' ), 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Utilities/NumberUtilTest.php' ), 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Utilities/StringUtilTest.php' ), 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Utilities/ArrayUtilTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/AssignDefaultCategoryTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/RestApiUtilTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php' ), 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/CodeHack.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/CodeHacking/CodeHacker.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/FakeQueue.php' ), 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Package.php' ), 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypesController.php' ), 'Automattic\\WooCommerce\\Blocks\\Installer' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Installer.php' ), 'Automattic\\WooCommerce\\Blocks\\Library' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Library.php' ), 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php' ), 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php' ), 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php' ), 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php' ), 'Automattic\\WooCommerce\\Blocks\\RestApi' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/RestApi.php' ), 'Automattic\\WooCommerce\\Blocks\\AssetsController' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/AssetsController.php' ), 'Automattic\\WooCommerce\\Blocks\\Assets' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Api.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php' ), 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php' ), 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php' ), 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/Container.php' ), 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/FactoryType.php' ), 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/SharedType.php' ), 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php' ), 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php' ), 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php' ), 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets/Api.php' ), 'Automattic\\WooCommerce\\Blocks\\Package' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/Package.php' ), 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => array( 'version' => '6.1.0.0', 'path' => $baseDir . '/packages/woocommerce-blocks/src/InboxNotifications.php' ), 'Automattic\\WooCommerce\\Admin\\Install' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Install.php' ), 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Composer/Package.php' ), 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVExporter.php' ), 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/CategoryLookup.php' ), 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php' ), 'Automattic\\WooCommerce\\Admin\\Loader' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Loader.php' ), 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/WCAdminHelper.php' ), 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/Order.php' ), 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php' ), 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php' ), 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php' ), 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php' ), 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php' ), 'Automattic\\WooCommerce\\Admin\\ReportsSync' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportsSync.php' ), 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php' ), 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Customers' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Customers.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Cache.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Options' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Options.php' ), 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php' ), 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductReviews.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingPayments.php' ), 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/MarketingOverview.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Themes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Themes.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Taxes.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Leaderboards.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductCategories.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Data' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Data.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Features' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Features.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributes.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Init.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingTasks.php' ), 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductVariations.php' ), 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/NavigationFavorites.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProfile.php' ), 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/SettingOptions.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Products' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Products.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Orders' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Orders.php' ), 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingThemes.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Marketing.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Coupons.php' ), 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/DataCountries.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Plugins.php' ), 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/NoteActions.php' ), 'Automattic\\WooCommerce\\Admin\\API\\Notes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Notes.php' ), 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PaymentPlugins.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php' ), 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php' ), 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php' ), 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Note.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DataStore.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/TestCheckout.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NoteTraits.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DrawAttention.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstProduct.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MobileApp.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Marketing.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php' ), 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Notes.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ActivityPanels.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Homescreen.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Analytics.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Onboarding.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Features' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Features.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Settings.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Init.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Marketing.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Coupons.php' ), 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/TransientNotices.php' ), 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php' ), 'Automattic\\WooCommerce\\Admin\\Events' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Events.php' ), 'Automattic\\WooCommerce\\Admin\\PageController' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PageController.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php' ), 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php' ), 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsInstaller.php' ), 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/FeaturePlugin.php' ), 'Automattic\\WooCommerce\\Admin\\Survey' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/Survey.php' ), 'Automattic\\WooCommerce\\Admin\\ReportExporter' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportExporter.php' ), 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVEmail.php' ), 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => array( 'version' => '2.8.0.0', 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsHelper.php' ), 'Automattic\\WooCommerce\\Container' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Container.php' ), 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Proxies/ActionsProxy.php' ), 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Proxies/LegacyProxy.php' ), 'Automattic\\WooCommerce\\Packages' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Packages.php' ), 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Utilities/NumberUtil.php' ), 'Automattic\\WooCommerce\\Utilities\\StringUtil' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Utilities/StringUtil.php' ), 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Utilities/ArrayUtil.php' ), 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Checkout/Helpers/ReserveStockException.php' ), 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Checkout/Helpers/ReserveStock.php' ), 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/RestockRefundedItemsAdjuster.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ContainerException.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/AbstractServiceProvider.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/ExtendedContainer.php' ), 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DependencyManagement/Definition.php' ), 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/AssignDefaultCategory.php' ), 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/WCCom/ConnectionHelper.php' ), 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/RestApiUtil.php' ), 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/LookupDataStore.php' ), 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/Filterer.php' ), 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/DataRegenerator.php' ), 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Internal/DownloadPermissionsAdjuster.php' ), 'Automattic\\WooCommerce\\Autoloader' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/src/Autoloader.php' ), 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => array( 'version' => '2.10.1.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php' ), 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => array( 'version' => '2.10.1.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/ManifestGenerator.php' ), 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => array( 'version' => '2.10.1.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php' ), 'Automattic\\Jetpack\\Autoloader\\AutoloadFileWriter' => array( 'version' => '2.10.1.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php' ), 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => array( 'version' => '2.10.1.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php' ), 'WC_REST_Shipping_Methods_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php' ), 'WC_REST_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php' ), 'WC_REST_Product_Attributes_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php' ), 'WC_REST_System_Status_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php' ), 'WC_REST_Report_Sales_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php' ), 'WC_REST_Shipping_Zones_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php' ), 'WC_REST_Product_Variations_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php' ), 'WC_REST_Network_Orders_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php' ), 'WC_REST_Product_Shipping_Classes_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php' ), 'WC_REST_Customer_Downloads_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php' ), 'WC_REST_Taxes_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php' ), 'WC_REST_Coupons_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php' ), 'WC_REST_Tax_Classes_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php' ), 'WC_REST_Posts_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php' ), 'WC_REST_Report_Coupons_Totals_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php' ), 'WC_REST_Setting_Options_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php' ), 'WC_REST_Terms_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php' ), 'WC_REST_Orders_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php' ), 'WC_REST_Report_Orders_Totals_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php' ), 'WC_REST_Data_Continents_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php' ), 'WC_REST_Shipping_Zone_Locations_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php' ), 'WC_REST_Shipping_Zone_Methods_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php' ), 'WC_REST_Report_Reviews_Totals_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php' ), 'WC_REST_Data_Countries_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php' ), 'WC_REST_Settings_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php' ), 'WC_REST_Data_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php' ), 'WC_REST_Product_Tags_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php' ), 'WC_REST_Report_Customers_Totals_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php' ), 'WC_REST_Report_Top_Sellers_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php' ), 'WC_REST_CRUD_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php' ), 'WC_REST_Customers_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php' ), 'WC_REST_System_Status_Tools_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php' ), 'WC_REST_Order_Refunds_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php' ), 'WC_REST_Report_Products_Totals_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php' ), 'WC_REST_Payment_Gateways_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php' ), 'WC_REST_Order_Notes_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php' ), 'WC_REST_Products_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php' ), 'WC_REST_Product_Categories_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php' ), 'WC_REST_Data_Currencies_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php' ), 'WC_REST_Product_Reviews_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php' ), 'WC_REST_Shipping_Zones_Controller_Base' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php' ), 'WC_REST_Product_Attribute_Terms_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php' ), 'WC_REST_Reports_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php' ), 'WC_REST_Webhooks_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php' ), 'WC_REST_Customers_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php' ), 'WC_REST_Reports_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php' ), 'WC_REST_Product_Reviews_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php' ), 'WC_REST_Order_Refunds_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php' ), 'WC_REST_Product_Variations_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php' ), 'WC_REST_Network_Orders_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php' ), 'WC_REST_Taxes_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php' ), 'WC_REST_System_Status_Tools_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php' ), 'WC_REST_Setting_Options_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php' ), 'WC_REST_Coupons_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php' ), 'WC_REST_Webhooks_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php' ), 'WC_REST_System_Status_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php' ), 'WC_REST_Order_Notes_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php' ), 'WC_REST_Products_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php' ), 'WC_REST_Product_Attributes_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php' ), 'WC_REST_Customer_Downloads_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php' ), 'WC_REST_Product_Tags_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php' ), 'WC_REST_Report_Sales_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php' ), 'WC_REST_Product_Categories_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php' ), 'WC_REST_Payment_Gateways_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php' ), 'WC_REST_Shipping_Zones_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php' ), 'WC_REST_Tax_Classes_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php' ), 'WC_REST_Webhook_Deliveries_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php' ), 'WC_REST_Settings_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php' ), 'WC_REST_Product_Shipping_Classes_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php' ), 'WC_REST_Shipping_Zone_Locations_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php' ), 'WC_REST_Orders_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php' ), 'WC_REST_Shipping_Methods_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php' ), 'WC_REST_Report_Top_Sellers_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php' ), 'WC_REST_Product_Attribute_Terms_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php' ), 'WC_REST_Shipping_Zone_Methods_V2_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php' ), 'WC_REST_Telemetry_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php' ), 'WC_REST_Webhooks_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php' ), 'WC_REST_Customers_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php' ), 'WC_REST_Customer_Downloads_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php' ), 'WC_REST_Product_Tags_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php' ), 'WC_REST_Tax_Classes_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php' ), 'WC_REST_Product_Reviews_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php' ), 'WC_REST_Report_Sales_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php' ), 'WC_REST_Product_Attributes_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php' ), 'WC_REST_Order_Notes_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php' ), 'WC_REST_Report_Top_Sellers_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php' ), 'WC_REST_Coupons_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php' ), 'WC_REST_Taxes_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php' ), 'WC_REST_Product_Shipping_Classes_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php' ), 'WC_REST_Orders_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php' ), 'WC_REST_Webhook_Deliveries_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php' ), 'WC_REST_Reports_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php' ), 'WC_REST_Products_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php' ), 'WC_REST_Order_Refunds_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php' ), 'WC_REST_Product_Attribute_Terms_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php' ), 'WC_REST_Product_Categories_V1_Controller' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php' ), 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Utilities/SingletonTrait.php' ), 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Utilities/ImageAttachment.php' ), 'Automattic\\WooCommerce\\RestApi\\Package' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Package.php' ), 'Automattic\\WooCommerce\\RestApi\\Server' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/includes/rest-api/Server.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php' ), 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => array( 'version' => '5.9.0.0', 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php' ), 'Automattic\\Jetpack\\Constants' => array( 'version' => '1.5.1.0', 'path' => $vendorDir . '/automattic/jetpack-constants/src/class-constants.php' ), ); vendor/composer/autoload_classmap.php 0000644 00000353340 15132754523 0014113 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Automattic\\Jetpack\\Autoloader\\AutoloadFileWriter' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php', 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php', 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => $vendorDir . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php', 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => $vendorDir . '/automattic/jetpack-autoloader/src/ManifestGenerator.php', 'Automattic\\Jetpack\\Constants' => $vendorDir . '/automattic/jetpack-constants/src/class-constants.php', 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => $baseDir . '/packages/woocommerce-admin/src/API/Coupons.php', 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => $baseDir . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php', 'Automattic\\WooCommerce\\Admin\\API\\Customers' => $baseDir . '/packages/woocommerce-admin/src/API/Customers.php', 'Automattic\\WooCommerce\\Admin\\API\\Data' => $baseDir . '/packages/woocommerce-admin/src/API/Data.php', 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => $baseDir . '/packages/woocommerce-admin/src/API/DataCountries.php', 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => $baseDir . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php', 'Automattic\\WooCommerce\\Admin\\API\\Features' => $baseDir . '/packages/woocommerce-admin/src/API/Features.php', 'Automattic\\WooCommerce\\Admin\\API\\Init' => $baseDir . '/packages/woocommerce-admin/src/API/Init.php', 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => $baseDir . '/packages/woocommerce-admin/src/API/Leaderboards.php', 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/API/Marketing.php', 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => $baseDir . '/packages/woocommerce-admin/src/API/MarketingOverview.php', 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => $baseDir . '/packages/woocommerce-admin/src/API/NavigationFavorites.php', 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => $baseDir . '/packages/woocommerce-admin/src/API/NoteActions.php', 'Automattic\\WooCommerce\\Admin\\API\\Notes' => $baseDir . '/packages/woocommerce-admin/src/API/Notes.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingPayments.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProfile.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingTasks.php', 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingThemes.php', 'Automattic\\WooCommerce\\Admin\\API\\Options' => $baseDir . '/packages/woocommerce-admin/src/API/Options.php', 'Automattic\\WooCommerce\\Admin\\API\\Orders' => $baseDir . '/packages/woocommerce-admin/src/API/Orders.php', 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => $baseDir . '/packages/woocommerce-admin/src/API/Plugins.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributes.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => $baseDir . '/packages/woocommerce-admin/src/API/ProductCategories.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => $baseDir . '/packages/woocommerce-admin/src/API/ProductReviews.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => $baseDir . '/packages/woocommerce-admin/src/API/ProductVariations.php', 'Automattic\\WooCommerce\\Admin\\API\\Products' => $baseDir . '/packages/woocommerce-admin/src/API/Products.php', 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => $baseDir . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Cache.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php', 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php', 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => $baseDir . '/packages/woocommerce-admin/src/API/SettingOptions.php', 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => $baseDir . '/packages/woocommerce-admin/src/API/Taxes.php', 'Automattic\\WooCommerce\\Admin\\API\\Themes' => $baseDir . '/packages/woocommerce-admin/src/API/Themes.php', 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => $baseDir . '/packages/woocommerce-admin/src/CategoryLookup.php', 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => $baseDir . '/packages/woocommerce-admin/src/Composer/Package.php', 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php', 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php', 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => $baseDir . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php', 'Automattic\\WooCommerce\\Admin\\Events' => $baseDir . '/packages/woocommerce-admin/src/Events.php', 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => $baseDir . '/packages/woocommerce-admin/src/FeaturePlugin.php', 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => $baseDir . '/packages/woocommerce-admin/src/Features/ActivityPanels.php', 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => $baseDir . '/packages/woocommerce-admin/src/Features/Analytics.php', 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => $baseDir . '/packages/woocommerce-admin/src/Features/Coupons.php', 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => $baseDir . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php', 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => $baseDir . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php', 'Automattic\\WooCommerce\\Admin\\Features\\Features' => $baseDir . '/packages/woocommerce-admin/src/Features/Features.php', 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => $baseDir . '/packages/woocommerce-admin/src/Features/Homescreen.php', 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Features/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => $baseDir . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php', 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php', 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => $baseDir . '/packages/woocommerce-admin/src/Features/Onboarding.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php', 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php', 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => $baseDir . '/packages/woocommerce-admin/src/Features/Settings.php', 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php', 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php', 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => $baseDir . '/packages/woocommerce-admin/src/Features/TransientNotices.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php', 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php', 'Automattic\\WooCommerce\\Admin\\Install' => $baseDir . '/packages/woocommerce-admin/src/Install.php', 'Automattic\\WooCommerce\\Admin\\Loader' => $baseDir . '/packages/woocommerce-admin/src/Loader.php', 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => $baseDir . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php', 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => $baseDir . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php', 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/DataStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php', 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => $baseDir . '/packages/woocommerce-admin/src/Notes/DrawAttention.php', 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => $baseDir . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php', 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => $baseDir . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => $baseDir . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstProduct.php', 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => $baseDir . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php', 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => $baseDir . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php', 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => $baseDir . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php', 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => $baseDir . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php', 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => $baseDir . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php', 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/Marketing.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => $baseDir . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => $baseDir . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php', 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => $baseDir . '/packages/woocommerce-admin/src/Notes/MobileApp.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => $baseDir . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => $baseDir . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => $baseDir . '/packages/woocommerce-admin/src/Notes/Note.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => $baseDir . '/packages/woocommerce-admin/src/Notes/NoteTraits.php', 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/Notes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => $baseDir . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => $baseDir . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php', 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => $baseDir . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php', 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php', 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => $baseDir . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php', 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => $baseDir . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php', 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => $baseDir . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => $baseDir . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php', 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => $baseDir . '/packages/woocommerce-admin/src/Notes/TestCheckout.php', 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => $baseDir . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php', 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => $baseDir . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => $baseDir . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php', 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => $baseDir . '/packages/woocommerce-admin/src/Overrides/Order.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php', 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php', 'Automattic\\WooCommerce\\Admin\\PageController' => $baseDir . '/packages/woocommerce-admin/src/PageController.php', 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => $baseDir . '/packages/woocommerce-admin/src/PaymentPlugins.php', 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => $baseDir . '/packages/woocommerce-admin/src/PluginsHelper.php', 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => $baseDir . '/packages/woocommerce-admin/src/PluginsInstaller.php', 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php', 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php', 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVEmail.php', 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVExporter.php', 'Automattic\\WooCommerce\\Admin\\ReportExporter' => $baseDir . '/packages/woocommerce-admin/src/ReportExporter.php', 'Automattic\\WooCommerce\\Admin\\ReportsSync' => $baseDir . '/packages/woocommerce-admin/src/ReportsSync.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php', 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php', 'Automattic\\WooCommerce\\Admin\\Survey' => $baseDir . '/packages/woocommerce-admin/src/Survey.php', 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => $baseDir . '/packages/woocommerce-admin/src/WCAdminHelper.php', 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => $baseDir . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php', 'Automattic\\WooCommerce\\Autoloader' => $baseDir . '/src/Autoloader.php', 'Automattic\\WooCommerce\\Blocks\\Assets' => $baseDir . '/packages/woocommerce-blocks/src/Assets.php', 'Automattic\\WooCommerce\\Blocks\\AssetsController' => $baseDir . '/packages/woocommerce-blocks/src/AssetsController.php', 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => $baseDir . '/packages/woocommerce-blocks/src/Assets/Api.php', 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypesController.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php', 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Package.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php', 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php', 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => $baseDir . '/packages/woocommerce-blocks/src/InboxNotifications.php', 'Automattic\\WooCommerce\\Blocks\\Installer' => $baseDir . '/packages/woocommerce-blocks/src/Installer.php', 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php', 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php', 'Automattic\\WooCommerce\\Blocks\\Library' => $baseDir . '/packages/woocommerce-blocks/src/Library.php', 'Automattic\\WooCommerce\\Blocks\\Package' => $baseDir . '/packages/woocommerce-blocks/src/Package.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Api.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php', 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => $baseDir . '/packages/woocommerce-blocks/src/Registry/Container.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/FactoryType.php', 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/SharedType.php', 'Automattic\\WooCommerce\\Blocks\\RestApi' => $baseDir . '/packages/woocommerce-blocks/src/RestApi.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php', 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php', 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => $baseDir . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php', 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => $baseDir . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php', 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => $baseDir . '/src/Checkout/Helpers/ReserveStock.php', 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => $baseDir . '/src/Checkout/Helpers/ReserveStockException.php', 'Automattic\\WooCommerce\\Container' => $baseDir . '/src/Container.php', 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => $baseDir . '/src/Internal/AssignDefaultCategory.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/AbstractServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => $baseDir . '/src/Internal/DependencyManagement/ContainerException.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => $baseDir . '/src/Internal/DependencyManagement/Definition.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => $baseDir . '/src/Internal/DependencyManagement/ExtendedContainer.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php', 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => $baseDir . '/src/Internal/DownloadPermissionsAdjuster.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => $baseDir . '/src/Internal/ProductAttributesLookup/DataRegenerator.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => $baseDir . '/src/Internal/ProductAttributesLookup/Filterer.php', 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => $baseDir . '/src/Internal/ProductAttributesLookup/LookupDataStore.php', 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => $baseDir . '/src/Internal/RestApiUtil.php', 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => $baseDir . '/src/Internal/RestockRefundedItemsAdjuster.php', 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => $baseDir . '/src/Internal/WCCom/ConnectionHelper.php', 'Automattic\\WooCommerce\\Packages' => $baseDir . '/src/Packages.php', 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => $baseDir . '/src/Proxies/ActionsProxy.php', 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => $baseDir . '/src/Proxies/LegacyProxy.php', 'Automattic\\WooCommerce\\RestApi\\Package' => $baseDir . '/includes/rest-api/Package.php', 'Automattic\\WooCommerce\\RestApi\\Server' => $baseDir . '/includes/rest-api/Server.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php', 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php', 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => $baseDir . '/includes/rest-api/Utilities/ImageAttachment.php', 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => $baseDir . '/includes/rest-api/Utilities/SingletonTrait.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => $baseDir . '/tests/Tools/CodeHacking/CodeHacker.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/CodeHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php', 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => $baseDir . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php', 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => $baseDir . '/tests/Tools/FakeQueue.php', 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => $baseDir . '/tests/php/src/Internal/AssignDefaultCategoryTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => $baseDir . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => $baseDir . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => $baseDir . '/tests/php/src/Internal/RestApiUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => $baseDir . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => $baseDir . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => $baseDir . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => $baseDir . '/tests/php/src/Proxies/LegacyProxyTest.php', 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => $baseDir . '/tests/php/src/Proxies/MockableLegacyProxyTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => $baseDir . '/tests/php/src/Utilities/ArrayUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => $baseDir . '/tests/php/src/Utilities/NumberUtilTest.php', 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => $baseDir . '/tests/php/src/Utilities/StringUtilTest.php', 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => $baseDir . '/src/Utilities/ArrayUtil.php', 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => $baseDir . '/src/Utilities/NumberUtil.php', 'Automattic\\WooCommerce\\Utilities\\StringUtil' => $baseDir . '/src/Utilities/StringUtil.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => $baseDir . '/lib/packages/League/Container/Argument/ClassName.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => $baseDir . '/lib/packages/League/Container/Argument/RawArgument.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => $baseDir . '/lib/packages/League/Container/Argument/RawArgumentInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => $baseDir . '/lib/packages/League/Container/Container.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => $baseDir . '/lib/packages/League/Container/ContainerAwareInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => $baseDir . '/lib/packages/League/Container/ContainerAwareTrait.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => $baseDir . '/lib/packages/League/Container/Definition/Definition.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => $baseDir . '/lib/packages/League/Container/Exception/ContainerException.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => $baseDir . '/lib/packages/League/Container/Exception/NotFoundException.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => $baseDir . '/lib/packages/League/Container/Inflector/Inflector.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => $baseDir . '/lib/packages/League/Container/ReflectionContainer.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => $baseDir . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php', 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php', 'Composer\\Installers\\AglInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php', 'Composer\\Installers\\AimeosInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AimeosInstaller.php', 'Composer\\Installers\\AnnotateCmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', 'Composer\\Installers\\AsgardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', 'Composer\\Installers\\AttogramInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', 'Composer\\Installers\\BaseInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BaseInstaller.php', 'Composer\\Installers\\BitrixInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', 'Composer\\Installers\\BonefishInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', 'Composer\\Installers\\CakePHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', 'Composer\\Installers\\ChefInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ChefInstaller.php', 'Composer\\Installers\\CiviCrmInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', 'Composer\\Installers\\ClanCatsFrameworkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', 'Composer\\Installers\\CockpitInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', 'Composer\\Installers\\CodeIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', 'Composer\\Installers\\Concrete5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', 'Composer\\Installers\\CraftInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CraftInstaller.php', 'Composer\\Installers\\CroogoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', 'Composer\\Installers\\DecibelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', 'Composer\\Installers\\DframeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DframeInstaller.php', 'Composer\\Installers\\DokuWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', 'Composer\\Installers\\DolibarrInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', 'Composer\\Installers\\DrupalInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', 'Composer\\Installers\\ElggInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ElggInstaller.php', 'Composer\\Installers\\EliasisInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', 'Composer\\Installers\\ExpressionEngineInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', 'Composer\\Installers\\EzPlatformInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', 'Composer\\Installers\\FuelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelInstaller.php', 'Composer\\Installers\\FuelphpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', 'Composer\\Installers\\GravInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/GravInstaller.php', 'Composer\\Installers\\HuradInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/HuradInstaller.php', 'Composer\\Installers\\ImageCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', 'Composer\\Installers\\Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Installer.php', 'Composer\\Installers\\ItopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ItopInstaller.php', 'Composer\\Installers\\JoomlaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php', 'Composer\\Installers\\KanboardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', 'Composer\\Installers\\KirbyInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KirbyInstaller.php', 'Composer\\Installers\\KnownInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KnownInstaller.php', 'Composer\\Installers\\KodiCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', 'Composer\\Installers\\KohanaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', 'Composer\\Installers\\LanManagementSystemInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', 'Composer\\Installers\\LaravelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', 'Composer\\Installers\\LavaLiteInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', 'Composer\\Installers\\LithiumInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', 'Composer\\Installers\\MODULEWorkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', 'Composer\\Installers\\MODXEvoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', 'Composer\\Installers\\MagentoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', 'Composer\\Installers\\MajimaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', 'Composer\\Installers\\MakoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MakoInstaller.php', 'Composer\\Installers\\MantisBTInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', 'Composer\\Installers\\MauticInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MauticInstaller.php', 'Composer\\Installers\\MayaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MayaInstaller.php', 'Composer\\Installers\\MediaWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', 'Composer\\Installers\\MiaoxingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', 'Composer\\Installers\\MicroweberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', 'Composer\\Installers\\ModxInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ModxInstaller.php', 'Composer\\Installers\\MoodleInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', 'Composer\\Installers\\OctoberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', 'Composer\\Installers\\OntoWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', 'Composer\\Installers\\OsclassInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', 'Composer\\Installers\\OxidInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OxidInstaller.php', 'Composer\\Installers\\PPIInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PPIInstaller.php', 'Composer\\Installers\\PantheonInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', 'Composer\\Installers\\PhiftyInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', 'Composer\\Installers\\PhpBBInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', 'Composer\\Installers\\PimcoreInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php', 'Composer\\Installers\\PiwikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', 'Composer\\Installers\\PlentymarketsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', 'Composer\\Installers\\Plugin' => $vendorDir . '/composer/installers/src/Composer/Installers/Plugin.php', 'Composer\\Installers\\PortoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PortoInstaller.php', 'Composer\\Installers\\PrestashopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', 'Composer\\Installers\\ProcessWireInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', 'Composer\\Installers\\PuppetInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', 'Composer\\Installers\\PxcmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', 'Composer\\Installers\\RadPHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', 'Composer\\Installers\\ReIndexInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', 'Composer\\Installers\\Redaxo5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', 'Composer\\Installers\\RedaxoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', 'Composer\\Installers\\RoundcubeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', 'Composer\\Installers\\SMFInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SMFInstaller.php', 'Composer\\Installers\\ShopwareInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', 'Composer\\Installers\\SilverStripeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', 'Composer\\Installers\\SiteDirectInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', 'Composer\\Installers\\StarbugInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', 'Composer\\Installers\\SyDESInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', 'Composer\\Installers\\SyliusInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', 'Composer\\Installers\\Symfony1Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Symfony1Installer.php', 'Composer\\Installers\\TYPO3CmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php', 'Composer\\Installers\\TYPO3FlowInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php', 'Composer\\Installers\\TaoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TaoInstaller.php', 'Composer\\Installers\\TastyIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', 'Composer\\Installers\\TheliaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', 'Composer\\Installers\\TuskInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TuskInstaller.php', 'Composer\\Installers\\UserFrostingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', 'Composer\\Installers\\VanillaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', 'Composer\\Installers\\VgmcpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', 'Composer\\Installers\\WHMCSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', 'Composer\\Installers\\WinterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WinterInstaller.php', 'Composer\\Installers\\WolfCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', 'Composer\\Installers\\WordPressInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', 'Composer\\Installers\\YawikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/YawikInstaller.php', 'Composer\\Installers\\ZendInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZendInstaller.php', 'Composer\\Installers\\ZikulaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', 'MaxMind\\Db\\Reader' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader.php', 'MaxMind\\Db\\Reader\\Decoder' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php', 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php', 'MaxMind\\Db\\Reader\\Metadata' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php', 'MaxMind\\Db\\Reader\\Util' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php', 'Pelago\\Emogrifier' => $vendorDir . '/pelago/emogrifier/src/Emogrifier.php', 'Pelago\\Emogrifier\\CssInliner' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssInliner.php', 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php', 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php', 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php', 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php', 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php', 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php', 'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php', 'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php', 'Psr\\Container\\NotFoundExceptionInterface' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php', 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php', 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php', 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php', 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php', 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php', 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php', 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php', 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php', 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php', 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php', 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php', 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php', 'Symfony\\Component\\CssSelector\\Node\\HashNode' => $vendorDir . '/symfony/css-selector/Node/HashNode.php', 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php', 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php', 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php', 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php', 'Symfony\\Component\\CssSelector\\Node\\Specificity' => $vendorDir . '/symfony/css-selector/Node/Specificity.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', 'Symfony\\Component\\CssSelector\\Parser\\Parser' => $vendorDir . '/symfony/css-selector/Parser/Parser.php', 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php', 'Symfony\\Component\\CssSelector\\Parser\\Reader' => $vendorDir . '/symfony/css-selector/Parser/Reader.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php', 'Symfony\\Component\\CssSelector\\Parser\\Token' => $vendorDir . '/symfony/css-selector/Parser/Token.php', 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', 'Symfony\\Component\\CssSelector\\XPath\\Translator' => $vendorDir . '/symfony/css-selector/XPath/Translator.php', 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php', 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php', 'WC_REST_CRUD_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php', 'WC_REST_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php', 'WC_REST_Coupons_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php', 'WC_REST_Coupons_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php', 'WC_REST_Coupons_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php', 'WC_REST_Customer_Downloads_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php', 'WC_REST_Customer_Downloads_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php', 'WC_REST_Customer_Downloads_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php', 'WC_REST_Customers_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php', 'WC_REST_Customers_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php', 'WC_REST_Customers_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php', 'WC_REST_Data_Continents_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php', 'WC_REST_Data_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php', 'WC_REST_Data_Countries_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php', 'WC_REST_Data_Currencies_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php', 'WC_REST_Network_Orders_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php', 'WC_REST_Network_Orders_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php', 'WC_REST_Order_Notes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php', 'WC_REST_Order_Notes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php', 'WC_REST_Order_Notes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php', 'WC_REST_Order_Refunds_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php', 'WC_REST_Order_Refunds_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php', 'WC_REST_Order_Refunds_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php', 'WC_REST_Orders_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php', 'WC_REST_Orders_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php', 'WC_REST_Orders_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php', 'WC_REST_Payment_Gateways_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php', 'WC_REST_Payment_Gateways_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php', 'WC_REST_Posts_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php', 'WC_REST_Product_Attribute_Terms_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php', 'WC_REST_Product_Attribute_Terms_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php', 'WC_REST_Product_Attribute_Terms_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php', 'WC_REST_Product_Attributes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php', 'WC_REST_Product_Attributes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php', 'WC_REST_Product_Attributes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php', 'WC_REST_Product_Categories_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php', 'WC_REST_Product_Categories_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php', 'WC_REST_Product_Categories_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php', 'WC_REST_Product_Reviews_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php', 'WC_REST_Product_Reviews_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php', 'WC_REST_Product_Reviews_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php', 'WC_REST_Product_Shipping_Classes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php', 'WC_REST_Product_Shipping_Classes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php', 'WC_REST_Product_Shipping_Classes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php', 'WC_REST_Product_Tags_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php', 'WC_REST_Product_Tags_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php', 'WC_REST_Product_Tags_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php', 'WC_REST_Product_Variations_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php', 'WC_REST_Product_Variations_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php', 'WC_REST_Products_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php', 'WC_REST_Products_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php', 'WC_REST_Products_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php', 'WC_REST_Report_Coupons_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php', 'WC_REST_Report_Customers_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php', 'WC_REST_Report_Orders_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php', 'WC_REST_Report_Products_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php', 'WC_REST_Report_Reviews_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php', 'WC_REST_Report_Sales_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php', 'WC_REST_Report_Sales_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php', 'WC_REST_Report_Sales_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php', 'WC_REST_Report_Top_Sellers_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php', 'WC_REST_Report_Top_Sellers_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php', 'WC_REST_Report_Top_Sellers_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php', 'WC_REST_Reports_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php', 'WC_REST_Reports_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php', 'WC_REST_Reports_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php', 'WC_REST_Setting_Options_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php', 'WC_REST_Setting_Options_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php', 'WC_REST_Settings_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php', 'WC_REST_Settings_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php', 'WC_REST_Shipping_Methods_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php', 'WC_REST_Shipping_Methods_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php', 'WC_REST_Shipping_Zone_Locations_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php', 'WC_REST_Shipping_Zone_Locations_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php', 'WC_REST_Shipping_Zone_Methods_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php', 'WC_REST_Shipping_Zone_Methods_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php', 'WC_REST_Shipping_Zones_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php', 'WC_REST_Shipping_Zones_Controller_Base' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php', 'WC_REST_Shipping_Zones_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php', 'WC_REST_System_Status_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php', 'WC_REST_System_Status_Tools_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php', 'WC_REST_System_Status_Tools_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php', 'WC_REST_System_Status_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php', 'WC_REST_Tax_Classes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php', 'WC_REST_Tax_Classes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php', 'WC_REST_Tax_Classes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php', 'WC_REST_Taxes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php', 'WC_REST_Taxes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php', 'WC_REST_Taxes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php', 'WC_REST_Telemetry_Controller' => $baseDir . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php', 'WC_REST_Terms_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php', 'WC_REST_Webhook_Deliveries_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php', 'WC_REST_Webhook_Deliveries_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php', 'WC_REST_Webhooks_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php', 'WC_REST_Webhooks_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php', 'WC_REST_Webhooks_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php', ); vendor/composer/installers/phpstan.neon.dist 0000644 00000000325 15132754523 0015357 0 ustar 00 parameters: level: 5 paths: - src - tests excludes_analyse: - tests/Composer/Installers/Test/PolyfillTestCase.php includes: - vendor/phpstan/phpstan-phpunit/extension.neon vendor/composer/installers/src/Composer/Installers/StarbugInstaller.php 0000644 00000000461 15132754523 0022554 0 ustar 00 <?php namespace Composer\Installers; class StarbugInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', 'custom-module' => 'app/modules/{$name}/', 'custom-theme' => 'app/themes/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php 0000644 00000000341 15132754523 0022401 0 ustar 00 <?php namespace Composer\Installers; class ZikulaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}-{$name}/', 'theme' => 'themes/{$vendor}-{$name}/' ); } vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php 0000644 00000000741 15132754523 0022367 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle MODX Evolution specifics when installing packages. */ class MODXEvoInstaller extends BaseInstaller { protected $locations = array( 'snippet' => 'assets/snippets/{$name}/', 'plugin' => 'assets/plugins/{$name}/', 'module' => 'assets/modules/{$name}/', 'template' => 'assets/templates/{$name}/', 'lib' => 'assets/lib/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/ProcessWireInstaller.php 0000644 00000001053 15132754523 0023410 0 ustar 00 <?php namespace Composer\Installers; class ProcessWireInstaller extends BaseInstaller { protected $locations = array( 'module' => 'site/modules/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php 0000644 00000000421 15132754523 0022533 0 ustar 00 <?php namespace Composer\Installers; class MagentoInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'app/design/frontend/{$name}/', 'skin' => 'skin/frontend/default/{$name}/', 'library' => 'lib/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php 0000644 00000000461 15132754523 0022536 0 ustar 00 <?php namespace Composer\Installers; class EliasisInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$name}/', 'template' => 'templates/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/OxidInstaller.php 0000644 00000002652 15132754523 0022054 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class OxidInstaller extends BaseInstaller { const VENDOR_PATTERN = '/^modules\/(?P<vendor>.+)\/.+/'; protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'application/views/{$name}/', 'out' => 'out/{$name}/', ); /** * getInstallPath * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { $installPath = parent::getInstallPath($package, $frameworkType); $type = $this->package->getType(); if ($type === 'oxid-module') { $this->prepareVendorDirectory($installPath); } return $installPath; } /** * prepareVendorDirectory * * Makes sure there is a vendormetadata.php file inside * the vendor folder if there is a vendor folder. * * @param string $installPath * @return void */ protected function prepareVendorDirectory($installPath) { $matches = ''; $hasVendorDirectory = preg_match(self::VENDOR_PATTERN, $installPath, $matches); if (!$hasVendorDirectory) { return; } $vendorDirectory = $matches['vendor']; $vendorPath = getcwd() . '/modules/' . $vendorDirectory; if (!file_exists($vendorPath)) { mkdir($vendorPath, 0755, true); } $vendorMetaDataPath = $vendorPath . '/vendormetadata.php'; touch($vendorMetaDataPath); } } vendor/composer/installers/src/Composer/Installers/PortoInstaller.php 0000644 00000000260 15132754523 0022245 0 ustar 00 <?php namespace Composer\Installers; class PortoInstaller extends BaseInstaller { protected $locations = array( 'container' => 'app/Containers/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/SyliusInstaller.php 0000644 00000000245 15132754523 0022435 0 ustar 00 <?php namespace Composer\Installers; class SyliusInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/CraftInstaller.php 0000644 00000001446 15132754523 0022210 0 ustar 00 <?php namespace Composer\Installers; /** * Installer for Craft Plugins */ class CraftInstaller extends BaseInstaller { const NAME_PREFIX = 'craft'; const NAME_SUFFIX = 'plugin'; protected $locations = array( 'plugin' => 'craft/plugins/{$name}/', ); /** * Strip `craft-` prefix and/or `-plugin` suffix from package names * * @param array $vars * * @return array */ final public function inflectPackageVars($vars) { return $this->inflectPluginVars($vars); } private function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-' . self::NAME_SUFFIX . '$/i', '', $vars['name']); $vars['name'] = preg_replace('/^' . self::NAME_PREFIX . '-/i', '', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php 0000644 00000010251 15132754523 0022404 0 ustar 00 <?php namespace Composer\Installers; use Composer\Util\Filesystem; /** * Installer for Bitrix Framework. Supported types of extensions: * - `bitrix-d7-module` — copy the module to directory `bitrix/modules/<vendor>.<name>`. * - `bitrix-d7-component` — copy the component to directory `bitrix/components/<vendor>/<name>`. * - `bitrix-d7-template` — copy the template to directory `bitrix/templates/<vendor>_<name>`. * * You can set custom path to directory with Bitrix kernel in `composer.json`: * * ```json * { * "extra": { * "bitrix-dir": "s1/bitrix" * } * } * ``` * * @author Nik Samokhvalov <nik@samokhvalov.info> * @author Denis Kulichkin <onexhovia@gmail.com> */ class BitrixInstaller extends BaseInstaller { protected $locations = array( 'module' => '{$bitrix_dir}/modules/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'component' => '{$bitrix_dir}/components/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'theme' => '{$bitrix_dir}/templates/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'd7-module' => '{$bitrix_dir}/modules/{$vendor}.{$name}/', 'd7-component' => '{$bitrix_dir}/components/{$vendor}/{$name}/', 'd7-template' => '{$bitrix_dir}/templates/{$vendor}_{$name}/', ); /** * @var array Storage for informations about duplicates at all the time of installation packages. */ private static $checkedDuplicates = array(); /** * {@inheritdoc} */ public function inflectPackageVars($vars) { if ($this->composer->getPackage()) { $extra = $this->composer->getPackage()->getExtra(); if (isset($extra['bitrix-dir'])) { $vars['bitrix_dir'] = $extra['bitrix-dir']; } } if (!isset($vars['bitrix_dir'])) { $vars['bitrix_dir'] = 'bitrix'; } return parent::inflectPackageVars($vars); } /** * {@inheritdoc} */ protected function templatePath($path, array $vars = array()) { $templatePath = parent::templatePath($path, $vars); $this->checkDuplicates($templatePath, $vars); return $templatePath; } /** * Duplicates search packages. * * @param string $path * @param array $vars */ protected function checkDuplicates($path, array $vars = array()) { $packageType = substr($vars['type'], strlen('bitrix') + 1); $localDir = explode('/', $vars['bitrix_dir']); array_pop($localDir); $localDir[] = 'local'; $localDir = implode('/', $localDir); $oldPath = str_replace( array('{$bitrix_dir}', '{$name}'), array($localDir, $vars['name']), $this->locations[$packageType] ); if (in_array($oldPath, static::$checkedDuplicates)) { return; } if ($oldPath !== $path && file_exists($oldPath) && $this->io && $this->io->isInteractive()) { $this->io->writeError(' <error>Duplication of packages:</error>'); $this->io->writeError(' <info>Package ' . $oldPath . ' will be called instead package ' . $path . '</info>'); while (true) { switch ($this->io->ask(' <info>Delete ' . $oldPath . ' [y,n,?]?</info> ', '?')) { case 'y': $fs = new Filesystem(); $fs->removeDirectory($oldPath); break 2; case 'n': break 2; case '?': default: $this->io->writeError(array( ' y - delete package ' . $oldPath . ' and to continue with the installation', ' n - don\'t delete and to continue with the installation', )); $this->io->writeError(' ? - print help'); break; } } } static::$checkedDuplicates[] = $oldPath; } } vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php 0000644 00000000640 15132754523 0022365 0 ustar 00 <?php namespace Composer\Installers; class JoomlaInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', 'module' => 'modules/{$name}/', 'template' => 'templates/{$name}/', 'plugin' => 'plugins/{$name}/', 'library' => 'libraries/{$name}/', ); // TODO: Add inflector for mod_ and com_ names } vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php 0000644 00000000542 15132754523 0022703 0 ustar 00 <?php namespace Composer\Installers; /** * Class DolibarrInstaller * * @package Composer\Installers * @author Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr> */ class DolibarrInstaller extends BaseInstaller { //TODO: Add support for scripts and themes protected $locations = array( 'module' => 'htdocs/custom/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php 0000644 00000000405 15132754523 0022076 0 ustar 00 <?php namespace Composer\Installers; class PhpBBInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'ext/{$vendor}/{$name}/', 'language' => 'language/{$name}/', 'style' => 'styles/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php 0000644 00000001066 15132754523 0022674 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin installer for symfony 1.x * * @author Jérôme Tamarelle <jerome@tamarelle.net> */ class Symfony1Installer extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php 0000644 00000002264 15132754523 0022077 0 ustar 00 <?php namespace Composer\Installers; class SyDESInstaller extends BaseInstaller { protected $locations = array( 'module' => 'app/modules/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format module name. * * Strip `sydes-` prefix and a trailing '-theme' or '-module' from package name if present. * * {@inerhitDoc} */ public function inflectPackageVars($vars) { if ($vars['type'] == 'sydes-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'sydes-theme') { return $this->inflectThemeVars($vars); } return $vars; } public function inflectModuleVars($vars) { $vars['name'] = preg_replace('/(^sydes-|-module$)/i', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/(^sydes-|-theme$)/', '', $vars['name']); $vars['name'] = strtolower($vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php 0000644 00000002354 15132754523 0022676 0 ustar 00 <?php namespace Composer\Installers; class DokuWikiInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'lib/plugins/{$name}/', 'template' => 'lib/tpl/{$name}/', ); /** * Format package name. * * For package type dokuwiki-plugin, cut off a trailing '-plugin', * or leading dokuwiki_ if present. * * For package type dokuwiki-template, cut off a trailing '-template' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'dokuwiki-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'dokuwiki-template') { return $this->inflectTemplateVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-plugin$/', '', $vars['name']); $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); return $vars; } protected function inflectTemplateVars($vars) { $vars['name'] = preg_replace('/-template$/', '', $vars['name']); $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/WinterInstaller.php 0000644 00000002676 15132754523 0022427 0 ustar 00 <?php namespace Composer\Installers; class WinterInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/' ); /** * Format package name. * * For package type winter-plugin, cut off a trailing '-plugin' if present. * * For package type winter-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'winter-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'winter-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'winter-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectModuleVars($vars) { $vars['name'] = preg_replace('/^wn-|-module$/', '', $vars['name']); return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/^wn-|-plugin$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/^wn-|-theme$/', '', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/YawikInstaller.php 0000644 00000001246 15132754523 0022233 0 ustar 00 <?php /** * Created by PhpStorm. * User: cbleek * Date: 25.03.16 * Time: 20:55 */ namespace Composer\Installers; class YawikInstaller extends BaseInstaller { protected $locations = array( 'module' => 'module/{$name}/', ); /** * Format package name to CamelCase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/PantheonInstaller.php 0000644 00000000446 15132754523 0022724 0 ustar 00 <?php namespace Composer\Installers; class PantheonInstaller extends BaseInstaller { /** @var array<string, string> */ protected $locations = array( 'script' => 'web/private/scripts/quicksilver/{$name}', 'module' => 'web/private/scripts/quicksilver/{$name}', ); } vendor/composer/installers/src/Composer/Installers/ZendInstaller.php 0000644 00000000376 15132754523 0022052 0 ustar 00 <?php namespace Composer\Installers; class ZendInstaller extends BaseInstaller { protected $locations = array( 'library' => 'library/{$name}/', 'extra' => 'extras/library/{$name}/', 'module' => 'module/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php 0000644 00000002457 15132754523 0022230 0 ustar 00 <?php namespace Composer\Installers; class VgmcpInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'src/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/' ); /** * Format package name. * * For package type vgmcp-bundle, cut off a trailing '-bundle' if present. * * For package type vgmcp-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'vgmcp-bundle') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'vgmcp-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-bundle$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php 0000644 00000002377 15132754523 0022552 0 ustar 00 <?php namespace Composer\Installers; class OctoberInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/', 'theme' => 'themes/{$vendor}-{$name}/' ); /** * Format package name. * * For package type october-plugin, cut off a trailing '-plugin' if present. * * For package type october-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'october-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'october-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/^oc-|-plugin$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/^oc-|-theme$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } } vendor/composer/installers/src/Composer/Installers/Installer.php 0000644 00000024372 15132754523 0021233 0 ustar 00 <?php namespace Composer\Installers; use Composer\Composer; use Composer\Installer\BinaryInstaller; use Composer\Installer\LibraryInstaller; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use React\Promise\PromiseInterface; class Installer extends LibraryInstaller { /** * Package types to installer class map * * @var array */ private $supportedTypes = array( 'aimeos' => 'AimeosInstaller', 'asgard' => 'AsgardInstaller', 'attogram' => 'AttogramInstaller', 'agl' => 'AglInstaller', 'annotatecms' => 'AnnotateCmsInstaller', 'bitrix' => 'BitrixInstaller', 'bonefish' => 'BonefishInstaller', 'cakephp' => 'CakePHPInstaller', 'chef' => 'ChefInstaller', 'civicrm' => 'CiviCrmInstaller', 'ccframework' => 'ClanCatsFrameworkInstaller', 'cockpit' => 'CockpitInstaller', 'codeigniter' => 'CodeIgniterInstaller', 'concrete5' => 'Concrete5Installer', 'craft' => 'CraftInstaller', 'croogo' => 'CroogoInstaller', 'dframe' => 'DframeInstaller', 'dokuwiki' => 'DokuWikiInstaller', 'dolibarr' => 'DolibarrInstaller', 'decibel' => 'DecibelInstaller', 'drupal' => 'DrupalInstaller', 'elgg' => 'ElggInstaller', 'eliasis' => 'EliasisInstaller', 'ee3' => 'ExpressionEngineInstaller', 'ee2' => 'ExpressionEngineInstaller', 'ezplatform' => 'EzPlatformInstaller', 'fuel' => 'FuelInstaller', 'fuelphp' => 'FuelphpInstaller', 'grav' => 'GravInstaller', 'hurad' => 'HuradInstaller', 'tastyigniter' => 'TastyIgniterInstaller', 'imagecms' => 'ImageCMSInstaller', 'itop' => 'ItopInstaller', 'joomla' => 'JoomlaInstaller', 'kanboard' => 'KanboardInstaller', 'kirby' => 'KirbyInstaller', 'known' => 'KnownInstaller', 'kodicms' => 'KodiCMSInstaller', 'kohana' => 'KohanaInstaller', 'lms' => 'LanManagementSystemInstaller', 'laravel' => 'LaravelInstaller', 'lavalite' => 'LavaLiteInstaller', 'lithium' => 'LithiumInstaller', 'magento' => 'MagentoInstaller', 'majima' => 'MajimaInstaller', 'mantisbt' => 'MantisBTInstaller', 'mako' => 'MakoInstaller', 'maya' => 'MayaInstaller', 'mautic' => 'MauticInstaller', 'mediawiki' => 'MediaWikiInstaller', 'miaoxing' => 'MiaoxingInstaller', 'microweber' => 'MicroweberInstaller', 'modulework' => 'MODULEWorkInstaller', 'modx' => 'ModxInstaller', 'modxevo' => 'MODXEvoInstaller', 'moodle' => 'MoodleInstaller', 'october' => 'OctoberInstaller', 'ontowiki' => 'OntoWikiInstaller', 'oxid' => 'OxidInstaller', 'osclass' => 'OsclassInstaller', 'pxcms' => 'PxcmsInstaller', 'phpbb' => 'PhpBBInstaller', 'pimcore' => 'PimcoreInstaller', 'piwik' => 'PiwikInstaller', 'plentymarkets'=> 'PlentymarketsInstaller', 'ppi' => 'PPIInstaller', 'puppet' => 'PuppetInstaller', 'radphp' => 'RadPHPInstaller', 'phifty' => 'PhiftyInstaller', 'porto' => 'PortoInstaller', 'processwire' => 'ProcessWireInstaller', 'quicksilver' => 'PantheonInstaller', 'redaxo' => 'RedaxoInstaller', 'redaxo5' => 'Redaxo5Installer', 'reindex' => 'ReIndexInstaller', 'roundcube' => 'RoundcubeInstaller', 'shopware' => 'ShopwareInstaller', 'sitedirect' => 'SiteDirectInstaller', 'silverstripe' => 'SilverStripeInstaller', 'smf' => 'SMFInstaller', 'starbug' => 'StarbugInstaller', 'sydes' => 'SyDESInstaller', 'sylius' => 'SyliusInstaller', 'symfony1' => 'Symfony1Installer', 'tao' => 'TaoInstaller', 'thelia' => 'TheliaInstaller', 'tusk' => 'TuskInstaller', 'typo3-cms' => 'TYPO3CmsInstaller', 'typo3-flow' => 'TYPO3FlowInstaller', 'userfrosting' => 'UserFrostingInstaller', 'vanilla' => 'VanillaInstaller', 'whmcs' => 'WHMCSInstaller', 'winter' => 'WinterInstaller', 'wolfcms' => 'WolfCMSInstaller', 'wordpress' => 'WordPressInstaller', 'yawik' => 'YawikInstaller', 'zend' => 'ZendInstaller', 'zikula' => 'ZikulaInstaller', 'prestashop' => 'PrestashopInstaller' ); /** * Installer constructor. * * Disables installers specified in main composer extra installer-disable * list * * @param IOInterface $io * @param Composer $composer * @param string $type * @param Filesystem|null $filesystem * @param BinaryInstaller|null $binaryInstaller */ public function __construct( IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null, BinaryInstaller $binaryInstaller = null ) { parent::__construct($io, $composer, $type, $filesystem, $binaryInstaller); $this->removeDisabledInstallers(); } /** * {@inheritDoc} */ public function getInstallPath(PackageInterface $package) { $type = $package->getType(); $frameworkType = $this->findFrameworkType($type); if ($frameworkType === false) { throw new \InvalidArgumentException( 'Sorry the package type of this package is not yet supported.' ); } $class = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; $installer = new $class($package, $this->composer, $this->getIO()); return $installer->getInstallPath($package, $frameworkType); } public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { $installPath = $this->getPackageBasePath($package); $io = $this->io; $outputStatus = function () use ($io, $installPath) { $io->write(sprintf('Deleting %s - %s', $installPath, !file_exists($installPath) ? '<comment>deleted</comment>' : '<error>not deleted</error>')); }; $promise = parent::uninstall($repo, $package); // Composer v2 might return a promise here if ($promise instanceof PromiseInterface) { return $promise->then($outputStatus); } // If not, execute the code right away as parent::uninstall executed synchronously (composer v1, or v2 without async) $outputStatus(); return null; } /** * {@inheritDoc} */ public function supports($packageType) { $frameworkType = $this->findFrameworkType($packageType); if ($frameworkType === false) { return false; } $locationPattern = $this->getLocationPattern($frameworkType); return preg_match('#' . $frameworkType . '-' . $locationPattern . '#', $packageType, $matches) === 1; } /** * Finds a supported framework type if it exists and returns it * * @param string $type * @return string|false */ protected function findFrameworkType($type) { krsort($this->supportedTypes); foreach ($this->supportedTypes as $key => $val) { if ($key === substr($type, 0, strlen($key))) { return substr($type, 0, strlen($key)); } } return false; } /** * Get the second part of the regular expression to check for support of a * package type * * @param string $frameworkType * @return string */ protected function getLocationPattern($frameworkType) { $pattern = false; if (!empty($this->supportedTypes[$frameworkType])) { $frameworkClass = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; /** @var BaseInstaller $framework */ $framework = new $frameworkClass(null, $this->composer, $this->getIO()); $locations = array_keys($framework->getLocations()); $pattern = $locations ? '(' . implode('|', $locations) . ')' : false; } return $pattern ? : '(\w+)'; } /** * Get I/O object * * @return IOInterface */ private function getIO() { return $this->io; } /** * Look for installers set to be disabled in composer's extra config and * remove them from the list of supported installers. * * Globals: * - true, "all", and "*" - disable all installers. * - false - enable all installers (useful with * wikimedia/composer-merge-plugin or similar) * * @return void */ protected function removeDisabledInstallers() { $extra = $this->composer->getPackage()->getExtra(); if (!isset($extra['installer-disable']) || $extra['installer-disable'] === false) { // No installers are disabled return; } // Get installers to disable $disable = $extra['installer-disable']; // Ensure $disabled is an array if (!is_array($disable)) { $disable = array($disable); } // Check which installers should be disabled $all = array(true, "all", "*"); $intersect = array_intersect($all, $disable); if (!empty($intersect)) { // Disable all installers $this->supportedTypes = array(); } else { // Disable specified installers foreach ($disable as $key => $installer) { if (is_string($installer) && key_exists($installer, $this->supportedTypes)) { unset($this->supportedTypes[$installer]); } } } } } vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php 0000644 00000000444 15132754523 0022533 0 ustar 00 <?php namespace Composer\Installers; class ImageCMSInstaller extends BaseInstaller { protected $locations = array( 'template' => 'templates/{$name}/', 'module' => 'application/modules/{$name}/', 'library' => 'application/libraries/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php 0000644 00000000450 15132754523 0022664 0 ustar 00 <?php namespace Composer\Installers; /** * * Installer for kanboard plugins * * kanboard.net * * Class KanboardInstaller * @package Composer\Installers */ class KanboardInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MiaoxingInstaller.php 0000644 00000000252 15132754523 0022716 0 ustar 00 <?php namespace Composer\Installers; class MiaoxingInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/CiviCrmInstaller.php 0000644 00000000243 15132754523 0022477 0 ustar 00 <?php namespace Composer\Installers; class CiviCrmInstaller extends BaseInstaller { protected $locations = array( 'ext' => 'ext/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php 0000644 00000001506 15132754523 0022027 0 ustar 00 <?php namespace Composer\Installers; class WHMCSInstaller extends BaseInstaller { protected $locations = array( 'addons' => 'modules/addons/{$vendor}_{$name}/', 'fraud' => 'modules/fraud/{$vendor}_{$name}/', 'gateways' => 'modules/gateways/{$vendor}_{$name}/', 'notifications' => 'modules/notifications/{$vendor}_{$name}/', 'registrars' => 'modules/registrars/{$vendor}_{$name}/', 'reports' => 'modules/reports/{$vendor}_{$name}/', 'security' => 'modules/security/{$vendor}_{$name}/', 'servers' => 'modules/servers/{$vendor}_{$name}/', 'social' => 'modules/social/{$vendor}_{$name}/', 'support' => 'modules/support/{$vendor}_{$name}/', 'templates' => 'templates/{$vendor}_{$name}/', 'includes' => 'includes/{$vendor}_{$name}/' ); } vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php 0000644 00000000256 15132754523 0022777 0 ustar 00 <?php namespace Composer\Installers; class MODULEWorkInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/KnownInstaller.php 0000644 00000000411 15132754523 0022234 0 ustar 00 <?php namespace Composer\Installers; class KnownInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'IdnoPlugins/{$name}/', 'theme' => 'Themes/{$name}/', 'console' => 'ConsolePlugins/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MakoInstaller.php 0000644 00000000253 15132754523 0022033 0 ustar 00 <?php namespace Composer\Installers; class MakoInstaller extends BaseInstaller { protected $locations = array( 'package' => 'app/packages/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/OsclassInstaller.php 0000644 00000000447 15132754523 0022560 0 ustar 00 <?php namespace Composer\Installers; class OsclassInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'oc-content/plugins/{$name}/', 'theme' => 'oc-content/themes/{$name}/', 'language' => 'oc-content/languages/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php 0000644 00000000336 15132754523 0022561 0 ustar 00 <?php namespace Composer\Installers; class LithiumInstaller extends BaseInstaller { protected $locations = array( 'library' => 'libraries/{$name}/', 'source' => 'libraries/_source/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php 0000644 00000002422 15132754523 0023007 0 ustar 00 <?php namespace Composer\Installers; class MediaWikiInstaller extends BaseInstaller { protected $locations = array( 'core' => 'core/', 'extension' => 'extensions/{$name}/', 'skin' => 'skins/{$name}/', ); /** * Format package name. * * For package type mediawiki-extension, cut off a trailing '-extension' if present and transform * to CamelCase keeping existing uppercase chars. * * For package type mediawiki-skin, cut off a trailing '-skin' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'mediawiki-extension') { return $this->inflectExtensionVars($vars); } if ($vars['type'] === 'mediawiki-skin') { return $this->inflectSkinVars($vars); } return $vars; } protected function inflectExtensionVars($vars) { $vars['name'] = preg_replace('/-extension$/', '', $vars['name']); $vars['name'] = str_replace('-', ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectSkinVars($vars) { $vars['name'] = preg_replace('/-skin$/', '', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php 0000644 00000000767 15132754523 0022406 0 ustar 00 <?php namespace Composer\Installers; class CroogoInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'Plugin/{$name}/', 'theme' => 'View/Themed/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(str_replace(array('-', '_'), ' ', $vars['name'])); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php 0000644 00000006054 15132754523 0022370 0 ustar 00 <?php namespace Composer\Installers; class MoodleInstaller extends BaseInstaller { protected $locations = array( 'mod' => 'mod/{$name}/', 'admin_report' => 'admin/report/{$name}/', 'atto' => 'lib/editor/atto/plugins/{$name}/', 'tool' => 'admin/tool/{$name}/', 'assignment' => 'mod/assignment/type/{$name}/', 'assignsubmission' => 'mod/assign/submission/{$name}/', 'assignfeedback' => 'mod/assign/feedback/{$name}/', 'auth' => 'auth/{$name}/', 'availability' => 'availability/condition/{$name}/', 'block' => 'blocks/{$name}/', 'booktool' => 'mod/book/tool/{$name}/', 'cachestore' => 'cache/stores/{$name}/', 'cachelock' => 'cache/locks/{$name}/', 'calendartype' => 'calendar/type/{$name}/', 'fileconverter' => 'files/converter/{$name}/', 'format' => 'course/format/{$name}/', 'coursereport' => 'course/report/{$name}/', 'customcertelement' => 'mod/customcert/element/{$name}/', 'datafield' => 'mod/data/field/{$name}/', 'datapreset' => 'mod/data/preset/{$name}/', 'editor' => 'lib/editor/{$name}/', 'enrol' => 'enrol/{$name}/', 'filter' => 'filter/{$name}/', 'gradeexport' => 'grade/export/{$name}/', 'gradeimport' => 'grade/import/{$name}/', 'gradereport' => 'grade/report/{$name}/', 'gradingform' => 'grade/grading/form/{$name}/', 'local' => 'local/{$name}/', 'logstore' => 'admin/tool/log/store/{$name}/', 'ltisource' => 'mod/lti/source/{$name}/', 'ltiservice' => 'mod/lti/service/{$name}/', 'message' => 'message/output/{$name}/', 'mnetservice' => 'mnet/service/{$name}/', 'plagiarism' => 'plagiarism/{$name}/', 'portfolio' => 'portfolio/{$name}/', 'qbehaviour' => 'question/behaviour/{$name}/', 'qformat' => 'question/format/{$name}/', 'qtype' => 'question/type/{$name}/', 'quizaccess' => 'mod/quiz/accessrule/{$name}/', 'quiz' => 'mod/quiz/report/{$name}/', 'report' => 'report/{$name}/', 'repository' => 'repository/{$name}/', 'scormreport' => 'mod/scorm/report/{$name}/', 'search' => 'search/engine/{$name}/', 'theme' => 'theme/{$name}/', 'tinymce' => 'lib/editor/tinymce/plugins/{$name}/', 'profilefield' => 'user/profile/field/{$name}/', 'webservice' => 'webservice/{$name}/', 'workshopallocation' => 'mod/workshop/allocation/{$name}/', 'workshopeval' => 'mod/workshop/eval/{$name}/', 'workshopform' => 'mod/workshop/form/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/SMFInstaller.php 0000644 00000000312 15132754523 0021565 0 ustar 00 <?php namespace Composer\Installers; class SMFInstaller extends BaseInstaller { protected $locations = array( 'module' => 'Sources/{$name}/', 'theme' => 'Themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MauticInstaller.php 0000644 00000002225 15132754523 0022367 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class MauticInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'themes/{$name}/', 'core' => 'app/', ); private function getDirectoryName() { $extra = $this->package->getExtra(); if (!empty($extra['install-directory-name'])) { return $extra['install-directory-name']; } return $this->toCamelCase($this->package->getPrettyName()); } /** * @param string $packageName * * @return string */ private function toCamelCase($packageName) { return str_replace(' ', '', ucwords(str_replace('-', ' ', basename($packageName)))); } /** * Format package name of mautic-plugins to CamelCase */ public function inflectPackageVars($vars) { if ($vars['type'] == 'mautic-plugin' || $vars['type'] == 'mautic-theme') { $directoryName = $this->getDirectoryName(); $vars['name'] = $directoryName; } return $vars; } } vendor/composer/installers/src/Composer/Installers/PPIInstaller.php 0000644 00000000244 15132754523 0021574 0 ustar 00 <?php namespace Composer\Installers; class PPIInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/HuradInstaller.php 0000644 00000001276 15132754523 0022215 0 ustar 00 <?php namespace Composer\Installers; class HuradInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } } vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php 0000644 00000000344 15132754523 0022646 0 ustar 00 <?php namespace Composer\Installers; class LavaLiteInstaller extends BaseInstaller { protected $locations = array( 'package' => 'packages/{$vendor}/{$name}/', 'theme' => 'public/themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php 0000644 00000000436 15132754523 0023363 0 ustar 00 <?php namespace Composer\Installers; class AnnotateCmsInstaller extends BaseInstaller { protected $locations = array( 'module' => 'addons/modules/{$name}/', 'component' => 'addons/components/{$name}/', 'service' => 'addons/services/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MantisBTInstaller.php 0000644 00000001110 15132754523 0022616 0 ustar 00 <?php namespace Composer\Installers; use Composer\DependencyResolver\Pool; class MantisBTInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php 0000644 00000001271 15132754523 0022230 0 ustar 00 <?php namespace Composer\Installers; /** * Class PiwikInstaller * * @package Composer\Installers */ class PiwikInstaller extends BaseInstaller { /** * @var array */ protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/MajimaInstaller.php 0000644 00000001502 15132754523 0022340 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin/theme installer for majima * @author David Neustadt */ class MajimaInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Transforms the names * @param array $vars * @return array */ public function inflectPackageVars($vars) { return $this->correctPluginName($vars); } /** * Change hyphenated names to camelcase * @param array $vars * @return array */ private function correctPluginName($vars) { $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); $vars['name'] = ucfirst($camelCasedName); return $vars; } } vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php 0000644 00000003156 15132754523 0022741 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin/theme installer for shopware * @author Benjamin Boit */ class ShopwareInstaller extends BaseInstaller { protected $locations = array( 'backend-plugin' => 'engine/Shopware/Plugins/Local/Backend/{$name}/', 'core-plugin' => 'engine/Shopware/Plugins/Local/Core/{$name}/', 'frontend-plugin' => 'engine/Shopware/Plugins/Local/Frontend/{$name}/', 'theme' => 'templates/{$name}/', 'plugin' => 'custom/plugins/{$name}/', 'frontend-theme' => 'themes/Frontend/{$name}/', ); /** * Transforms the names * @param array $vars * @return array */ public function inflectPackageVars($vars) { if ($vars['type'] === 'shopware-theme') { return $this->correctThemeName($vars); } return $this->correctPluginName($vars); } /** * Changes the name to a camelcased combination of vendor and name * @param array $vars * @return array */ private function correctPluginName($vars) { $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); $vars['name'] = ucfirst($vars['vendor']) . ucfirst($camelCasedName); return $vars; } /** * Changes the name to a underscore separated name * @param array $vars * @return array */ private function correctThemeName($vars) { $vars['name'] = str_replace('-', '_', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php 0000644 00000000405 15132754523 0022223 0 ustar 00 <?php namespace Composer\Installers; class KirbyInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'site/plugins/{$name}/', 'field' => 'site/fields/{$name}/', 'tag' => 'site/tags/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/Redaxo5Installer.php 0000644 00000000404 15132754523 0022451 0 ustar 00 <?php namespace Composer\Installers; class Redaxo5Installer extends BaseInstaller { protected $locations = array( 'addon' => 'redaxo/src/addons/{$name}/', 'bestyle-plugin' => 'redaxo/src/addons/be_style/plugins/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php 0000644 00000000251 15132754523 0022720 0 ustar 00 <?php namespace Composer\Installers; class AttogramInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php 0000644 00000001334 15132754523 0024432 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class ExpressionEngineInstaller extends BaseInstaller { protected $locations = array(); private $ee2Locations = array( 'addon' => 'system/expressionengine/third_party/{$name}/', 'theme' => 'themes/third_party/{$name}/', ); private $ee3Locations = array( 'addon' => 'system/user/addons/{$name}/', 'theme' => 'themes/user/{$name}/', ); public function getInstallPath(PackageInterface $package, $frameworkType = '') { $version = "{$frameworkType}Locations"; $this->locations = $this->$version; return parent::getInstallPath($package, $frameworkType); } } vendor/composer/installers/src/Composer/Installers/TaoInstaller.php 0000644 00000001423 15132754523 0021667 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle TAO extensions. */ class TaoInstaller extends BaseInstaller { const EXTRA_TAO_EXTENSION_NAME = 'tao-extension-name'; protected $locations = array( 'extension' => '{$name}' ); public function inflectPackageVars($vars) { $extra = $this->package->getExtra(); if (array_key_exists(self::EXTRA_TAO_EXTENSION_NAME, $extra)) { $vars['name'] = $extra[self::EXTRA_TAO_EXTENSION_NAME]; return $vars; } $vars['name'] = str_replace('extension-', '', $vars['name']); $vars['name'] = str_replace('-', ' ', $vars['name']); $vars['name'] = lcfirst(str_replace(' ', '', ucwords($vars['name']))); return $vars; } } vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php 0000644 00000000253 15132754523 0022532 0 ustar 00 <?php namespace Composer\Installers; class LaravelInstaller extends BaseInstaller { protected $locations = array( 'library' => 'libraries/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php 0000644 00000000711 15132754523 0023071 0 ustar 00 <?php namespace Composer\Installers; class RoundcubeInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Lowercase name and changes the name to a underscores * * @param array $vars * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(str_replace('-', '_', $vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/UserFrostingInstaller.php 0000644 00000000265 15132754523 0023601 0 ustar 00 <?php namespace Composer\Installers; class UserFrostingInstaller extends BaseInstaller { protected $locations = array( 'sprinkle' => 'app/sprinkles/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php 0000644 00000000326 15132754523 0024513 0 ustar 00 <?php namespace Composer\Installers; class ClanCatsFrameworkInstaller extends BaseInstaller { protected $locations = array( 'ship' => 'CCF/orbit/{$name}/', 'theme' => 'CCF/app/themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php 0000644 00000001326 15132754523 0025062 0 ustar 00 <?php namespace Composer\Installers; class LanManagementSystemInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'template' => 'templates/{$name}/', 'document-template' => 'documents/templates/{$name}/', 'userpanel-module' => 'userpanel/modules/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php 0000644 00000010340 15132754523 0023240 0 ustar 00 <?php namespace Composer\Installers; class MicroweberInstaller extends BaseInstaller { protected $locations = array( 'module' => 'userfiles/modules/{$install_item_dir}/', 'module-skin' => 'userfiles/modules/{$install_item_dir}/templates/', 'template' => 'userfiles/templates/{$install_item_dir}/', 'element' => 'userfiles/elements/{$install_item_dir}/', 'vendor' => 'vendor/{$install_item_dir}/', 'components' => 'components/{$install_item_dir}/' ); /** * Format package name. * * For package type microweber-module, cut off a trailing '-module' if present * * For package type microweber-template, cut off a trailing '-template' if present. * */ public function inflectPackageVars($vars) { if ($this->package->getTargetDir()) { $vars['install_item_dir'] = $this->package->getTargetDir(); } else { $vars['install_item_dir'] = $vars['name']; if ($vars['type'] === 'microweber-template') { return $this->inflectTemplateVars($vars); } if ($vars['type'] === 'microweber-templates') { return $this->inflectTemplatesVars($vars); } if ($vars['type'] === 'microweber-core') { return $this->inflectCoreVars($vars); } if ($vars['type'] === 'microweber-adapter') { return $this->inflectCoreVars($vars); } if ($vars['type'] === 'microweber-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'microweber-modules') { return $this->inflectModulesVars($vars); } if ($vars['type'] === 'microweber-skin') { return $this->inflectSkinVars($vars); } if ($vars['type'] === 'microweber-element' or $vars['type'] === 'microweber-elements') { return $this->inflectElementVars($vars); } } return $vars; } protected function inflectTemplateVars($vars) { $vars['install_item_dir'] = preg_replace('/-template$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/template-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectTemplatesVars($vars) { $vars['install_item_dir'] = preg_replace('/-templates$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/templates-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectCoreVars($vars) { $vars['install_item_dir'] = preg_replace('/-providers$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-provider$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-adapter$/', '', $vars['install_item_dir']); return $vars; } protected function inflectModuleVars($vars) { $vars['install_item_dir'] = preg_replace('/-module$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/module-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectModulesVars($vars) { $vars['install_item_dir'] = preg_replace('/-modules$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/modules-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectSkinVars($vars) { $vars['install_item_dir'] = preg_replace('/-skin$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/skin-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectElementVars($vars) { $vars['install_item_dir'] = preg_replace('/-elements$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/elements-$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-element$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/element-$/', '', $vars['install_item_dir']); return $vars; } } vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php 0000644 00000000413 15132754523 0022364 0 ustar 00 <?php namespace Composer\Installers; class RedaxoInstaller extends BaseInstaller { protected $locations = array( 'addon' => 'redaxo/include/addons/{$name}/', 'bestyle-plugin' => 'redaxo/include/addons/be_style/plugins/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/FuelInstaller.php 0000644 00000000417 15132754523 0022041 0 ustar 00 <?php namespace Composer\Installers; class FuelInstaller extends BaseInstaller { protected $locations = array( 'module' => 'fuel/app/modules/{$name}/', 'package' => 'fuel/packages/{$name}/', 'theme' => 'fuel/app/themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/Plugin.php 0000644 00000001214 15132754523 0020522 0 ustar 00 <?php namespace Composer\Installers; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; class Plugin implements PluginInterface { private $installer; public function activate(Composer $composer, IOInterface $io) { $this->installer = new Installer($io, $composer); $composer->getInstallationManager()->addInstaller($this->installer); } public function deactivate(Composer $composer, IOInterface $io) { $composer->getInstallationManager()->removeInstaller($this->installer); } public function uninstall(Composer $composer, IOInterface $io) { } } vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php 0000644 00000000325 15132754523 0022532 0 ustar 00 <?php namespace Composer\Installers; class VanillaInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/AglInstaller.php 0000644 00000000711 15132754523 0021646 0 ustar 00 <?php namespace Composer\Installers; class AglInstaller extends BaseInstaller { protected $locations = array( 'module' => 'More/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = preg_replace_callback('/(?:^|_|-)(.?)/', function ($matches) { return strtoupper($matches[1]); }, $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php 0000644 00000000247 15132754523 0022350 0 ustar 00 <?php namespace Composer\Installers; class KohanaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php 0000644 00000000251 15132754523 0022417 0 ustar 00 <?php namespace Composer\Installers; class PuppetInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php 0000644 00000000322 15132754523 0023271 0 ustar 00 <?php namespace Composer\Installers; class PrestashopInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/SiteDirectInstaller.php 0000644 00000001216 15132754523 0023203 0 ustar 00 <?php namespace Composer\Installers; class SiteDirectInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/' ); public function inflectPackageVars($vars) { return $this->parseVars($vars); } protected function parseVars($vars) { $vars['vendor'] = strtolower($vars['vendor']) == 'sitedirect' ? 'SiteDirect' : $vars['vendor']; $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php 0000644 00000001223 15132754523 0022220 0 ustar 00 <?php namespace Composer\Installers; class RadPHPInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'src/{$name}/' ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } } vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php 0000644 00000000250 15132754523 0022356 0 ustar 00 <?php namespace Composer\Installers; class AimeosInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'ext/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php 0000644 00000003446 15132754523 0022366 0 ustar 00 <?php namespace Composer\Installers; use Composer\DependencyResolver\Pool; use Composer\Semver\Constraint\Constraint; class CakePHPInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'Plugin/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { if ($this->matchesCakeVersion('>=', '3.0.0')) { return $vars; } $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } /** * Change the default plugin location when cakephp >= 3.0 */ public function getLocations() { if ($this->matchesCakeVersion('>=', '3.0.0')) { $this->locations['plugin'] = $this->composer->getConfig()->get('vendor-dir') . '/{$vendor}/{$name}/'; } return $this->locations; } /** * Check if CakePHP version matches against a version * * @param string $matcher * @param string $version * @return bool * @phpstan-param Constraint::STR_OP_* $matcher */ protected function matchesCakeVersion($matcher, $version) { $repositoryManager = $this->composer->getRepositoryManager(); if (! $repositoryManager) { return false; } $repos = $repositoryManager->getLocalRepository(); if (!$repos) { return false; } return $repos->findPackage('cakephp/cakephp', new Constraint($matcher, $version)) !== null; } } vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php 0000644 00000001231 15132754523 0022535 0 ustar 00 <?php namespace Composer\Installers; class CockpitInstaller extends BaseInstaller { protected $locations = array( 'module' => 'cockpit/modules/addons/{$name}/', ); /** * Format module name. * * Strip `module-` prefix from package name. * * {@inheritDoc} */ public function inflectPackageVars($vars) { if ($vars['type'] == 'cockpit-module') { return $this->inflectModuleVars($vars); } return $vars; } public function inflectModuleVars($vars) { $vars['name'] = ucfirst(preg_replace('/cockpit-/i', '', $vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php 0000644 00000000524 15132754523 0023075 0 ustar 00 <?php namespace Composer\Installers; class WordPressInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'wp-content/plugins/{$name}/', 'theme' => 'wp-content/themes/{$name}/', 'muplugin' => 'wp-content/mu-plugins/{$name}/', 'dropin' => 'wp-content/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php 0000644 00000001543 15132754523 0022376 0 ustar 00 <?php namespace Composer\Installers; class DrupalInstaller extends BaseInstaller { protected $locations = array( 'core' => 'core/', 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', 'library' => 'libraries/{$name}/', 'profile' => 'profiles/{$name}/', 'database-driver' => 'drivers/lib/Drupal/Driver/Database/{$name}/', 'drush' => 'drush/{$name}/', 'custom-theme' => 'themes/custom/{$name}/', 'custom-module' => 'modules/custom/{$name}/', 'custom-profile' => 'profiles/custom/{$name}/', 'drupal-multisite' => 'sites/{$name}/', 'console' => 'console/{$name}/', 'console-language' => 'console/language/{$name}/', 'config' => 'config/sync/', ); } vendor/composer/installers/src/Composer/Installers/ModxInstaller.php 0000644 00000000364 15132754523 0022056 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle MODX specifics when installing packages. */ class ModxInstaller extends BaseInstaller { protected $locations = array( 'extra' => 'core/packages/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/TuskInstaller.php 0000644 00000000640 15132754523 0022072 0 ustar 00 <?php namespace Composer\Installers; /** * Composer installer for 3rd party Tusk utilities * @author Drew Ewing <drew@phenocode.com> */ class TuskInstaller extends BaseInstaller { protected $locations = array( 'task' => '.tusk/tasks/{$name}/', 'command' => '.tusk/commands/{$name}/', 'asset' => 'assets/tusk/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php 0000644 00000001311 15132754523 0024002 0 ustar 00 <?php namespace Composer\Installers; class PlentymarketsInstaller extends BaseInstaller { protected $locations = array( 'plugin' => '{$name}/' ); /** * Remove hyphen, "plugin" and format to camelcase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = explode("-", $vars['name']); foreach ($vars['name'] as $key => $name) { $vars['name'][$key] = ucfirst($vars['name'][$key]); if (strcasecmp($name, "Plugin") == 0) { unset($vars['name'][$key]); } } $vars['name'] = implode("",$vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php 0000644 00000000400 15132754523 0022401 0 ustar 00 <?php namespace Composer\Installers; class PhiftyInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'bundles/{$name}/', 'library' => 'libraries/{$name}/', 'framework' => 'frameworks/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php 0000644 00000002300 15132754523 0022645 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle TYPO3 Flow specifics when installing packages. */ class TYPO3FlowInstaller extends BaseInstaller { protected $locations = array( 'package' => 'Packages/Application/{$name}/', 'framework' => 'Packages/Framework/{$name}/', 'plugin' => 'Packages/Plugins/{$name}/', 'site' => 'Packages/Sites/{$name}/', 'boilerplate' => 'Packages/Boilerplates/{$name}/', 'build' => 'Build/{$name}/', ); /** * Modify the package name to be a TYPO3 Flow style key. * * @param array $vars * @return array */ public function inflectPackageVars($vars) { $autoload = $this->package->getAutoload(); if (isset($autoload['psr-0']) && is_array($autoload['psr-0'])) { $namespace = key($autoload['psr-0']); $vars['name'] = str_replace('\\', '.', $namespace); } if (isset($autoload['psr-4']) && is_array($autoload['psr-4'])) { $namespace = key($autoload['psr-4']); $vars['name'] = rtrim(str_replace('\\', '.', $namespace), '.'); } return $vars; } } vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php 0000644 00000000267 15132754523 0022706 0 ustar 00 <?php namespace Composer\Installers; class BonefishInstaller extends BaseInstaller { protected $locations = array( 'package' => 'Packages/{$vendor}/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php 0000644 00000000333 15132754523 0022374 0 ustar 00 <?php namespace Composer\Installers; class KodiCMSInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'cms/plugins/{$name}/', 'media' => 'cms/media/vendor/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/EzPlatformInstaller.php 0000644 00000000354 15132754523 0023231 0 ustar 00 <?php namespace Composer\Installers; class EzPlatformInstaller extends BaseInstaller { protected $locations = array( 'meta-assets' => 'web/assets/ezplatform/', 'assets' => 'web/assets/ezplatform/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php 0000644 00000000575 15132754523 0022474 0 ustar 00 <?php namespace Composer\Installers; /** * Extension installer for TYPO3 CMS * * @deprecated since 1.0.25, use https://packagist.org/packages/typo3/cms-composer-installers instead * * @author Sascha Egerer <sascha.egerer@dkd.de> */ class TYPO3CmsInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'typo3conf/ext/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php 0000644 00000000465 15132754523 0023345 0 ustar 00 <?php namespace Composer\Installers; class CodeIgniterInstaller extends BaseInstaller { protected $locations = array( 'library' => 'application/libraries/{$name}/', 'third-party' => 'application/third_party/{$name}/', 'module' => 'application/modules/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php 0000644 00000001553 15132754523 0023576 0 ustar 00 <?php namespace Composer\Installers; class TastyIgniterInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format package name. * * Cut off leading 'ti-ext-' or 'ti-theme-' if present. * Strip vendor name of characters that is not alphanumeric or an underscore * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'tastyigniter-extension') { $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); $vars['name'] = preg_replace('/^ti-ext-/', '', $vars['name']); } if ($vars['type'] === 'tastyigniter-theme') { $vars['name'] = preg_replace('/^ti-theme-/', '', $vars['name']); } return $vars; } } vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php 0000644 00000000324 15132754523 0022501 0 ustar 00 <?php namespace Composer\Installers; class ReIndexInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'themes/{$name}/', 'plugin' => 'plugins/{$name}/' ); } vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php 0000644 00000000257 15132754523 0022553 0 ustar 00 <?php namespace Composer\Installers; class FuelphpInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/ChefInstaller.php 0000644 00000000336 15132754523 0022013 0 ustar 00 <?php namespace Composer\Installers; class ChefInstaller extends BaseInstaller { protected $locations = array( 'cookbook' => 'Chef/{$vendor}/{$name}/', 'role' => 'Chef/roles/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/MayaInstaller.php 0000644 00000001427 15132754523 0022037 0 ustar 00 <?php namespace Composer\Installers; class MayaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); /** * Format package name. * * For package type maya-module, cut off a trailing '-module' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'maya-module') { return $this->inflectModuleVars($vars); } return $vars; } protected function inflectModuleVars($vars) { $vars['name'] = preg_replace('/-module$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/PxcmsInstaller.php 0000644 00000003734 15132754523 0022245 0 ustar 00 <?php namespace Composer\Installers; class PxcmsInstaller extends BaseInstaller { protected $locations = array( 'module' => 'app/Modules/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format package name. * * @param array $vars * * @return array */ public function inflectPackageVars($vars) { if ($vars['type'] === 'pxcms-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'pxcms-theme') { return $this->inflectThemeVars($vars); } return $vars; } /** * For package type pxcms-module, cut off a trailing '-plugin' if present. * * return string */ protected function inflectModuleVars($vars) { $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) $vars['name'] = str_replace('module-', '', $vars['name']); // strip out module- $vars['name'] = preg_replace('/-module$/', '', $vars['name']); // strip out -module $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s $vars['name'] = ucwords($vars['name']); // make module name camelcased return $vars; } /** * For package type pxcms-module, cut off a trailing '-plugin' if present. * * return string */ protected function inflectThemeVars($vars) { $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) $vars['name'] = str_replace('theme-', '', $vars['name']); // strip out theme- $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); // strip out -theme $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s $vars['name'] = ucwords($vars['name']); // make module name camelcased return $vars; } } vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php 0000644 00000002456 15132754523 0022354 0 ustar 00 <?php namespace Composer\Installers; class AsgardInstaller extends BaseInstaller { protected $locations = array( 'module' => 'Modules/{$name}/', 'theme' => 'Themes/{$name}/' ); /** * Format package name. * * For package type asgard-module, cut off a trailing '-plugin' if present. * * For package type asgard-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'asgard-module') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'asgard-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-module$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php 0000644 00000000272 15132754523 0022474 0 ustar 00 <?php namespace Composer\Installers; class DecibelInstaller extends BaseInstaller { /** @var array */ protected $locations = array( 'app' => 'app/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php 0000644 00000001040 15132754523 0022535 0 ustar 00 <?php namespace Composer\Installers; class PimcoreInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php 0000644 00000000556 15132754523 0023001 0 ustar 00 <?php namespace Composer\Installers; class Concrete5Installer extends BaseInstaller { protected $locations = array( 'core' => 'concrete/', 'block' => 'application/blocks/{$name}/', 'package' => 'packages/{$name}/', 'theme' => 'application/themes/{$name}/', 'update' => 'updates/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/ElggInstaller.php 0000644 00000000241 15132754523 0022017 0 ustar 00 <?php namespace Composer\Installers; class ElggInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'mod/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php 0000644 00000000255 15132754523 0022420 0 ustar 00 <?php namespace Composer\Installers; class WolfCMSInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'wolf/plugins/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php 0000644 00000002127 15132754523 0023601 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class SilverStripeInstaller extends BaseInstaller { protected $locations = array( 'module' => '{$name}/', 'theme' => 'themes/{$name}/', ); /** * Return the install path based on package type. * * Relies on built-in BaseInstaller behaviour with one exception: silverstripe/framework * must be installed to 'sapphire' and not 'framework' if the version is <3.0.0 * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { if ( $package->getName() == 'silverstripe/framework' && preg_match('/^\d+\.\d+\.\d+/', $package->getVersion()) && version_compare($package->getVersion(), '2.999.999') < 0 ) { return $this->templatePath($this->locations['module'], array('name' => 'sapphire')); } return parent::getInstallPath($package, $frameworkType); } } vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php 0000644 00000000604 15132754523 0022352 0 ustar 00 <?php namespace Composer\Installers; class TheliaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'local/modules/{$name}/', 'frontoffice-template' => 'templates/frontOffice/{$name}/', 'backoffice-template' => 'templates/backOffice/{$name}/', 'email-template' => 'templates/email/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/DframeInstaller.php 0000644 00000000263 15132754523 0022343 0 ustar 00 <?php namespace Composer\Installers; class DframeInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/ItopInstaller.php 0000644 00000000256 15132754523 0022062 0 ustar 00 <?php namespace Composer\Installers; class ItopInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$name}/', ); } vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php 0000644 00000001324 15132754523 0022707 0 ustar 00 <?php namespace Composer\Installers; class OntoWikiInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$name}/', 'theme' => 'extensions/themes/{$name}/', 'translation' => 'extensions/translations/{$name}/', ); /** * Format package name to lower case and remove ".ontowiki" suffix */ public function inflectPackageVars($vars) { $vars['name'] = strtolower($vars['name']); $vars['name'] = preg_replace('/.ontowiki$/', '', $vars['name']); $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = preg_replace('/-translation$/', '', $vars['name']); return $vars; } } vendor/composer/installers/src/Composer/Installers/BaseInstaller.php 0000644 00000007753 15132754523 0022032 0 ustar 00 <?php namespace Composer\Installers; use Composer\IO\IOInterface; use Composer\Composer; use Composer\Package\PackageInterface; abstract class BaseInstaller { protected $locations = array(); protected $composer; protected $package; protected $io; /** * Initializes base installer. * * @param PackageInterface $package * @param Composer $composer * @param IOInterface $io */ public function __construct(PackageInterface $package = null, Composer $composer = null, IOInterface $io = null) { $this->composer = $composer; $this->package = $package; $this->io = $io; } /** * Return the install path based on package type. * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { $type = $this->package->getType(); $prettyName = $this->package->getPrettyName(); if (strpos($prettyName, '/') !== false) { list($vendor, $name) = explode('/', $prettyName); } else { $vendor = ''; $name = $prettyName; } $availableVars = $this->inflectPackageVars(compact('name', 'vendor', 'type')); $extra = $package->getExtra(); if (!empty($extra['installer-name'])) { $availableVars['name'] = $extra['installer-name']; } if ($this->composer->getPackage()) { $extra = $this->composer->getPackage()->getExtra(); if (!empty($extra['installer-paths'])) { $customPath = $this->mapCustomInstallPaths($extra['installer-paths'], $prettyName, $type, $vendor); if ($customPath !== false) { return $this->templatePath($customPath, $availableVars); } } } $packageType = substr($type, strlen($frameworkType) + 1); $locations = $this->getLocations(); if (!isset($locations[$packageType])) { throw new \InvalidArgumentException(sprintf('Package type "%s" is not supported', $type)); } return $this->templatePath($locations[$packageType], $availableVars); } /** * For an installer to override to modify the vars per installer. * * @param array<string, string> $vars This will normally receive array{name: string, vendor: string, type: string} * @return array<string, string> */ public function inflectPackageVars($vars) { return $vars; } /** * Gets the installer's locations * * @return array<string, string> map of package types => install path */ public function getLocations() { return $this->locations; } /** * Replace vars in a path * * @param string $path * @param array<string, string> $vars * @return string */ protected function templatePath($path, array $vars = array()) { if (strpos($path, '{') !== false) { extract($vars); preg_match_all('@\{\$([A-Za-z0-9_]*)\}@i', $path, $matches); if (!empty($matches[1])) { foreach ($matches[1] as $var) { $path = str_replace('{$' . $var . '}', $$var, $path); } } } return $path; } /** * Search through a passed paths array for a custom install path. * * @param array $paths * @param string $name * @param string $type * @param string $vendor = NULL * @return string|false */ protected function mapCustomInstallPaths(array $paths, $name, $type, $vendor = NULL) { foreach ($paths as $path => $names) { $names = (array) $names; if (in_array($name, $names) || in_array('type:' . $type, $names) || in_array('vendor:' . $vendor, $names)) { return $path; } } return false; } } vendor/composer/installers/src/Composer/Installers/GravInstaller.php 0000644 00000001274 15132754523 0022047 0 ustar 00 <?php namespace Composer\Installers; class GravInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'user/plugins/{$name}/', 'theme' => 'user/themes/{$name}/', ); /** * Format package name * * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $restrictedWords = implode('|', array_keys($this->locations)); $vars['name'] = strtolower($vars['name']); $vars['name'] = preg_replace('/^(?:grav-)?(?:(?:'.$restrictedWords.')-)?(.*?)(?:-(?:'.$restrictedWords.'))?$/ui', '$1', $vars['name'] ); return $vars; } } vendor/composer/installers/src/bootstrap.php 0000644 00000000724 15132754523 0015377 0 ustar 00 <?php function includeIfExists($file) { if (file_exists($file)) { return include $file; } } if ((!$loader = includeIfExists(__DIR__ . '/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__ . '/../../../autoload.php'))) { die('You must set up the project dependencies, run the following commands:'.PHP_EOL. 'curl -s http://getcomposer.org/installer | php'.PHP_EOL. 'php composer.phar install'.PHP_EOL); } return $loader; vendor/composer/installers/LICENSE 0000644 00000002046 15132754523 0013066 0 ustar 00 Copyright (c) 2012 Kyle Robinson Young Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/composer/LICENSE 0000644 00000002056 15132754523 0010707 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/composer/autoload_psr4.php 0000644 00000002162 15132754523 0013171 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Pelago\\' => array($vendorDir . '/pelago/emogrifier/src'), 'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'), 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'), 'Automattic\\WooCommerce\\Vendor\\' => array($baseDir . '/lib/packages'), 'Automattic\\WooCommerce\\Tests\\' => array($baseDir . '/tests/php/src'), 'Automattic\\WooCommerce\\Testing\\Tools\\' => array($baseDir . '/tests/Tools'), 'Automattic\\WooCommerce\\Blocks\\' => array($baseDir . '/packages/woocommerce-blocks/src'), 'Automattic\\WooCommerce\\Admin\\' => array($baseDir . '/packages/woocommerce-admin/src'), 'Automattic\\WooCommerce\\' => array($baseDir . '/src'), 'Automattic\\Jetpack\\Autoloader\\' => array($vendorDir . '/automattic/jetpack-autoloader/src'), ); vendor/composer/autoload_real.php 0000644 00000003440 15132754523 0013224 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInit7fa27687a59114a5aec1ac3080434897 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register(array('ComposerAutoloaderInit7fa27687a59114a5aec1ac3080434897', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit7fa27687a59114a5aec1ac3080434897', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit7fa27687a59114a5aec1ac3080434897::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); return $loader; } } vendor/composer/installed.json 0000644 00000053705 15132754523 0012563 0 ustar 00 [ { "name": "automattic/jetpack-autoloader", "version": "2.10.1", "version_normalized": "2.10.1.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-autoloader.git", "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3", "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3", "shasum": "" }, "require": { "composer-plugin-api": "^1.1 || ^2.0" }, "require-dev": { "automattic/jetpack-changelogger": "^1.1", "yoast/phpunit-polyfills": "0.2.0" }, "time": "2021-03-30T15:15:59+00:00", "type": "composer-plugin", "extra": { "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", "mirror-repo": "Automattic/jetpack-autoloader", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" }, "branch-alias": { "dev-master": "2.10.x-dev" } }, "installation-source": "dist", "autoload": { "classmap": [ "src/AutoloadGenerator.php" ], "psr-4": { "Automattic\\Jetpack\\Autoloader\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], "description": "Creates a custom autoloader for a plugin or theme.", "support": { "source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1" } }, { "name": "automattic/jetpack-constants", "version": "v1.5.1", "version_normalized": "1.5.1.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/18f772daddc8be5df76c9f4a92e017a3c2569a5b", "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b", "shasum": "" }, "require-dev": { "php-mock/php-mock": "^2.1", "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5" }, "time": "2020-10-28T19:00:31+00:00", "type": "library", "installation-source": "dist", "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], "description": "A wrapper for defining constants in a more testable way.", "support": { "source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1" } }, { "name": "composer/installers", "version": "v1.12.0", "version_normalized": "1.12.0.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19", "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0" }, "replace": { "roundcube/plugin-installer": "*", "shama/baton": "*" }, "require-dev": { "composer/composer": "1.6.* || ^2.0", "composer/semver": "^1 || ^3", "phpstan/phpstan": "^0.12.55", "phpstan/phpstan-phpunit": "^0.12.16", "symfony/phpunit-bridge": "^4.2 || ^5", "symfony/process": "^2.3" }, "time": "2021-09-13T08:19:44+00:00", "type": "composer-plugin", "extra": { "class": "Composer\\Installers\\Plugin", "branch-alias": { "dev-main": "1.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Composer\\Installers\\": "src/Composer/Installers" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "Kyle Robinson Young", "email": "kyle@dontkry.com", "homepage": "https://github.com/shama" } ], "description": "A multi-framework Composer library installer", "homepage": "https://composer.github.io/installers/", "keywords": [ "Craft", "Dolibarr", "Eliasis", "Hurad", "ImageCMS", "Kanboard", "Lan Management System", "MODX Evo", "MantisBT", "Mautic", "Maya", "OXID", "Plentymarkets", "Porto", "RadPHP", "SMF", "Starbug", "Thelia", "Whmcs", "WolfCMS", "agl", "aimeos", "annotatecms", "attogram", "bitrix", "cakephp", "chef", "cockpit", "codeigniter", "concrete5", "croogo", "dokuwiki", "drupal", "eZ Platform", "elgg", "expressionengine", "fuelphp", "grav", "installer", "itop", "joomla", "known", "kohana", "laravel", "lavalite", "lithium", "magento", "majima", "mako", "mediawiki", "miaoxing", "modulework", "modx", "moodle", "osclass", "pantheon", "phpbb", "piwik", "ppi", "processwire", "puppet", "pxcms", "reindex", "roundcube", "shopware", "silverstripe", "sydes", "sylius", "symfony", "tastyigniter", "typo3", "wordpress", "yawik", "zend", "zikula" ], "support": { "issues": "https://github.com/composer/installers/issues", "source": "https://github.com/composer/installers/tree/v1.12.0" }, "funding": [ { "url": "https://packagist.com", "type": "custom" }, { "url": "https://github.com/composer", "type": "github" }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ] }, { "name": "maxmind-db/reader", "version": "v1.6.0", "version_normalized": "1.6.0.0", "source": { "type": "git", "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/febd4920bf17c1da84cef58e56a8227dfb37fbe4", "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4", "shasum": "" }, "require": { "php": ">=5.6" }, "conflict": { "ext-maxminddb": "<1.6.0,>=2.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", "php-coveralls/php-coveralls": "^2.1", "phpunit/phpcov": "^3.0", "phpunit/phpunit": "5.*", "squizlabs/php_codesniffer": "3.*" }, "suggest": { "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" }, "time": "2019-12-19T22:59:03+00:00", "type": "library", "installation-source": "dist", "autoload": { "psr-4": { "MaxMind\\Db\\": "src/MaxMind/Db" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "Apache-2.0" ], "authors": [ { "name": "Gregory J. Oschwald", "email": "goschwald@maxmind.com", "homepage": "https://www.maxmind.com/" } ], "description": "MaxMind DB Reader API", "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", "keywords": [ "database", "geoip", "geoip2", "geolocation", "maxmind" ], "support": { "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0" } }, { "name": "pelago/emogrifier", "version": "v3.1.0", "version_normalized": "3.1.0.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", "reference": "f6a5c7d44612d86c3901c93f1592f5440e6b2cd8" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6a5c7d44612d86c3901c93f1592f5440e6b2cd8", "reference": "f6a5c7d44612d86c3901c93f1592f5440e6b2cd8", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^5.6 || ~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4", "symfony/css-selector": "^2.8 || ^3.0 || ^4.0 || ^5.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15.3", "phpmd/phpmd": "^2.7.0", "phpunit/phpunit": "^5.7.27", "squizlabs/php_codesniffer": "^3.5.0" }, "time": "2019-12-26T19:37:31+00:00", "type": "library", "extra": { "branch-alias": { "dev-master": "4.0.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Pelago\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "Oliver Klee", "email": "github@oliverklee.de" }, { "name": "Zoli Szabó", "email": "zoli.szabo+github@gmail.com" }, { "name": "John Reeve", "email": "jreeve@pelagodesign.com" }, { "name": "Jake Hotson", "email": "jake@qzdesign.co.uk" }, { "name": "Cameron Brooks" }, { "name": "Jaime Prado" } ], "description": "Converts CSS styles into inline style attributes in your HTML code", "homepage": "https://www.myintervals.com/emogrifier.php", "keywords": [ "css", "email", "pre-processing" ], "support": { "issues": "https://github.com/MyIntervals/emogrifier/issues", "source": "https://github.com/MyIntervals/emogrifier" } }, { "name": "psr/container", "version": "1.0.0", "version_normalized": "1.0.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", "shasum": "" }, "require": { "php": ">=5.3.0" }, "time": "2017-02-14T16:28:37+00:00", "type": "library", "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "PHP-FIG", "homepage": "http://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", "homepage": "https://github.com/php-fig/container", "keywords": [ "PSR-11", "container", "container-interface", "container-interop", "psr" ], "support": { "issues": "https://github.com/php-fig/container/issues", "source": "https://github.com/php-fig/container/tree/master" } }, { "name": "symfony/css-selector", "version": "v3.3.6", "version_normalized": "3.3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", "reference": "4d882dced7b995d5274293039370148e291808f2" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/symfony/css-selector/zipball/4d882dced7b995d5274293039370148e291808f2", "reference": "4d882dced7b995d5274293039370148e291808f2", "shasum": "" }, "require": { "php": ">=5.5.9" }, "time": "2017-05-01T15:01:29+00:00", "type": "library", "extra": { "branch-alias": { "dev-master": "3.3-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Symfony\\Component\\CssSelector\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "Jean-François Simon", "email": "jeanfrancois.simon@sensiolabs.com" }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", "support": { "source": "https://github.com/symfony/css-selector/tree/master" } }, { "name": "woocommerce/action-scheduler", "version": "3.3.0", "version_normalized": "3.3.0.0", "source": { "type": "git", "url": "https://github.com/woocommerce/action-scheduler.git", "reference": "5588a831cd2453ecf7d4803f3a81063e13cde93d" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/5588a831cd2453ecf7d4803f3a81063e13cde93d", "reference": "5588a831cd2453ecf7d4803f3a81063e13cde93d", "shasum": "" }, "require-dev": { "phpunit/phpunit": "^7.5", "woocommerce/woocommerce-sniffs": "0.1.0", "wp-cli/wp-cli": "~2.5.0" }, "time": "2021-09-15T21:08:48+00:00", "type": "wordpress-plugin", "extra": { "scripts-description": { "test": "Run unit tests", "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } }, "installation-source": "dist", "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-3.0-or-later" ], "description": "Action Scheduler for WordPress and WooCommerce", "homepage": "https://actionscheduler.org/", "support": { "issues": "https://github.com/woocommerce/action-scheduler/issues", "source": "https://github.com/woocommerce/action-scheduler/tree/3.3.0" } }, { "name": "woocommerce/woocommerce-admin", "version": "2.8.0", "version_normalized": "2.8.0.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-admin.git", "reference": "63b93a95db4bf788f42587a41f2378128a2adfdf" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/63b93a95db4bf788f42587a41f2378128a2adfdf", "reference": "63b93a95db4bf788f42587a41f2378128a2adfdf", "shasum": "" }, "require": { "automattic/jetpack-autoloader": "^2.9.1", "composer/installers": "^1.9.0", "php": ">=7.0" }, "require-dev": { "automattic/jetpack-changelogger": "^1.1", "bamarni/composer-bin-plugin": "^1.4", "suin/phpcs-psr4-sniff": "^2.2", "woocommerce/woocommerce-sniffs": "0.1.0", "yoast/phpunit-polyfills": "^1.0" }, "time": "2021-11-02T19:28:38+00:00", "type": "wordpress-plugin", "extra": { "scripts-description": { "test": "Run unit tests", "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" }, "bamarni-bin": { "target-directory": "bin/composer" }, "changelogger": { "changelog": "./changelog.txt", "formatter": { "filename": "bin/changelogger/WCAdminFormatter.php" }, "versioning": "semver", "changes-dir": "./changelogs", "types": [ "Fix", "Add", "Update", "Dev", "Tweak", "Performance", "Enhancement" ] } }, "installation-source": "dist", "autoload": { "psr-4": { "Automattic\\WooCommerce\\Admin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-3.0-or-later" ], "description": "A modern, javascript-driven WooCommerce Admin experience.", "homepage": "https://github.com/woocommerce/woocommerce-admin", "support": { "issues": "https://github.com/woocommerce/woocommerce-admin/issues", "source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.8.0" } }, { "name": "woocommerce/woocommerce-blocks", "version": "v6.1.0", "version_normalized": "6.1.0.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", "reference": "8556efd69e85c01f5571d39e6581d9b8486b682f" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/8556efd69e85c01f5571d39e6581d9b8486b682f", "reference": "8556efd69e85c01f5571d39e6581d9b8486b682f", "shasum": "" }, "require": { "automattic/jetpack-autoloader": "^2.9.1", "composer/installers": "^1.7.0" }, "require-dev": { "woocommerce/woocommerce-sniffs": "0.1.0", "wp-phpunit/wp-phpunit": "^5.4", "yoast/phpunit-polyfills": "^1.0" }, "time": "2021-10-12T13:07:11+00:00", "type": "wordpress-plugin", "extra": { "scripts-description": { "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } }, "installation-source": "dist", "autoload": { "psr-4": { "Automattic\\WooCommerce\\Blocks\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-3.0-or-later" ], "description": "WooCommerce blocks for the Gutenberg editor.", "homepage": "https://woocommerce.com/", "keywords": [ "blocks", "gutenberg", "woocommerce" ], "support": { "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v6.1.0" } } ] vendor/composer/autoload_namespaces.php 0000644 00000000343 15132754523 0014417 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Automattic\\WooCommerce\\Vendor\\' => array($baseDir . '/lib/packages'), ); vendor/composer/ClassLoader.php 0000644 00000032241 15132754523 0012606 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see http://www.php-fig.org/psr/psr-0/ * @see http://www.php-fig.org/psr/psr-4/ */ class ClassLoader { // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); private $fallbackDirsPsr4 = array(); // PSR-0 private $prefixesPsr0 = array(); private $fallbackDirsPsr0 = array(); private $useIncludePath = false; private $classMap = array(); private $classMapAuthoritative = false; private $missingClasses = array(); private $apcuPrefix; public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } public function getFallbackDirs() { return $this->fallbackDirsPsr0; } public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 base directories */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } /** * Unregisters this instance as an autoloader. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $class The name of the class * @return bool|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. */ function includeFile($file) { include $file; } vendor/autoload_packages.php 0000644 00000000475 15132754523 0012235 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jp7fa27687a59114a5aec1ac3080434897; // phpcs:ignore require_once __DIR__ . '/jetpack-autoloader/class-autoloader.php'; Autoloader::init(); lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php 0000644 00000001663 15132754523 0024620 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\ServiceProvider; use IteratorAggregate; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; interface ServiceProviderAggregateInterface extends ContainerAwareInterface, IteratorAggregate { /** * Add a service provider to the aggregate. * * @param string|ServiceProviderInterface $provider * * @return self */ public function add($provider) : ServiceProviderAggregateInterface; /** * Determines whether a service is provided by the aggregate. * * @param string $service * * @return boolean */ public function provides(string $service) : bool; /** * Invokes the register method of a provider that provides a specific service. * * @param string $service * * @return void */ public function register(string $service); } lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php 0000644 00000002526 15132754523 0023010 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\ServiceProvider; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; interface ServiceProviderInterface extends ContainerAwareInterface { /** * Returns a boolean if checking whether this provider provides a specific * service or returns an array of provided services if no argument passed. * * @param string $service * * @return boolean */ public function provides(string $service) : bool; /** * Use the register method to register items with the container via the * protected $this->leagueContainer property or the `getLeagueContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(); /** * Set a custom id for the service provider. This enables * registering the same service provider multiple times. * * @param string $id * * @return self */ public function setIdentifier(string $id) : ServiceProviderInterface; /** * The id of the service provider uniquely identifies it, so * that we can quickly determine if it has already been registered. * Defaults to get_class($provider). * * @return string */ public function getIdentifier() : string; } lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php 0000644 00000005433 15132754523 0022776 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\ServiceProvider; use Generator; use Automattic\WooCommerce\Vendor\League\Container\{ContainerAwareInterface, ContainerAwareTrait}; use Automattic\WooCommerce\Vendor\League\Container\Exception\ContainerException; class ServiceProviderAggregate implements ServiceProviderAggregateInterface { use ContainerAwareTrait; /** * @var ServiceProviderInterface[] */ protected $providers = []; /** * @var array */ protected $registered = []; /** * {@inheritdoc} */ public function add($provider) : ServiceProviderAggregateInterface { if (is_string($provider) && $this->getContainer()->has($provider)) { $provider = $this->getContainer()->get($provider); } elseif (is_string($provider) && class_exists($provider)) { $provider = new $provider; } if (in_array($provider, $this->providers, true)) { return $this; } if ($provider instanceof ContainerAwareInterface) { $provider->setLeagueContainer($this->getLeagueContainer()); } if ($provider instanceof BootableServiceProviderInterface) { $provider->boot(); } if ($provider instanceof ServiceProviderInterface) { $this->providers[] = $provider; return $this; } throw new ContainerException( 'A service provider must be a fully qualified class name or instance ' . 'of (\Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\ServiceProviderInterface)' ); } /** * {@inheritdoc} */ public function provides(string $service) : bool { foreach ($this->getIterator() as $provider) { if ($provider->provides($service)) { return true; } } return false; } /** * {@inheritdoc} */ public function getIterator() : Generator { $count = count($this->providers); for ($i = 0; $i < $count; $i++) { yield $this->providers[$i]; } } /** * {@inheritdoc} */ public function register(string $service) { if (false === $this->provides($service)) { throw new ContainerException( sprintf('(%s) is not provided by a service provider', $service) ); } foreach ($this->getIterator() as $provider) { if (in_array($provider->getIdentifier(), $this->registered, true)) { continue; } if ($provider->provides($service)) { $provider->register(); $this->registered[] = $provider->getIdentifier(); } } } } lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php 0000644 00000001605 15132754523 0022650 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\ServiceProvider; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareTrait; abstract class AbstractServiceProvider implements ServiceProviderInterface { use ContainerAwareTrait; /** * @var array */ protected $provides = []; /** * @var string */ protected $identifier; /** * {@inheritdoc} */ public function provides(string $alias) : bool { return in_array($alias, $this->provides, true); } /** * {@inheritdoc} */ public function setIdentifier(string $id) : ServiceProviderInterface { $this->identifier = $id; return $this; } /** * {@inheritdoc} */ public function getIdentifier() : string { return $this->identifier ?? get_class($this); } } lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php 0000644 00000000643 15132754523 0024456 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\ServiceProvider; interface BootableServiceProviderInterface extends ServiceProviderInterface { /** * Method will be invoked on registration of a service provider implementing * this interface. Provides ability for eager loading of Service Providers. * * @return void */ public function boot(); } lib/packages/League/Container/Definition/DefinitionInterface.php 0000644 00000004765 15132754523 0020751 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Definition; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; interface DefinitionInterface extends ContainerAwareInterface { /** * Add a tag to the definition. * * @param string $tag * * @return self */ public function addTag(string $tag) : DefinitionInterface; /** * Does the definition have a tag? * * @param string $tag * * @return boolean */ public function hasTag(string $tag) : bool; /** * Set the alias of the definition. * * @param string $id * * @return DefinitionInterface */ public function setAlias(string $id) : DefinitionInterface; /** * Get the alias of the definition. * * @return string */ public function getAlias() : string; /** * Set whether this is a shared definition. * * @param boolean $shared * * @return self */ public function setShared(bool $shared) : DefinitionInterface; /** * Is this a shared definition? * * @return boolean */ public function isShared() : bool; /** * Get the concrete of the definition. * * @return mixed */ public function getConcrete(); /** * Set the concrete of the definition. * * @param mixed $concrete * * @return DefinitionInterface */ public function setConcrete($concrete) : DefinitionInterface; /** * Add an argument to be injected. * * @param mixed $arg * * @return self */ public function addArgument($arg) : DefinitionInterface; /** * Add multiple arguments to be injected. * * @param array $args * * @return self */ public function addArguments(array $args) : DefinitionInterface; /** * Add a method to be invoked * * @param string $method * @param array $args * * @return self */ public function addMethodCall(string $method, array $args = []) : DefinitionInterface; /** * Add multiple methods to be invoked * * @param array $methods * * @return self */ public function addMethodCalls(array $methods = []) : DefinitionInterface; /** * Handle instantiation and manipulation of value and return. * * @param boolean $new * * @return mixed */ public function resolve(bool $new = false); } lib/packages/League/Container/Definition/DefinitionAggregate.php 0000644 00000005645 15132754523 0020735 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Definition; use Generator; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareTrait; use Automattic\WooCommerce\Vendor\League\Container\Exception\NotFoundException; class DefinitionAggregate implements DefinitionAggregateInterface { use ContainerAwareTrait; /** * @var DefinitionInterface[] */ protected $definitions = []; /** * Construct. * * @param DefinitionInterface[] $definitions */ public function __construct(array $definitions = []) { $this->definitions = array_filter($definitions, function ($definition) { return ($definition instanceof DefinitionInterface); }); } /** * {@inheritdoc} */ public function add(string $id, $definition, bool $shared = false) : DefinitionInterface { if (!$definition instanceof DefinitionInterface) { $definition = new Definition($id, $definition); } $this->definitions[] = $definition ->setAlias($id) ->setShared($shared) ; return $definition; } /** * {@inheritdoc} */ public function has(string $id) : bool { foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return true; } } return false; } /** * {@inheritdoc} */ public function hasTag(string $tag) : bool { foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { return true; } } return false; } /** * {@inheritdoc} */ public function getDefinition(string $id) : DefinitionInterface { foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return $definition->setLeagueContainer($this->getLeagueContainer()); } } throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); } /** * {@inheritdoc} */ public function resolve(string $id, bool $new = false) { return $this->getDefinition($id)->resolve($new); } /** * {@inheritdoc} */ public function resolveTagged(string $tag, bool $new = false) : array { $arrayOf = []; foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { $arrayOf[] = $definition->setLeagueContainer($this->getLeagueContainer())->resolve($new); } } return $arrayOf; } /** * {@inheritdoc} */ public function getIterator() : Generator { $count = count($this->definitions); for ($i = 0; $i < $count; $i++) { yield $this->definitions[$i]; } } } lib/packages/League/Container/Definition/DefinitionAggregateInterface.php 0000644 00000003101 15132754523 0022537 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Definition; use IteratorAggregate; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; interface DefinitionAggregateInterface extends ContainerAwareInterface, IteratorAggregate { /** * Add a definition to the aggregate. * * @param string $id * @param mixed $definition * @param boolean $shared * * @return DefinitionInterface */ public function add(string $id, $definition, bool $shared = false) : DefinitionInterface; /** * Checks whether alias exists as definition. * * @param string $id * * @return boolean */ public function has(string $id) : bool; /** * Checks whether tag exists as definition. * * @param string $tag * * @return boolean */ public function hasTag(string $tag) : bool; /** * Get the definition to be extended. * * @param string $id * * @return DefinitionInterface */ public function getDefinition(string $id) : DefinitionInterface; /** * Resolve and build a concrete value from an id/alias. * * @param string $id * @param boolean $new * * @return mixed */ public function resolve(string $id, bool $new = false); /** * Resolve and build an array of concrete values from a tag. * * @param string $tag * @param boolean $new * * @return mixed */ public function resolveTagged(string $tag, bool $new = false); } lib/packages/League/Container/Definition/Definition.php 0000644 00000012603 15132754523 0017116 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Definition; use Automattic\WooCommerce\Vendor\League\Container\Argument\{ ArgumentResolverInterface, ArgumentResolverTrait, ClassNameInterface, RawArgumentInterface }; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareTrait; use ReflectionClass; use ReflectionException; class Definition implements ArgumentResolverInterface, DefinitionInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $alias; /** * @var mixed */ protected $concrete; /** * @var boolean */ protected $shared = false; /** * @var array */ protected $tags = []; /** * @var array */ protected $arguments = []; /** * @var array */ protected $methods = []; /** * @var mixed */ protected $resolved; /** * Constructor. * * @param string $id * @param mixed $concrete */ public function __construct(string $id, $concrete = null) { $concrete = $concrete ?? $id; $this->alias = $id; $this->concrete = $concrete; } /** * {@inheritdoc} */ public function addTag(string $tag) : DefinitionInterface { $this->tags[$tag] = true; return $this; } /** * {@inheritdoc} */ public function hasTag(string $tag) : bool { return isset($this->tags[$tag]); } /** * {@inheritdoc} */ public function setAlias(string $id) : DefinitionInterface { $this->alias = $id; return $this; } /** * {@inheritdoc} */ public function getAlias() : string { return $this->alias; } /** * {@inheritdoc} */ public function setShared(bool $shared = true) : DefinitionInterface { $this->shared = $shared; return $this; } /** * {@inheritdoc} */ public function isShared() : bool { return $this->shared; } /** * {@inheritdoc} */ public function getConcrete() { return $this->concrete; } /** * {@inheritdoc} */ public function setConcrete($concrete) : DefinitionInterface { $this->concrete = $concrete; $this->resolved = null; return $this; } /** * {@inheritdoc} */ public function addArgument($arg) : DefinitionInterface { $this->arguments[] = $arg; return $this; } /** * {@inheritdoc} */ public function addArguments(array $args) : DefinitionInterface { foreach ($args as $arg) { $this->addArgument($arg); } return $this; } /** * {@inheritdoc} */ public function addMethodCall(string $method, array $args = []) : DefinitionInterface { $this->methods[] = [ 'method' => $method, 'arguments' => $args ]; return $this; } /** * {@inheritdoc} */ public function addMethodCalls(array $methods = []) : DefinitionInterface { foreach ($methods as $method => $args) { $this->addMethodCall($method, $args); } return $this; } /** * {@inheritdoc} */ public function resolve(bool $new = false) { $concrete = $this->concrete; if ($this->isShared() && $this->resolved !== null && $new === false) { return $this->resolved; } if (is_callable($concrete)) { $concrete = $this->resolveCallable($concrete); } if ($concrete instanceof RawArgumentInterface) { $this->resolved = $concrete->getValue(); return $concrete->getValue(); } if ($concrete instanceof ClassNameInterface) { $concrete = $concrete->getClassName(); } if (is_string($concrete) && class_exists($concrete)) { $concrete = $this->resolveClass($concrete); } if (is_object($concrete)) { $concrete = $this->invokeMethods($concrete); } $this->resolved = $concrete; return $concrete; } /** * Resolve a callable. * * @param callable $concrete * * @return mixed */ protected function resolveCallable(callable $concrete) { $resolved = $this->resolveArguments($this->arguments); return call_user_func_array($concrete, $resolved); } /** * Resolve a class. * * @param string $concrete * * @return object * * @throws ReflectionException */ protected function resolveClass(string $concrete) { $resolved = $this->resolveArguments($this->arguments); $reflection = new ReflectionClass($concrete); return $reflection->newInstanceArgs($resolved); } /** * Invoke methods on resolved instance. * * @param object $instance * * @return object */ protected function invokeMethods($instance) { foreach ($this->methods as $method) { $args = $this->resolveArguments($method['arguments']); /** @var callable $callable */ $callable = [$instance, $method['method']]; call_user_func_array($callable, $args); } return $instance; } } lib/packages/League/Container/Exception/NotFoundException.php 0000644 00000000374 15132754523 0020311 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\League\Container\Exception; use Psr\Container\NotFoundExceptionInterface; use InvalidArgumentException; class NotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface { } lib/packages/League/Container/Exception/ContainerException.php 0000644 00000000357 15132754523 0020500 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\League\Container\Exception; use Psr\Container\ContainerExceptionInterface; use RuntimeException; class ContainerException extends RuntimeException implements ContainerExceptionInterface { } lib/packages/League/Container/ContainerAwareInterface.php 0000644 00000001731 15132754523 0017461 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container; use Psr\Container\ContainerInterface; interface ContainerAwareInterface { /** * Set a container * * @param ContainerInterface $container * * @return self */ public function setContainer(ContainerInterface $container) : self; /** * Get the container * * @return ContainerInterface */ public function getContainer() : ContainerInterface; /** * Set a container. This will be removed in favour of setContainer receiving Container in next major release. * * @param Container $container * * @return self */ public function setLeagueContainer(Container $container) : self; /** * Get the container. This will be removed in favour of getContainer returning Container in next major release. * * @return Container */ public function getLeagueContainer() : Container; } lib/packages/League/Container/ContainerAwareTrait.php 0000644 00000003201 15132754523 0016636 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container; use Automattic\WooCommerce\Vendor\League\Container\Exception\ContainerException; use Psr\Container\ContainerInterface; trait ContainerAwareTrait { /** * @var ContainerInterface */ protected $container; /** * @var Container */ protected $leagueContainer; /** * Set a container. * * @param ContainerInterface $container * * @return self */ public function setContainer(ContainerInterface $container) : ContainerAwareInterface { $this->container = $container; return $this; } /** * Get the container. * * @return ContainerInterface */ public function getContainer() : ContainerInterface { if ($this->container instanceof ContainerInterface) { return $this->container; } throw new ContainerException('No container implementation has been set.'); } /** * Set a container. * * @param Container $container * * @return self */ public function setLeagueContainer(Container $container) : ContainerAwareInterface { $this->container = $container; $this->leagueContainer = $container; return $this; } /** * Get the container. * * @return Container */ public function getLeagueContainer() : Container { if ($this->leagueContainer instanceof Container) { return $this->leagueContainer; } throw new ContainerException('No container implementation has been set.'); } } lib/packages/League/Container/Container.php 0000644 00000015146 15132754523 0014665 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container; use Automattic\WooCommerce\Vendor\League\Container\Definition\{DefinitionAggregate, DefinitionInterface, DefinitionAggregateInterface}; use Automattic\WooCommerce\Vendor\League\Container\Exception\{NotFoundException, ContainerException}; use Automattic\WooCommerce\Vendor\League\Container\Inflector\{InflectorAggregate, InflectorInterface, InflectorAggregateInterface}; use Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\{ ServiceProviderAggregate, ServiceProviderAggregateInterface, ServiceProviderInterface }; use Psr\Container\ContainerInterface; class Container implements ContainerInterface { /** * @var boolean */ protected $defaultToShared = false; /** * @var DefinitionAggregateInterface */ protected $definitions; /** * @var ServiceProviderAggregateInterface */ protected $providers; /** * @var InflectorAggregateInterface */ protected $inflectors; /** * @var ContainerInterface[] */ protected $delegates = []; /** * Construct. * * @param DefinitionAggregateInterface|null $definitions * @param ServiceProviderAggregateInterface|null $providers * @param InflectorAggregateInterface|null $inflectors */ public function __construct( DefinitionAggregateInterface $definitions = null, ServiceProviderAggregateInterface $providers = null, InflectorAggregateInterface $inflectors = null ) { $this->definitions = $definitions ?? new DefinitionAggregate; $this->providers = $providers ?? new ServiceProviderAggregate; $this->inflectors = $inflectors ?? new InflectorAggregate; if ($this->definitions instanceof ContainerAwareInterface) { $this->definitions->setLeagueContainer($this); } if ($this->providers instanceof ContainerAwareInterface) { $this->providers->setLeagueContainer($this); } if ($this->inflectors instanceof ContainerAwareInterface) { $this->inflectors->setLeagueContainer($this); } } /** * Add an item to the container. * * @param string $id * @param mixed $concrete * @param boolean $shared * * @return DefinitionInterface */ public function add(string $id, $concrete = null, bool $shared = null) : DefinitionInterface { $concrete = $concrete ?? $id; $shared = $shared ?? $this->defaultToShared; return $this->definitions->add($id, $concrete, $shared); } /** * Proxy to add with shared as true. * * @param string $id * @param mixed $concrete * * @return DefinitionInterface */ public function share(string $id, $concrete = null) : DefinitionInterface { return $this->add($id, $concrete, true); } /** * Whether the container should default to defining shared definitions. * * @param boolean $shared * * @return self */ public function defaultToShared(bool $shared = true) : ContainerInterface { $this->defaultToShared = $shared; return $this; } /** * Get a definition to extend. * * @param string $id [description] * * @return DefinitionInterface */ public function extend(string $id) : DefinitionInterface { if ($this->providers->provides($id)) { $this->providers->register($id); } if ($this->definitions->has($id)) { return $this->definitions->getDefinition($id); } throw new NotFoundException( sprintf('Unable to extend alias (%s) as it is not being managed as a definition', $id) ); } /** * Add a service provider. * * @param ServiceProviderInterface|string $provider * * @return self */ public function addServiceProvider($provider) : self { $this->providers->add($provider); return $this; } /** * {@inheritdoc} */ public function get($id, bool $new = false) { if ($this->definitions->has($id)) { $resolved = $this->definitions->resolve($id, $new); return $this->inflectors->inflect($resolved); } if ($this->definitions->hasTag($id)) { $arrayOf = $this->definitions->resolveTagged($id, $new); array_walk($arrayOf, function (&$resolved) { $resolved = $this->inflectors->inflect($resolved); }); return $arrayOf; } if ($this->providers->provides($id)) { $this->providers->register($id); if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); } return $this->get($id, $new); } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { $resolved = $delegate->get($id); return $this->inflectors->inflect($resolved); } } throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); } /** * {@inheritdoc} */ public function has($id) : bool { if ($this->definitions->has($id)) { return true; } if ($this->definitions->hasTag($id)) { return true; } if ($this->providers->provides($id)) { return true; } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { return true; } } return false; } /** * Allows for manipulation of specific types on resolution. * * @param string $type * @param callable|null $callback * * @return InflectorInterface */ public function inflector(string $type, callable $callback = null) : InflectorInterface { return $this->inflectors->add($type, $callback); } /** * Delegate a backup container to be checked for services if it * cannot be resolved via this container. * * @param ContainerInterface $container * * @return self */ public function delegate(ContainerInterface $container) : self { $this->delegates[] = $container; if ($container instanceof ContainerAwareInterface) { $container->setLeagueContainer($this); } return $this; } } lib/packages/League/Container/Inflector/InflectorAggregate.php 0000644 00000002373 15132754523 0020422 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Inflector; use Generator; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareTrait; class InflectorAggregate implements InflectorAggregateInterface { use ContainerAwareTrait; /** * @var Inflector[] */ protected $inflectors = []; /** * {@inheritdoc} */ public function add(string $type, callable $callback = null) : Inflector { $inflector = new Inflector($type, $callback); $this->inflectors[] = $inflector; return $inflector; } /** * {@inheritdoc} */ public function getIterator() : Generator { $count = count($this->inflectors); for ($i = 0; $i < $count; $i++) { yield $this->inflectors[$i]; } } /** * {@inheritdoc} */ public function inflect($object) { foreach ($this->getIterator() as $inflector) { $type = $inflector->getType(); if (! $object instanceof $type) { continue; } $inflector->setLeagueContainer($this->getLeagueContainer()); $inflector->inflect($object); } return $object; } } lib/packages/League/Container/Inflector/Inflector.php 0000644 00000005426 15132754523 0016615 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Inflector; use Automattic\WooCommerce\Vendor\League\Container\Argument\ArgumentResolverInterface; use Automattic\WooCommerce\Vendor\League\Container\Argument\ArgumentResolverTrait; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareTrait; class Inflector implements ArgumentResolverInterface, InflectorInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $type; /** * @var callable|null */ protected $callback; /** * @var array */ protected $methods = []; /** * @var array */ protected $properties = []; /** * Construct. * * @param string $type * @param callable|null $callback */ public function __construct(string $type, callable $callback = null) { $this->type = $type; $this->callback = $callback; } /** * {@inheritdoc} */ public function getType() : string { return $this->type; } /** * {@inheritdoc} */ public function invokeMethod(string $name, array $args) : InflectorInterface { $this->methods[$name] = $args; return $this; } /** * {@inheritdoc} */ public function invokeMethods(array $methods) : InflectorInterface { foreach ($methods as $name => $args) { $this->invokeMethod($name, $args); } return $this; } /** * {@inheritdoc} */ public function setProperty(string $property, $value) : InflectorInterface { $this->properties[$property] = $this->resolveArguments([$value])[0]; return $this; } /** * {@inheritdoc} */ public function setProperties(array $properties) : InflectorInterface { foreach ($properties as $property => $value) { $this->setProperty($property, $value); } return $this; } /** * {@inheritdoc} */ public function inflect($object) { $properties = $this->resolveArguments(array_values($this->properties)); $properties = array_combine(array_keys($this->properties), $properties); // array_combine() can technically return false foreach ($properties ?: [] as $property => $value) { $object->{$property} = $value; } foreach ($this->methods as $method => $args) { $args = $this->resolveArguments($args); /** @var callable $callable */ $callable = [$object, $method]; call_user_func_array($callable, $args); } if ($this->callback !== null) { call_user_func($this->callback, $object); } } } lib/packages/League/Container/Inflector/InflectorInterface.php 0000644 00000002475 15132754523 0020437 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Inflector; interface InflectorInterface { /** * Get the type. * * @return string */ public function getType() : string; /** * Defines a method to be invoked on the subject object. * * @param string $name * @param array $args * * @return self */ public function invokeMethod(string $name, array $args) : InflectorInterface; /** * Defines multiple methods to be invoked on the subject object. * * @param array $methods * * @return self */ public function invokeMethods(array $methods) : InflectorInterface; /** * Defines a property to be set on the subject object. * * @param string $property * @param mixed $value * * @return self */ public function setProperty(string $property, $value) : InflectorInterface; /** * Defines multiple properties to be set on the subject object. * * @param array $properties * * @return self */ public function setProperties(array $properties) : InflectorInterface; /** * Apply inflections to an object. * * @param object $object * * @return void */ public function inflect($object); } lib/packages/League/Container/Inflector/InflectorAggregateInterface.php 0000644 00000001261 15132754523 0022236 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Inflector; use IteratorAggregate; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; interface InflectorAggregateInterface extends ContainerAwareInterface, IteratorAggregate { /** * Add an inflector to the aggregate. * * @param string $type * @param callable $callback * * @return Inflector */ public function add(string $type, callable $callback = null) : Inflector; /** * Applies all inflectors to an object. * * @param object $object * @return object */ public function inflect($object); } lib/packages/League/Container/ReflectionContainer.php 0000644 00000006416 15132754523 0016700 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container; use Automattic\WooCommerce\Vendor\League\Container\Argument\{ArgumentResolverInterface, ArgumentResolverTrait}; use Automattic\WooCommerce\Vendor\League\Container\Exception\NotFoundException; use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; use ReflectionFunction; use ReflectionMethod; class ReflectionContainer implements ArgumentResolverInterface, ContainerInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var boolean */ protected $cacheResolutions = false; /** * Cache of resolutions. * * @var array */ protected $cache = []; /** * {@inheritdoc} * * @throws ReflectionException */ public function get($id, array $args = []) { if ($this->cacheResolutions === true && array_key_exists($id, $this->cache)) { return $this->cache[$id]; } if (! $this->has($id)) { throw new NotFoundException( sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) ); } $reflector = new ReflectionClass($id); $construct = $reflector->getConstructor(); $resolution = $construct === null ? new $id : $resolution = $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) ; if ($this->cacheResolutions === true) { $this->cache[$id] = $resolution; } return $resolution; } /** * {@inheritdoc} */ public function has($id) : bool { return class_exists($id); } /** * Invoke a callable via the container. * * @param callable $callable * @param array $args * * @return mixed * * @throws ReflectionException */ public function call(callable $callable, array $args = []) { if (is_string($callable) && strpos($callable, '::') !== false) { $callable = explode('::', $callable); } if (is_array($callable)) { if (is_string($callable[0])) { $callable[0] = $this->getContainer()->get($callable[0]); } $reflection = new ReflectionMethod($callable[0], $callable[1]); if ($reflection->isStatic()) { $callable[0] = null; } return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); } if (is_object($callable)) { $reflection = new ReflectionMethod($callable, '__invoke'); return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); } $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); } /** * Whether the container should default to caching resolutions and returning * the cache on following calls. * * @param boolean $option * * @return self */ public function cacheResolutions(bool $option = true) : ContainerInterface { $this->cacheResolutions = $option; return $this; } } lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php 0000644 00000001326 15132754523 0021725 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\League\Container\Argument; class ClassNameWithOptionalValue implements ClassNameInterface { /** * @var string */ private $className; /** * @var mixed */ private $optionalValue; /** * @param string $className * @param mixed $optionalValue */ public function __construct(string $className, $optionalValue) { $this->className = $className; $this->optionalValue = $optionalValue; } /** * @inheritDoc */ public function getClassName(): string { return $this->className; } public function getOptionalValue() { return $this->optionalValue; } } lib/packages/League/Container/Argument/RawArgumentInterface.php 0000644 00000000400 15132754523 0020565 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; interface RawArgumentInterface { /** * Return the value of the raw argument. * * @return mixed */ public function getValue(); } lib/packages/League/Container/Argument/ClassName.php 0000644 00000000752 15132754523 0016370 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; class ClassName implements ClassNameInterface { /** * @var string */ protected $value; /** * Construct. * * @param string $value */ public function __construct(string $value) { $this->value = $value; } /** * {@inheritdoc} */ public function getClassName() : string { return $this->value; } } lib/packages/League/Container/Argument/RawArgument.php 0000644 00000000730 15132754523 0016752 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; class RawArgument implements RawArgumentInterface { /** * @var mixed */ protected $value; /** * Construct. * * @param mixed $value */ public function __construct($value) { $this->value = $value; } /** * {@inheritdoc} */ public function getValue() { return $this->value; } } lib/packages/League/Container/Argument/ClassNameInterface.php 0000644 00000000375 15132754523 0020212 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; interface ClassNameInterface { /** * Return the class name. * * @return string */ public function getClassName() : string; } lib/packages/League/Container/Argument/ArgumentResolverTrait.php 0000644 00000007150 15132754523 0021031 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; use Automattic\WooCommerce\Vendor\League\Container\Container; use Automattic\WooCommerce\Vendor\League\Container\Exception\{ContainerException, NotFoundException}; use Automattic\WooCommerce\Vendor\League\Container\ReflectionContainer; use Psr\Container\ContainerInterface; use ReflectionFunctionAbstract; use ReflectionParameter; trait ArgumentResolverTrait { /** * {@inheritdoc} */ public function resolveArguments(array $arguments) : array { return array_map(function ($argument) { $justStringValue = false; if ($argument instanceof RawArgumentInterface) { return $argument->getValue(); } elseif ($argument instanceof ClassNameInterface) { $id = $argument->getClassName(); } elseif (!is_string($argument)) { return $argument; } else { $justStringValue = true; $id = $argument; } $container = null; try { $container = $this->getLeagueContainer(); } catch (ContainerException $e) { if ($this instanceof ReflectionContainer) { $container = $this; } } if ($container !== null) { try { return $container->get($id); } catch (NotFoundException $exception) { if ($argument instanceof ClassNameWithOptionalValue) { return $argument->getOptionalValue(); } if ($justStringValue) { return $id; } throw $exception; } } if ($argument instanceof ClassNameWithOptionalValue) { return $argument->getOptionalValue(); } // Just a string value. return $id; }, $arguments); } /** * {@inheritdoc} */ public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []) : array { $arguments = array_map(function (ReflectionParameter $param) use ($method, $args) { $name = $param->getName(); $type = $param->getType(); if (array_key_exists($name, $args)) { return new RawArgument($args[$name]); } if ($type) { if (PHP_VERSION_ID >= 70200) { $typeName = $type->getName(); } else { $typeName = (string) $type; } $typeName = ltrim($typeName, '?'); if ($param->isDefaultValueAvailable()) { return new ClassNameWithOptionalValue($typeName, $param->getDefaultValue()); } return new ClassName($typeName); } if ($param->isDefaultValueAvailable()) { return new RawArgument($param->getDefaultValue()); } throw new NotFoundException(sprintf( 'Unable to resolve a value for parameter (%s) in the function/method (%s)', $name, $method->getName() )); }, $method->getParameters()); return $this->resolveArguments($arguments); } /** * @return ContainerInterface */ abstract public function getContainer() : ContainerInterface; /** * @return Container */ abstract public function getLeagueContainer() : Container; } lib/packages/League/Container/Argument/ArgumentResolverInterface.php 0000644 00000001453 15132754523 0021646 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\League\Container\Argument; use Automattic\WooCommerce\Vendor\League\Container\ContainerAwareInterface; use ReflectionFunctionAbstract; interface ArgumentResolverInterface extends ContainerAwareInterface { /** * Resolve an array of arguments to their concrete implementations. * * @param array $arguments * * @return array */ public function resolveArguments(array $arguments) : array; /** * Resolves the correct arguments to be passed to a method. * * @param ReflectionFunctionAbstract $method * @param array $args * * @return array */ public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []) : array; } src/Internal/DownloadPermissionsAdjuster.php 0000644 00000013527 15132754523 0015344 0 ustar 00 <?php /** * DownloadPermissionsAdjuster class file. */ namespace Automattic\WooCommerce\Internal; use Automattic\WooCommerce\Proxies\LegacyProxy; defined( 'ABSPATH' ) || exit; /** * Class to adjust download permissions on product save. */ class DownloadPermissionsAdjuster { /** * The downloads data store to use. * * @var WC_Data_Store */ private $downloads_data_store; /** * Class initialization, to be executed when the class is resolved by the container. * * @internal */ final public function init() { $this->downloads_data_store = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Data_Store::class, 'customer-download' ); add_action( 'adjust_download_permissions', array( $this, 'adjust_download_permissions' ), 10, 1 ); } /** * Schedule a download permissions adjustment for a product if necessary. * This should be executed whenever a product is saved. * * @param \WC_Product $product The product to schedule a download permission adjustments for. */ public function maybe_schedule_adjust_download_permissions( \WC_Product $product ) { $children_ids = $product->get_children(); if ( ! $children_ids ) { return; } $scheduled_action_args = array( $product->get_id() ); $already_scheduled_actions = WC()->call_function( 'as_get_scheduled_actions', array( 'hook' => 'adjust_download_permissions', 'args' => $scheduled_action_args, 'status' => \ActionScheduler_Store::STATUS_PENDING, ), 'ids' ); if ( empty( $already_scheduled_actions ) ) { WC()->call_function( 'as_schedule_single_action', WC()->call_function( 'time' ) + 1, 'adjust_download_permissions', $scheduled_action_args ); } } /** * Create additional download permissions for variations if necessary. * * When a simple downloadable product is converted to a variable product, * existing download permissions are still present in the database but they don't apply anymore. * This method creates additional download permissions for the variations based on * the old existing ones for the main product. * * The procedure is as follows. For each existing download permission for the parent product, * check if there's any variation offering the same file for download (the file URL, not name, is checked). * If that is found, check if an equivalent permission exists (equivalent means for the same file and with * the same order id and customer id). If no equivalent permission exists, create it. * * @param int $product_id The id of the product to check permissions for. */ public function adjust_download_permissions( int $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return; } $children_ids = $product->get_children(); if ( ! $children_ids ) { return; } $parent_downloads = $this->get_download_files_and_permissions( $product ); if ( ! $parent_downloads ) { return; } $children_with_downloads = array(); foreach ( $children_ids as $child_id ) { $child = wc_get_product( $child_id ); $children_with_downloads[ $child_id ] = $this->get_download_files_and_permissions( $child ); } foreach ( $parent_downloads['permission_data_by_file_order_user'] as $parent_file_order_and_user => $parent_download_data ) { foreach ( $children_with_downloads as $child_id => $child_download_data ) { $file_url = $parent_download_data['file']; $must_create_permission = // The variation offers the same file as the parent for download... in_array( $file_url, array_keys( $child_download_data['download_ids_by_file_url'] ), true ) && // ...but no equivalent download permission (same file URL, order id and user id) exists. ! array_key_exists( $parent_file_order_and_user, $child_download_data['permission_data_by_file_order_user'] ); if ( $must_create_permission ) { // The new child download permission is a copy of the parent's, // but with the product and download ids changed to match those of the variation. $new_download_data = $parent_download_data['data']; $new_download_data['product_id'] = $child_id; $new_download_data['download_id'] = $child_download_data['download_ids_by_file_url'][ $file_url ]; $this->downloads_data_store->create_from_data( $new_download_data ); } } } } /** * Get the existing downloadable files and download permissions for a given product. * The returned value is an array with two keys: * * - download_ids_by_file_url: an associative array of file url => download_id. * - permission_data_by_file_order_user: an associative array where key is "file_url:customer_id:order_id" and value is the full permission data set. * * @param \WC_Product $product The product to get the downloadable files and permissions for. * @return array[] Information about the downloadable files and permissions for the product. */ private function get_download_files_and_permissions( \WC_Product $product ) { $result = array( 'permission_data_by_file_order_user' => array(), 'download_ids_by_file_url' => array(), ); $downloads = $product->get_downloads(); foreach ( $downloads as $download ) { $result['download_ids_by_file_url'][ $download->get_file() ] = $download->get_id(); } $permissions = $this->downloads_data_store->get_downloads( array( 'product_id' => $product->get_id() ) ); foreach ( $permissions as $permission ) { $permission_data = (array) $permission->data; if ( array_key_exists( $permission_data['download_id'], $downloads ) ) { $file = $downloads[ $permission_data['download_id'] ]->get_file(); $data = array( 'file' => $file, 'data' => (array) $permission->data, ); $result['permission_data_by_file_order_user'][ "${file}:${permission_data['user_id']}:${permission_data['order_id']}" ] = $data; } } return $result; } } src/Internal/RestApiUtil.php 0000644 00000013304 15132754523 0012035 0 ustar 00 <?php /** * ApiUtil class file. */ namespace Automattic\WooCommerce\Internal; /** * Helper methos for the REST API. * * Class ApiUtil * * @package Automattic\WooCommerce\Internal */ class RestApiUtil { /** * Converts a create refund request from the public API format: * * [ * "reason" => "", * "api_refund" => "x", * "api_restock" => "x", * "line_items" => [ * "id" => "111", * "quantity" => 222, * "refund_total" => 333, * "refund_tax" => [ * [ * "id": "444", * "refund_total": 555 * ],... * ],... * ] * * ...to the internally used format: * * [ * "reason" => null, (if it's missing or any empty value, set as null) * "api_refund" => true, (if it's missing or non-bool, set as "true") * "api_restock" => true, (if it's missing or non-bool, set as "true") * "line_items" => [ (convert sequential array to associative based on "id") * "111" => [ * "qty" => 222, (rename "quantity" to "qty") * "refund_total" => 333, * "refund_tax" => [ (convert sequential array to associative based on "id" and "refund_total) * "444" => 555,... * ],... * ] * ] * * It also calculates the amount if missing and whenever possible, see maybe_calculate_refund_amount_from_line_items. * * The conversion is done in a way that if the request is already in the internal format, * then nothing is changed for compatibility. For example, if the line items array * is already an associative array or any of its elements * is missing the "id" key, then the entire array is left unchanged. * Same for the "refund_tax" array inside each line item. * * @param \WP_REST_Request $request The request to adjust. */ public static function adjust_create_refund_request_parameters( \WP_REST_Request &$request ) { if ( empty( $request['reason'] ) ) { $request['reason'] = null; } if ( ! is_bool( $request['api_refund'] ) ) { $request['api_refund'] = true; } if ( ! is_bool( $request['api_restock'] ) ) { $request['api_restock'] = true; } if ( empty( $request['line_items'] ) ) { $request['line_items'] = array(); } else { $request['line_items'] = self::adjust_line_items_for_create_refund_request( $request['line_items'] ); } if ( ! isset( $request['amount'] ) ) { $amount = self::calculate_refund_amount_from_line_items( $request ); if ( null !== $amount ) { $request['amount'] = strval( $amount ); } } } /** * Calculate the "amount" parameter for the request based on the amounts found in line items. * This will ONLY be possible if ALL of the following is true: * * - "line_items" in the request is a non-empty array. * - All line items have a "refund_total" field with a numeric value. * - All values inside "refund_tax" in all line items are a numeric value. * * The request is assumed to be in internal format already. * * @param \WP_REST_Request $request The request to maybe calculate the total amount for. * @return number|null The calculated amount, or null if it can't be calculated. */ private static function calculate_refund_amount_from_line_items( $request ) { $line_items = $request['line_items']; if ( ! is_array( $line_items ) || empty( $line_items ) ) { return null; } $amount = 0; foreach ( $line_items as $item ) { if ( ! isset( $item['refund_total'] ) || ! is_numeric( $item['refund_total'] ) ) { return null; } $amount += $item['refund_total']; if ( ! isset( $item['refund_tax'] ) ) { continue; } foreach ( $item['refund_tax'] as $tax ) { if ( ! is_numeric( $tax ) ) { return null; } $amount += $tax; } } return $amount; } /** * Convert the line items of a refund request to internal format (see adjust_create_refund_request_parameters). * * @param array $line_items The line items to convert. * @return array The converted line items. */ private static function adjust_line_items_for_create_refund_request( $line_items ) { if ( ! is_array( $line_items ) || empty( $line_items ) || self::is_associative( $line_items ) ) { return $line_items; } $new_array = array(); foreach ( $line_items as $item ) { if ( ! isset( $item['id'] ) ) { return $line_items; } if ( isset( $item['quantity'] ) && ! isset( $item['qty'] ) ) { $item['qty'] = $item['quantity']; } unset( $item['quantity'] ); if ( isset( $item['refund_tax'] ) ) { $item['refund_tax'] = self::adjust_taxes_for_create_refund_request_line_item( $item['refund_tax'] ); } $id = $item['id']; $new_array[ $id ] = $item; unset( $new_array[ $id ]['id'] ); } return $new_array; } /** * Adjust the taxes array from a line item in a refund request, see adjust_create_refund_parameters. * * @param array $taxes_array The array to adjust. * @return array The adjusted array. */ private static function adjust_taxes_for_create_refund_request_line_item( $taxes_array ) { if ( ! is_array( $taxes_array ) || empty( $taxes_array ) || self::is_associative( $taxes_array ) ) { return $taxes_array; } $new_array = array(); foreach ( $taxes_array as $item ) { if ( ! isset( $item['id'] ) || ! isset( $item['refund_total'] ) ) { return $taxes_array; } $id = $item['id']; $refund_total = $item['refund_total']; $new_array[ $id ] = $refund_total; } return $new_array; } /** * Is an array sequential or associative? * * @param array $array The array to check. * @return bool True if the array is associative, false if it's sequential. */ private static function is_associative( array $array ) { return array_keys( $array ) !== range( 0, count( $array ) - 1 ); } } src/Internal/ProductAttributesLookup/DataRegenerator.php 0000644 00000032722 15132754523 0017565 0 ustar 00 <?php /** * DataRegenerator class file. */ namespace Automattic\WooCommerce\Internal\ProductAttributesLookup; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; use Automattic\WooCommerce\Utilities\ArrayUtil; defined( 'ABSPATH' ) || exit; /** * This class handles the (re)generation of the product attributes lookup table. * It schedules the regeneration in small product batches by itself, so it can be used outside the * regular WooCommerce data regenerations mechanism. * * After the regeneration is completed a wp_wc_product_attributes_lookup table will exist with entries for * all the products that existed when initiate_regeneration was invoked; entries for products created after that * are supposed to be created/updated by the appropriate data store classes (or by the code that uses * the data store classes) whenever a product is created/updated. * * Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option * with a value of 'no' will have been created. * * This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents, * and another one for enabling or disabling the actual lookup table usage. */ class DataRegenerator { const PRODUCTS_PER_GENERATION_STEP = 10; /** * The data store to use. * * @var LookupDataStore */ private $data_store; /** * The lookup table name. * * @var string */ private $lookup_table_name; /** * DataRegenerator constructor. */ public function __construct() { global $wpdb; $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; add_filter( 'woocommerce_debug_tools', function( $tools ) { return $this->add_initiate_regeneration_entry_to_tools_array( $tools ); }, 1, 999 ); add_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback', function () { $this->run_regeneration_step_callback(); } ); } /** * Class initialization, invoked by the DI container. * * @internal * @param LookupDataStore $data_store The data store to use. */ final public function init( LookupDataStore $data_store ) { $this->data_store = $data_store; } /** * Initialize the regeneration procedure: * deletes the lookup table and related options if they exist, * then it creates the table and runs the first step of the regeneration process. * * This is the method that should be used as a callback for a data regeneration in wc-update-functions, e.g.: * * function wc_update_XX_regenerate_product_attributes_lookup_table() { * wc_get_container()->get(DataRegenerator::class)->initiate_regeneration(); * return false; * } * * (Note how we are returning "false" since the class handles the step scheduling by itself). */ public function initiate_regeneration() { $this->enable_or_disable_lookup_table_usage( false ); $this->delete_all_attributes_lookup_data(); $products_exist = $this->initialize_table_and_data(); if ( $products_exist ) { $this->enqueue_regeneration_step_run(); } else { $this->finalize_regeneration(); } } /** * Delete all the existing data related to the lookup table, including the table itself. * * Shortcut to run this method in case the debug tools UI isn't available or for quick debugging: * * wp eval "wc_get_container()->get(Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class)->delete_all_attributes_lookup_data();" */ public function delete_all_attributes_lookup_data() { global $wpdb; delete_option( 'woocommerce_attribute_lookup_enabled' ); delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); $this->data_store->unset_regeneration_in_progress_flag(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name ); } /** * Create the lookup table and initialize the options that will be temporarily used * while the regeneration is in progress. * * @return bool True if there's any product at all in the database, false otherwise. */ private function initialize_table_and_data() { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( ' CREATE TABLE ' . $this->lookup_table_name . '( product_id bigint(20) NOT NULL, product_or_parent_id bigint(20) NOT NULL, taxonomy varchar(32) NOT NULL, term_id bigint(20) NOT NULL, is_variation_attribute tinyint(1) NOT NULL, in_stock tinyint(1) NOT NULL ); ' ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared $last_existing_product_id = WC()->call_function( 'wc_get_products', array( 'return' => 'ids', 'limit' => 1, 'orderby' => array( 'ID' => 'DESC', ), ) ); if ( ! $last_existing_product_id ) { // No products exist, nothing to (re)generate. return false; } $this->data_store->set_regeneration_in_progress_flag(); update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', current( $last_existing_product_id ) ); update_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ); return true; } /** * Action scheduler callback, performs one regeneration step and then * schedules the next step if necessary. */ private function run_regeneration_step_callback() { if ( ! $this->data_store->regeneration_is_in_progress() ) { return; } $result = $this->do_regeneration_step(); if ( $result ) { $this->enqueue_regeneration_step_run(); } else { $this->finalize_regeneration(); } } /** * Enqueue one regeneration step in action scheduler. */ private function enqueue_regeneration_step_run() { $queue = WC()->get_instance_of( \WC_Queue::class ); $queue->schedule_single( WC()->call_function( 'time' ) + 1, 'woocommerce_run_product_attribute_lookup_regeneration_callback', array(), 'woocommerce-db-updates' ); } /** * Perform one regeneration step: grabs a chunk of products and creates * the appropriate entries for them in the lookup table. * * @return bool True if more steps need to be run, false otherwise. */ private function do_regeneration_step() { $last_products_page_processed = get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); $current_products_page = (int) $last_products_page_processed + 1; $product_ids = WC()->call_function( 'wc_get_products', array( 'limit' => self::PRODUCTS_PER_GENERATION_STEP, 'page' => $current_products_page, 'orderby' => array( 'ID' => 'ASC', ), 'return' => 'ids', ) ); if ( ! $product_ids ) { return false; } foreach ( $product_ids as $id ) { $this->data_store->create_data_for_product( $id ); } update_option( 'woocommerce_attribute_lookup_last_products_page_processed', $current_products_page ); $last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); return end( $product_ids ) < $last_product_id_to_process; } /** * Cleanup/final option setup after the regeneration has been completed. */ private function finalize_regeneration() { delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); update_option( 'woocommerce_attribute_lookup_enabled', 'no' ); $this->data_store->unset_regeneration_in_progress_flag(); } /** * Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page. * * @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter. * @return array The tools array with the entry added. */ private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) { if ( ! $this->data_store->is_feature_visible() ) { return $tools_array; } $lookup_table_exists = $this->data_store->check_lookup_table_exists(); $generation_is_in_progress = $this->data_store->regeneration_is_in_progress(); // Regenerate table. if ( $lookup_table_exists ) { $generate_item_name = __( 'Regenerate the product attributes lookup table', 'woocommerce' ); $generate_item_desc = __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ); $generate_item_return = __( 'Product attributes lookup table data is regenerating', 'woocommerce' ); $generate_item_button = __( 'Regenerate', 'woocommerce' ); } else { $generate_item_name = __( 'Create and fill product attributes lookup table', 'woocommerce' ); $generate_item_desc = __( 'This tool will create the product attributes lookup table data and fill it with existing products data. This process may take a while.', 'woocommerce' ); $generate_item_return = __( 'Product attributes lookup table is being filled', 'woocommerce' ); $generate_item_button = __( 'Create', 'woocommerce' ); } $entry = array( 'name' => $generate_item_name, 'desc' => $generate_item_desc, 'requires_refresh' => true, 'callback' => function() use ( $generate_item_return ) { $this->initiate_regeneration_from_tools_page(); return $generate_item_return; }, ); if ( $lookup_table_exists ) { $entry['selector'] = array( 'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ), 'class' => 'wc-product-search', 'search_action' => 'woocommerce_json_search_products', 'name' => 'regenerate_product_attribute_lookup_data_product_id', 'placeholder' => esc_attr__( 'Search for a product…', 'woocommerce' ), ); } if ( $generation_is_in_progress ) { $entry['button'] = sprintf( /* translators: %d: How many products have been processed so far. */ __( 'Filling in progress (%d)', 'woocommerce' ), get_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ) * self::PRODUCTS_PER_GENERATION_STEP ); $entry['disabled'] = true; } else { $entry['button'] = $generate_item_button; } $tools_array['regenerate_product_attributes_lookup_table'] = $entry; if ( $lookup_table_exists ) { // Delete the table. $tools_array['delete_product_attributes_lookup_table'] = array( 'name' => __( 'Delete the product attributes lookup table', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This will delete the product attributes lookup table. You can create it again with the "Create and fill product attributes lookup table" tool.', 'woocommerce' ) ), 'button' => __( 'Delete', 'woocommerce' ), 'requires_refresh' => true, 'callback' => function () { $this->delete_all_attributes_lookup_data(); return __( 'Product attributes lookup table has been deleted.', 'woocommerce' ); }, ); } return $tools_array; } /** * Callback to initiate the regeneration process from the Status - Tools page. * * @throws \Exception The regeneration is already in progress. */ private function initiate_regeneration_from_tools_page() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) ) { throw new \Exception( 'Invalid nonce' ); } if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) { $product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id']; $this->check_can_do_lookup_table_regeneration( $product_id ); $this->data_store->create_data_for_product( $product_id ); } else { $this->check_can_do_lookup_table_regeneration(); $this->initiate_regeneration(); } } /** * Enable or disable the actual lookup table usage. * * @param bool $enable True to enable, false to disable. * @throws \Exception A lookup table regeneration is currently in progress. */ private function enable_or_disable_lookup_table_usage( $enable ) { if ( $this->data_store->regeneration_is_in_progress() ) { throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." ); } update_option( 'woocommerce_attribute_lookup_enabled', $enable ? 'yes' : 'no' ); } /** * Check if everything is good to go to perform a complete or per product lookup table data regeneration * and throw an exception if not. * * @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible. * @throws \Exception Something prevents the regeneration from starting. */ private function check_can_do_lookup_table_regeneration( $product_id = null ) { if ( ! $this->data_store->is_feature_visible() ) { throw new \Exception( "Can't do product attribute lookup data regeneration: feature is not visible" ); } if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) { throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" ); } if ( $this->data_store->regeneration_is_in_progress() ) { throw new \Exception( "Can't do product attribute lookup data regeneration: regeneration is already in progress" ); } if ( $product_id && ! wc_get_product( $product_id ) ) { throw new \Exception( "Can't do product attribute lookup data regeneration: product doesn't exist" ); } } } src/Internal/ProductAttributesLookup/LookupDataStore.php 0000644 00000052600 15132754523 0017573 0 ustar 00 <?php /** * LookupDataStore class file. */ namespace Automattic\WooCommerce\Internal\ProductAttributesLookup; use Automattic\WooCommerce\Utilities\ArrayUtil; defined( 'ABSPATH' ) || exit; /** * Data store class for the product attributes lookup table. */ class LookupDataStore { /** * Types of updates to perform depending on the current changest */ const ACTION_NONE = 0; const ACTION_INSERT = 1; const ACTION_UPDATE_STOCK = 2; const ACTION_DELETE = 3; /** * The lookup table name. * * @var string */ private $lookup_table_name; /** * Is the feature visible? * * @var bool */ private $is_feature_visible; /** * LookupDataStore constructor. Makes the feature hidden by default. */ public function __construct() { global $wpdb; $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; $this->is_feature_visible = false; $this->init_hooks(); } /** * Initialize the hooks used by the class. */ private function init_hooks() { add_action( 'woocommerce_run_product_attribute_lookup_update_callback', function ( $product_id, $action ) { $this->run_update_callback( $product_id, $action ); }, 10, 2 ); add_filter( 'woocommerce_get_sections_products', function ( $products ) { if ( $this->is_feature_visible() && $this->check_lookup_table_exists() ) { $products['advanced'] = __( 'Advanced', 'woocommerce' ); } return $products; }, 100, 1 ); add_filter( 'woocommerce_get_settings_products', function ( $settings, $section_id ) { if ( 'advanced' === $section_id && $this->is_feature_visible() && $this->check_lookup_table_exists() ) { $title_item = array( 'title' => __( 'Product attributes lookup table', 'woocommerce' ), 'type' => 'title', ); $regeneration_is_in_progress = $this->regeneration_is_in_progress(); if ( $regeneration_is_in_progress ) { $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' ); } $settings[] = $title_item; if ( ! $regeneration_is_in_progress ) { $settings[] = array( 'title' => __( 'Enable table usage', 'woocommerce' ), 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ), 'id' => 'woocommerce_attribute_lookup_enabled', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', ); $settings[] = array( 'title' => __( 'Direct updates', 'woocommerce' ), 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ), 'id' => 'woocommerce_attribute_lookup_direct_updates', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', ); } $settings[] = array( 'type' => 'sectionend' ); } return $settings; }, 100, 2 ); } /** * Check if the lookup table exists in the database. * * TODO: Remove this method and references to it once the lookup table is created via data migration. * * @return bool */ public function check_lookup_table_exists() { global $wpdb; $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return $this->lookup_table_name === $wpdb->get_var( $query ); } /** * Checks if the feature is visible (so that dedicated entries will be added to the debug tools page). * * @return bool True if the feature is visible. */ public function is_feature_visible() { return $this->is_feature_visible; } /** * Makes the feature visible, so that dedicated entries will be added to the debug tools page. */ public function show_feature() { $this->is_feature_visible = true; } /** * Hides the feature, so that no entries will be added to the debug tools page. */ public function hide_feature() { $this->is_feature_visible = false; } /** * Get the name of the lookup table. * * @return string */ public function get_lookup_table_name() { return $this->lookup_table_name; } /** * Insert/update the appropriate lookup table entries for a new or modified product or variation. * This must be invoked after a product or a variation is created (including untrashing and duplication) * or modified. * * @param int|\WC_Product $product Product object or product id. * @param null|array $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created. */ public function on_product_changed( $product, $changeset = null ) { if ( ! $this->check_lookup_table_exists() ) { return; } if ( ! is_a( $product, \WC_Product::class ) ) { $product = WC()->call_function( 'wc_get_product', $product ); } $action = $this->get_update_action( $changeset ); if ( self::ACTION_NONE !== $action ) { $this->maybe_schedule_update( $product->get_id(), $action ); } } /** * Schedule an update of the product attributes lookup table for a given product. * If an update for the same action is already scheduled, nothing is done. * * If the 'woocommerce_attribute_lookup_direct_update' option is set to 'yes', * the update is done directly, without scheduling. * * @param int $product_id The product id to schedule the update for. * @param int $action The action to perform, one of the ACTION_ constants. */ private function maybe_schedule_update( int $product_id, int $action ) { if ( 'yes' === get_option( 'woocommerce_attribute_lookup_direct_updates' ) ) { $this->run_update_callback( $product_id, $action ); return; } $args = array( $product_id, $action ); $queue = WC()->get_instance_of( \WC_Queue::class ); $already_scheduled = $queue->search( array( 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback', 'args' => $args, 'status' => \ActionScheduler_Store::STATUS_PENDING, ), 'ids' ); if ( empty( $already_scheduled ) ) { $queue->schedule_single( WC()->call_function( 'time' ) + 1, 'woocommerce_run_product_attribute_lookup_update_callback', $args, 'woocommerce-db-updates' ); } } /** * Perform an update of the lookup table for a specific product. * * @param int $product_id The product id to perform the update for. * @param int $action The action to perform, one of the ACTION_ constants. */ private function run_update_callback( int $product_id, int $action ) { if ( ! $this->check_lookup_table_exists() ) { return; } $product = WC()->call_function( 'wc_get_product', $product_id ); if ( ! $product ) { $action = self::ACTION_DELETE; } switch ( $action ) { case self::ACTION_INSERT: $this->delete_data_for( $product_id ); $this->create_data_for( $product ); break; case self::ACTION_UPDATE_STOCK: $this->update_stock_status_for( $product ); break; case self::ACTION_DELETE: $this->delete_data_for( $product_id ); break; } } /** * Determine the type of action to perform depending on the received changeset. * * @param array|null $changeset The changeset received by on_product_changed. * @return int One of the ACTION_ constants. */ private function get_update_action( $changeset ) { if ( is_null( $changeset ) ) { // No changeset at all means that the product is new. return self::ACTION_INSERT; } $keys = array_keys( $changeset ); // Order matters: // - The change with the most precedence is a change in catalog visibility // (which will result in all data being regenerated or deleted). // - Then a change in attributes (all data will be regenerated). // - And finally a change in stock status (existing data will be updated). // Thus these conditions must be checked in that same order. if ( in_array( 'catalog_visibility', $keys, true ) ) { $new_visibility = $changeset['catalog_visibility']; if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) { return self::ACTION_INSERT; } else { return self::ACTION_DELETE; } } if ( in_array( 'attributes', $keys, true ) ) { return self::ACTION_INSERT; } if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) { return self::ACTION_UPDATE_STOCK; } return self::ACTION_NONE; } /** * Update the stock status of the lookup table entries for a given product. * * @param \WC_Product $product The product to update the entries for. */ private function update_stock_status_for( \WC_Product $product ) { global $wpdb; $in_stock = $product->is_in_stock(); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( 'UPDATE ' . $this->lookup_table_name . ' SET in_stock = %d WHERE product_id = %d', $in_stock ? 1 : 0, $product->get_id() ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } /** * Delete the lookup table contents related to a given product or variation, * if it's a variable product it deletes the information for variations too. * This must be invoked after a product or a variation is trashed or deleted. * * @param int|\WC_Product $product Product object or product id. */ public function on_product_deleted( $product ) { if ( ! $this->check_lookup_table_exists() ) { return; } if ( is_a( $product, \WC_Product::class ) ) { $product_id = $product->get_id(); } else { $product_id = $product; } $this->maybe_schedule_update( $product_id, self::ACTION_DELETE ); } /** * Create the lookup data for a given product, if a variable product is passed * the information is created for all of its variations. * This method is intended to be called from the data regenerator. * * @param int|WC_Product $product Product object or id. * @throws \Exception A variation object is passed. */ public function create_data_for_product( $product ) { if ( ! is_a( $product, \WC_Product::class ) ) { $product = WC()->call_function( 'wc_get_product', $product ); } if ( $this->is_variation( $product ) ) { throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." ); } $this->delete_data_for( $product->get_id() ); $this->create_data_for( $product ); } /** * Create lookup table data for a given product. * * @param \WC_Product $product The product to create the data for. */ private function create_data_for( \WC_Product $product ) { if ( $this->is_variation( $product ) ) { $this->create_data_for_variation( $product ); } elseif ( $this->is_variable_product( $product ) ) { $this->create_data_for_variable_product( $product ); } else { $this->create_data_for_simple_product( $product ); } } /** * Delete all the lookup table entries for a given product, * if it's a variable product information for variations is deleted too. * * @param int $product_id Simple product id, or main/parent product id for variable products. */ private function delete_data_for( int $product_id ) { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_id = %d OR product_or_parent_id = %d', $product_id, $product_id ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } /** * Create lookup table entries for a simple (non variable) product. * Assumes that no entries exist yet. * * @param \WC_Product $product The product to create the entries for. */ private function create_data_for_simple_product( \WC_Product $product ) { $product_attributes_data = $this->get_attribute_taxonomies( $product ); $has_stock = $product->is_in_stock(); $product_id = $product->get_id(); foreach ( $product_attributes_data as $taxonomy => $data ) { $term_ids = $data['term_ids']; foreach ( $term_ids as $term_id ) { $this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock ); } } } /** * Create lookup table entries for a variable product. * Assumes that no entries exist yet. * * @param \WC_Product_Variable $product The product to create the entries for. */ private function create_data_for_variable_product( \WC_Product_Variable $product ) { $product_attributes_data = $this->get_attribute_taxonomies( $product ); $variation_attributes_data = array_filter( $product_attributes_data, function( $item ) { return $item['used_for_variations']; } ); $non_variation_attributes_data = array_filter( $product_attributes_data, function( $item ) { return ! $item['used_for_variations']; } ); $main_product_has_stock = $product->is_in_stock(); $main_product_id = $product->get_id(); foreach ( $non_variation_attributes_data as $taxonomy => $data ) { $term_ids = $data['term_ids']; foreach ( $term_ids as $term_id ) { $this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock ); } } $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) ); $variations = $this->get_variations_of( $product ); foreach ( $variation_attributes_data as $taxonomy => $data ) { foreach ( $variations as $variation ) { $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache ); } } } /** * Create all the necessary lookup data for a given variation. * * @param \WC_Product_Variation $variation The variation to create entries for. */ private function create_data_for_variation( \WC_Product_Variation $variation ) { $main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() ); $product_attributes_data = $this->get_attribute_taxonomies( $main_product ); $variation_attributes_data = array_filter( $product_attributes_data, function( $item ) { return $item['used_for_variations']; } ); $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) ); foreach ( $variation_attributes_data as $taxonomy => $data ) { $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache ); } } /** * Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids. * * @param \WC_Product_Variation $variation The variation to create entries for. * @param string $taxonomy The taxonomy to create the entries for. * @param int $main_product_id The parent product id. * @param array $term_ids The term ids to create entries for. * @param array $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'. */ private function insert_lookup_table_data_for_variation( \WC_Product_Variation $variation, string $taxonomy, int $main_product_id, array $term_ids, array $term_ids_by_slug_cache ) { $variation_id = $variation->get_id(); $variation_has_stock = $variation->is_in_stock(); $variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache ); if ( $variation_definition_term_id ) { $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock ); } else { $term_ids_for_taxonomy = $term_ids; foreach ( $term_ids_for_taxonomy as $term_id ) { $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock ); } } } /** * Get a cache of term ids by slug for a set of taxonomies, with this format: * * [ * 'taxonomy' => [ * 'slug_1' => id_1, * 'slug_2' => id_2, * ... * ], ... * ] * * @param array $taxonomies List of taxonomies to build the cache for. * @return array A dictionary of taxonomies => dictionary of term slug => term id. */ private function get_term_ids_by_slug_cache( $taxonomies ) { $result = array(); foreach ( $taxonomies as $taxonomy ) { $terms = WC()->call_function( 'get_terms', array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'fields' => 'id=>slug', ) ); $result[ $taxonomy ] = array_flip( $terms ); } return $result; } /** * Get the id of the term that defines a variation for a given taxonomy, * or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition) * * @param \WC_Product_Variation $variation The variation to get the defining term id for. * @param string $taxonomy The taxonomy to get the defining term id for. * @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache. * @return int|null The term id, or null if there's no defining id for that taxonomy in that variation. */ private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) { $variation_attributes = $variation->get_attributes(); $term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy ); if ( $term_slug ) { return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ]; } else { return null; } } /** * Get the variations of a given variable product. * * @param \WC_Product_Variable $product The product to get the variations for. * @return array An array of WC_Product_Variation objects. */ private function get_variations_of( \WC_Product_Variable $product ) { $variation_ids = $product->get_children(); return array_map( function( $id ) { return WC()->call_function( 'wc_get_product', $id ); }, $variation_ids ); } /** * Check if a given product is a variable product. * * @param \WC_Product $product The product to check. * @return bool True if it's a variable product, false otherwise. */ private function is_variable_product( \WC_Product $product ) { return is_a( $product, \WC_Product_Variable::class ); } /** * Check if a given product is a variation. * * @param \WC_Product $product The product to check. * @return bool True if it's a variation, false otherwise. */ private function is_variation( \WC_Product $product ) { return is_a( $product, \WC_Product_Variation::class ); } /** * Return the list of taxonomies used for variations on a product together with * the associated term ids, with the following format: * * [ * 'taxonomy_name' => * [ * 'term_ids' => [id, id, ...], * 'used_for_variations' => true|false * ], ... * ] * * @param \WC_Product $product The product to get the attribute taxonomies for. * @return array Information about the attribute taxonomies of the product. */ private function get_attribute_taxonomies( \WC_Product $product ) { $product_attributes = $product->get_attributes(); $result = array(); foreach ( $product_attributes as $taxonomy_name => $attribute_data ) { if ( ! $attribute_data->get_id() ) { // Custom product attribute, not suitable for attribute-based filtering. continue; } $result[ $taxonomy_name ] = array( 'term_ids' => $attribute_data->get_options(), 'used_for_variations' => $attribute_data->get_variation(), ); } return $result; } /** * Insert one entry in the lookup table. * * @param int $product_id The product id. * @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations. * @param string $taxonomy Taxonomy name. * @param int $term_id Term id. * @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations. * @param bool $has_stock True if the product is in stock. */ private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( 'INSERT INTO ' . $this->lookup_table_name . ' ( product_id, product_or_parent_id, taxonomy, term_id, is_variation_attribute, in_stock) VALUES ( %d, %d, %s, %d, %d, %d )', $product_id, $product_or_parent_id, $taxonomy, $term_id, $is_variation_attribute ? 1 : 0, $has_stock ? 1 : 0 ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } /** * Tells if a lookup table regeneration is currently in progress. * * @return bool True if a lookup table regeneration is already in progress. */ public function regeneration_is_in_progress() { return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ); } /** * Set a permanent flag (via option) indicating that the lookup table regeneration is in process. */ public function set_regeneration_in_progress_flag() { update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' ); } /** * Remove the flag indicating that the lookup table regeneration is in process. */ public function unset_regeneration_in_progress_flag() { delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' ); } } src/Internal/ProductAttributesLookup/Filterer.php 0000644 00000027011 15132754523 0016265 0 ustar 00 <?php /** * Filterer class file. */ namespace Automattic\WooCommerce\Internal\ProductAttributesLookup; defined( 'ABSPATH' ) || exit; /** * Helper class for filtering products using the product attributes lookup table. */ class Filterer { /** * The product attributes lookup data store to use. * * @var LookupDataStore */ private $data_store; /** * The name of the product attributes lookup table. * * @var string */ private $lookup_table_name; /** * Class initialization, invoked by the DI container. * * @internal * @param LookupDataStore $data_store The data store to use. */ final public function init( LookupDataStore $data_store ) { $this->data_store = $data_store; $this->lookup_table_name = $data_store->get_lookup_table_name(); } /** * Checks if the product attribute filtering via lookup table feature is enabled. * * @return bool */ public function filtering_via_lookup_table_is_active() { return 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' ); } /** * Adds post clauses for filtering via lookup table. * This method should be invoked within a 'posts_clauses' filter. * * @param array $args Product query clauses as supplied to the 'posts_clauses' filter. * @param \WP_Query $wp_query Current product query as supplied to the 'posts_clauses' filter. * @param array $attributes_to_filter_by Attribute filtering data as generated by WC_Query::get_layered_nav_chosen_attributes. * @return array The updated product query clauses. */ public function filter_by_attribute_post_clauses( array $args, \WP_Query $wp_query, array $attributes_to_filter_by ) { global $wpdb; if ( ! $wp_query->is_main_query() || ! $this->filtering_via_lookup_table_is_active() ) { return $args; } $clause_root = " {$wpdb->prefix}posts.ID IN ("; if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $in_stock_clause = ' AND in_stock = 1'; } else { $in_stock_clause = ''; } foreach ( $attributes_to_filter_by as $taxonomy => $data ) { $all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); $term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' ); $term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) ); $term_ids_to_filter_by = array_map( 'absint', $term_ids_to_filter_by ); $term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')'; $is_and_query = 'and' === $data['query_type']; $count = count( $term_ids_to_filter_by ); if ( 0 !== $count ) { if ( $is_and_query ) { $clauses[] = " {$clause_root} SELECT product_or_parent_id FROM {$this->lookup_table_name} lt WHERE is_variation_attribute=0 {$in_stock_clause} AND term_id in {$term_ids_to_filter_by_list} GROUP BY product_id HAVING COUNT(product_id)={$count} UNION SELECT product_or_parent_id FROM {$this->lookup_table_name} lt WHERE is_variation_attribute=1 {$in_stock_clause} AND term_id in {$term_ids_to_filter_by_list} )"; } else { $clauses[] = " {$clause_root} SELECT product_or_parent_id FROM {$this->lookup_table_name} lt WHERE term_id in {$term_ids_to_filter_by_list} {$in_stock_clause} )"; } } } if ( ! empty( $clauses ) ) { $args['where'] .= ' AND (' . join( ' AND ', $clauses ) . ')'; } elseif ( ! empty( $attributes_to_filter_by ) ) { $args['where'] .= ' AND 1=0'; } return $args; } /** * Count products within certain terms, taking the main WP query into consideration, * for the WC_Widget_Layered_Nav widget. * * This query allows counts to be generated based on the viewed products, not all products. * * @param array $term_ids Term IDs. * @param string $taxonomy Taxonomy. * @param string $query_type Query Type. * @return array */ public function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { global $wpdb; $use_lookup_table = $this->filtering_via_lookup_table_is_active(); $tax_query = \WC_Query::get_main_tax_query(); $meta_query = \WC_Query::get_main_meta_query(); if ( 'or' === $query_type ) { foreach ( $tax_query as $key => $query ) { if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { unset( $tax_query[ $key ] ); } } } $meta_query = new \WP_Meta_Query( $meta_query ); $tax_query = new \WP_Tax_Query( $tax_query ); if ( $use_lookup_table ) { $query = $this->get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ); } else { $query = $this->get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ); } $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); $query_sql = implode( ' ', $query ); // We have a query - let's see if cached results of this query already exist. $query_hash = md5( $query_sql ); // Maybe store a transient of the count values. $cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); if ( true === $cache ) { $cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) ); } else { $cached_counts = array(); } if ( ! isset( $cached_counts[ $query_hash ] ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $query_sql, ARRAY_A ); $counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); $cached_counts[ $query_hash ] = $counts; if ( true === $cache ) { set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS ); } } return array_map( 'absint', (array) $cached_counts[ $query_hash ] ); } /** * Get the query for counting products by terms using the product attributes lookup table. * * @param \WP_Tax_Query $tax_query The current main tax query. * @param \WP_Meta_Query $meta_query The current main meta query. * @param string $taxonomy The attribute name to get the term counts for. * @param string $term_ids The term ids to include in the search. * @return array An array of SQL query parts. */ private function get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ) { global $wpdb; $meta_query_sql = $meta_query->get_sql( 'post', $this->lookup_table_name, 'product_or_parent_id' ); $tax_query_sql = $tax_query->get_sql( $this->lookup_table_name, 'product_or_parent_id' ); $hide_out_of_stock = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ); $in_stock_clause = $hide_out_of_stock ? ' AND in_stock = 1' : ''; $query['select'] = 'SELECT COUNT(DISTINCT product_or_parent_id) as term_count, term_id as term_count_id'; $query['from'] = "FROM {$this->lookup_table_name}"; $query['join'] = " {$tax_query_sql['join']} {$meta_query_sql['join']} INNER JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$this->lookup_table_name}.product_or_parent_id"; $term_ids_sql = $this->get_term_ids_sql( $term_ids ); $query['where'] = " WHERE {$wpdb->posts}.post_type IN ( 'product' ) AND {$wpdb->posts}.post_status = 'publish' {$tax_query_sql['where']} {$meta_query_sql['where']} AND {$this->lookup_table_name}.taxonomy='{$taxonomy}' AND {$this->lookup_table_name}.term_id IN $term_ids_sql {$in_stock_clause}"; if ( ! empty( $term_ids ) ) { $attributes_to_filter_by = \WC_Query::get_layered_nav_chosen_attributes(); if ( ! empty( $attributes_to_filter_by ) ) { $and_term_ids = array(); $or_term_ids = array(); foreach ( $attributes_to_filter_by as $taxonomy => $data ) { $all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); $term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' ); $term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) ); if ( 'and' === $data['query_type'] ) { $and_term_ids = array_merge( $and_term_ids, $term_ids_to_filter_by ); } else { $or_term_ids = array_merge( $or_term_ids, $term_ids_to_filter_by ); } } if ( ! empty( $and_term_ids ) ) { $terms_count = count( $and_term_ids ); $term_ids_list = '(' . join( ',', $and_term_ids ) . ')'; $query['where'] .= " AND product_or_parent_id IN ( SELECT product_or_parent_id FROM {$this->lookup_table_name} lt WHERE is_variation_attribute=0 {$in_stock_clause} AND term_id in {$term_ids_list} GROUP BY product_id HAVING COUNT(product_id)={$terms_count} UNION SELECT product_or_parent_id FROM {$this->lookup_table_name} lt WHERE is_variation_attribute=1 {$in_stock_clause} AND term_id in {$term_ids_list} )"; } if ( ! empty( $or_term_ids ) ) { $term_ids_list = '(' . join( ',', $or_term_ids ) . ')'; $query['where'] .= " AND product_or_parent_id IN ( SELECT product_or_parent_id FROM {$this->lookup_table_name} WHERE term_id in {$term_ids_list} {$in_stock_clause} )"; } } else { $query['where'] .= $in_stock_clause; } } elseif ( $hide_out_of_stock ) { $query['where'] .= " AND {$this->lookup_table_name}.in_stock=1"; } $search_query_sql = \WC_Query::get_main_search_query_sql(); if ( $search_query_sql ) { $query['where'] .= ' AND ' . $search_query_sql; } $query['group_by'] = 'GROUP BY terms.term_id'; $query['group_by'] = "GROUP BY {$this->lookup_table_name}.term_id"; return $query; } /** * Get the query for counting products by terms NOT using the product attributes lookup table. * * @param \WP_Tax_Query $tax_query The current main tax query. * @param \WP_Meta_Query $meta_query The current main meta query. * @param string $term_ids The term ids to include in the search. * @return array An array of SQL query parts. */ private function get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ) { global $wpdb; $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); // Generate query. $query = array(); $query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id"; $query['from'] = "FROM {$wpdb->posts}"; $query['join'] = " INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) INNER JOIN {$wpdb->terms} AS terms USING( term_id ) " . $tax_query_sql['join'] . $meta_query_sql['join']; $term_ids_sql = $this->get_term_ids_sql( $term_ids ); $query['where'] = " WHERE {$wpdb->posts}.post_type IN ( 'product' ) AND {$wpdb->posts}.post_status = 'publish' {$tax_query_sql['where']} {$meta_query_sql['where']} AND terms.term_id IN $term_ids_sql"; $search_query_sql = \WC_Query::get_main_search_query_sql(); if ( $search_query_sql ) { $query['where'] .= ' AND ' . $search_query_sql; } $query['group_by'] = 'GROUP BY terms.term_id'; return $query; } /** * Formats a list of term ids as "(id,id,id)". * * @param array $term_ids The list of terms to format. * @return string The formatted list. */ private function get_term_ids_sql( $term_ids ) { return '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; } } src/Internal/AssignDefaultCategory.php 0000644 00000003631 15132754523 0014061 0 ustar 00 <?php /** * AssignDefaultCategory class file. */ namespace Automattic\WooCommerce\Internal; defined( 'ABSPATH' ) || exit; /** * Class to assign default category to products. */ class AssignDefaultCategory { /** * Class initialization, to be executed when the class is resolved by the container. * * @internal */ final public function init() { add_action( 'wc_schedule_update_product_default_cat', array( $this, 'maybe_assign_default_product_cat' ) ); } /** * When a product category is deleted, we need to check * if the product has no categories assigned. Then assign * it a default category. We delay this with a scheduled * action job to not block the response. * * @return void */ public function schedule_action() { WC()->queue()->schedule_single( time(), 'wc_schedule_update_product_default_cat', array(), 'wc_update_product_default_cat' ); } /** * Assigns default product category for products * that have no categories. * * @return void */ public function maybe_assign_default_product_cat() { global $wpdb; $default_category = get_option( 'default_product_cat', 0 ); if ( $default_category ) { $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id) SELECT DISTINCT posts.ID, %s FROM {$wpdb->posts} posts LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} term_relationships LEFT JOIN {$wpdb->term_taxonomy} term_taxonomy ON term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id WHERE term_taxonomy.taxonomy = 'product_cat' ) AS tax_query ON posts.ID = tax_query.object_id WHERE posts.post_type = 'product' AND tax_query.object_id IS NULL", $default_category ) ); wp_cache_flush(); delete_transient( 'wc_term_counts' ); wp_update_term_count_now( array( $default_category ), 'product_cat' ); } } } src/Internal/RestockRefundedItemsAdjuster.php 0000644 00000004121 15132754523 0015420 0 ustar 00 <?php /** * RestockRefundedItemsAdjuster class file. */ namespace Automattic\WooCommerce\Internal; use Automattic\WooCommerce\Proxies\LegacyProxy; defined( 'ABSPATH' ) || exit; /** * Class to adjust or initialize the restock refunded items. */ class RestockRefundedItemsAdjuster { /** * The order factory to use. * * @var WC_Order_Factory */ private $order_factory; /** * Class initialization, to be executed when the class is resolved by the container. * * @internal */ final public function init() { $this->order_factory = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Order_Factory::class ); add_action( 'woocommerce_before_save_order_items', array( $this, 'initialize_restock_refunded_items' ), 10, 2 ); } /** * Initializes the restock refunded items meta for order version less than 5.5. * * @see https://github.com/woocommerce/woocommerce/issues/29502 * * @param int $order_id Order ID. * @param array $items Order items to save. */ public function initialize_restock_refunded_items( $order_id, $items ) { $order = wc_get_order( $order_id ); $order_version = $order->get_version(); if ( version_compare( $order_version, '5.5', '>=' ) ) { return; } // If there are no refund lines, then this migration isn't necessary because restock related meta's wouldn't be set. if ( 0 === count( $order->get_refunds() ) ) { return; } if ( isset( $items['order_item_id'] ) ) { foreach ( $items['order_item_id'] as $item_id ) { $item = $this->order_factory::get_order_item( absint( $item_id ) ); if ( ! $item ) { continue; } if ( 'line_item' !== $item->get_type() ) { continue; } // There could be code paths in custom code which don't update version number but still update the items. if ( '' !== $item->get_meta( '_restock_refunded_items', true ) ) { continue; } $refunded_item_quantity = abs( $order->get_qty_refunded_for_item( $item->get_id() ) ); $item->add_meta_data( '_restock_refunded_items', $refunded_item_quantity, false ); $item->save(); } } } } src/Internal/DependencyManagement/ExtendedContainer.php 0000644 00000013676 15132754523 0017322 0 ustar 00 <?php /** * ExtendedContainer class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement; use Automattic\WooCommerce\Utilities\StringUtil; use Automattic\WooCommerce\Vendor\League\Container\Container as BaseContainer; use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface; /** * This class extends the original League's Container object by adding some functionality * that we need for WooCommerce. */ class ExtendedContainer extends BaseContainer { /** * The root namespace of all WooCommerce classes in the `src` directory. * * @var string */ private $woocommerce_namespace = 'Automattic\\WooCommerce\\'; /** * Whitelist of classes that we can register using the container * despite not belonging to the WooCommerce root namespace. * * In general we allow only the registration of classes in the * WooCommerce root namespace to prevent registering 3rd party code * (which doesn't really belong to this container) or old classes * (which may be eventually deprecated, also the LegacyProxy * should be used for those). * * @var string[] */ private $registration_whitelist = array( \Psr\Container\ContainerInterface::class, ); /** * Register a class in the container. * * @param string $class_name Class name. * @param mixed $concrete How to resolve the class with `get`: a factory callback, a concrete instance, another class name, or null to just create an instance of the class. * @param bool|null $shared Whether the resolution should be performed only once and cached. * * @return DefinitionInterface The generated definition for the container. * @throws ContainerException Invalid parameters. */ public function add( string $class_name, $concrete = null, bool $shared = null ) : DefinitionInterface { if ( ! $this->is_class_allowed( $class_name ) ) { throw new ContainerException( "You cannot add '$class_name', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); } $concrete_class = $this->get_class_from_concrete( $concrete ); if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) { throw new ContainerException( "You cannot add concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); } // We want to use a definition class that does not support constructor injection to avoid accidental usage. if ( ! $concrete instanceof DefinitionInterface ) { $concrete = new Definition( $class_name, $concrete ); } return parent::add( $class_name, $concrete, $shared ); } /** * Replace an existing registration with a different concrete. * * @param string $class_name The class name whose definition will be replaced. * @param mixed $concrete The new concrete (same as "add"). * * @return DefinitionInterface The modified definition. * @throws ContainerException Invalid parameters. */ public function replace( string $class_name, $concrete ) : DefinitionInterface { if ( ! $this->has( $class_name ) ) { throw new ContainerException( "The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." ); } $concrete_class = $this->get_class_from_concrete( $concrete ); if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) && ! $this->is_anonymous_class( $concrete_class ) ) { throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); } return $this->extend( $class_name )->setConcrete( $concrete ); } /** * Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again. */ public function reset_all_resolved() { foreach ( $this->definitions->getIterator() as $definition ) { // setConcrete causes the cached resolved value to be forgotten. $concrete = $definition->getConcrete(); $definition->setConcrete( $concrete ); } } /** * Get an instance of a registered class. * * @param string $id The class name. * @param bool $new True to generate a new instance even if the class was registered as shared. * * @return object An instance of the requested class. * @throws ContainerException Attempt to get an instance of a non-namespaced class. */ public function get( $id, bool $new = false ) { if ( false === strpos( $id, '\\' ) ) { throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" ); } return parent::get( $id, $new ); } /** * Gets the class from the concrete regardless of type. * * @param mixed $concrete The concrete that we want the class from.. * * @return string|null The class from the concrete if one is available, null otherwise. */ protected function get_class_from_concrete( $concrete ) { if ( is_object( $concrete ) && ! is_callable( $concrete ) ) { if ( $concrete instanceof DefinitionInterface ) { return $this->get_class_from_concrete( $concrete->getConcrete() ); } return get_class( $concrete ); } if ( is_string( $concrete ) && class_exists( $concrete ) ) { return $concrete; } return null; } /** * Checks to see whether or not a class is allowed to be registered. * * @param string $class_name The class to check. * * @return bool True if the class is allowed to be registered, false otherwise. */ protected function is_class_allowed( string $class_name ): bool { return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true ); } /** * Check if a class name corresponds to an anonymous class. * * @param string $class_name The class name to check. * @return bool True if the name corresponds to an anonymous class. */ protected function is_anonymous_class( string $class_name ): bool { return StringUtil::starts_with( $class_name, 'class@anonymous' ); } } src/Internal/DependencyManagement/AbstractServiceProvider.php 0000644 00000016177 15132754523 0020515 0 ustar 00 <?php /** * AbstractServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement; use Automattic\WooCommerce\Vendor\League\Container\Argument\RawArgument; use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface; use Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\AbstractServiceProvider as BaseServiceProvider; /** * Base class for the service providers used to register classes in the container. * * See the documentation of the original class this one is based on (https://container.thephpleague.com/3.x/service-providers) * for basic usage details. What this class adds is: * * - The `add_with_auto_arguments` method that allows to register classes without having to specify the injection method arguments. * - The `share_with_auto_arguments` method, sibling of the above. * - Convenience `add` and `share` methods that are just proxies for the same methods in `$this->getContainer()`. */ abstract class AbstractServiceProvider extends BaseServiceProvider { /** * Register a class in the container and use reflection to guess the injection method arguments. * * WARNING: this method uses reflection, so please have performance in mind when using it. * * @param string $class_name Class name to register. * @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name. * @param bool $shared Whether to register the class as shared (`get` always returns the same instance) or not. * * @return DefinitionInterface The generated container definition. * * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint. */ protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface { $definition = new Definition( $class_name, $concrete ); $function = $this->reflect_class_or_callable( $class_name, $concrete ); if ( ! is_null( $function ) ) { $arguments = $function->getParameters(); foreach ( $arguments as $argument ) { if ( $argument->isDefaultValueAvailable() ) { $default_value = $argument->getDefaultValue(); $definition->addArgument( new RawArgument( $default_value ) ); } else { $argument_class = $this->get_class( $argument ); if ( is_null( $argument_class ) ) { throw new ContainerException( "Argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." ); } $definition->addArgument( $argument_class->name ); } } } // Register the definition only after being sure that no exception will be thrown. $this->getContainer()->add( $definition->getAlias(), $definition, $shared ); return $definition; } /** * Gets the class of a parameter. * * This method is a replacement for ReflectionParameter::getClass, * which is deprecated as of PHP 8. * * @param \ReflectionParameter $parameter The parameter to get the class for. * * @return \ReflectionClass|null The class of the parameter, or null if it hasn't any. */ private function get_class( \ReflectionParameter $parameter ) { // TODO: Remove this 'if' block once minimum PHP version for WooCommerce is bumped to at least 7.1. if ( version_compare( PHP_VERSION, '7.1', '<' ) ) { return $parameter->getClass(); } return $parameter->getType() && ! $parameter->getType()->isBuiltin() ? new \ReflectionClass( $parameter->getType()->getName() ) : null; } /** * Check if a combination of class name and concrete is valid for registration. * Also return the class injection method if the concrete is either a class name or null (then use the supplied class name). * * @param string $class_name The class name to check. * @param mixed $concrete The concrete to check. * * @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name injection method or $concrete injection method or callable; null otherwise. * @throws ContainerException Class has a private injection method, can't reflect class, or the concrete is invalid. */ private function reflect_class_or_callable( string $class_name, $concrete ) { if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) { try { $class = $concrete ?? $class_name; $method = new \ReflectionMethod( $class, Definition::INJECTION_METHOD ); if ( ! isset( $method ) ) { return null; } $missing_modifiers = array(); if ( ! $method->isFinal() ) { $missing_modifiers[] = 'final'; } if ( ! $method->isPublic() ) { $missing_modifiers[] = 'public'; } if ( ! empty( $missing_modifiers ) ) { throw new ContainerException( "Method '" . Definition::INJECTION_METHOD . "' of class '$class' isn't '" . implode( ' ', $missing_modifiers ) . "', instances can't be created." ); } return $method; } catch ( \ReflectionException $ex ) { return null; } } elseif ( is_callable( $concrete ) ) { try { return new \ReflectionFunction( $concrete ); } catch ( \ReflectionException $ex ) { throw new ContainerException( "Error when reflecting callable: {$ex->getMessage()}" ); } } return null; } /** * Register a class in the container and use reflection to guess the injection method arguments. * The class is registered as shared, so `get` on the container always returns the same instance. * * WARNING: this method uses reflection, so please have performance in mind when using it. * * @param string $class_name Class name to register. * @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name. * * @return DefinitionInterface The generated container definition. * * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint. */ protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface { return $this->add_with_auto_arguments( $class_name, $concrete, true ); } /** * Register an entry in the container. * * @param string $id Entry id (typically a class or interface name). * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation. * @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance) or not. * * @return DefinitionInterface The generated container definition. */ protected function add( string $id, $concrete = null, bool $shared = null ) : DefinitionInterface { return $this->getContainer()->add( $id, $concrete, $shared ); } /** * Register a shared entry in the container (`get` always returns the same instance). * * @param string $id Entry id (typically a class or interface name). * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation. * * @return DefinitionInterface The generated container definition. */ protected function share( string $id, $concrete = null ) : DefinitionInterface { return $this->add( $id, $concrete, true ); } } src/Internal/DependencyManagement/Definition.php 0000644 00000002177 15132754523 0016001 0 ustar 00 <?php /** * An extension to the Definition class to prevent constructor injection from being possible. */ namespace Automattic\WooCommerce\Internal\DependencyManagement; use Automattic\WooCommerce\Vendor\League\Container\Definition\Definition as BaseDefinition; /** * An extension of the definition class that replaces constructor injection with method injection. */ class Definition extends BaseDefinition { /** * The standard method that we use for dependency injection. */ const INJECTION_METHOD = 'init'; /** * Resolve a class using method injection instead of constructor injection. * * @param string $concrete The concrete to instantiate. * * @return object */ protected function resolveClass( string $concrete ) { $resolved = $this->resolveArguments( $this->arguments ); $concrete = new $concrete(); // Constructor injection causes backwards compatibility problems // so we will rely on method injection via an internal method. if ( method_exists( $concrete, static::INJECTION_METHOD ) ) { call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved ); } return $concrete; } } src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php 0000644 00000001464 15132754523 0023672 0 ustar 00 <?php /** * ProxiesServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Proxies\ActionsProxy; /** * Service provider for the classes in the Automattic\WooCommerce\Proxies namespace. */ class ProxiesServiceProvider extends AbstractServiceProvider { /** * The classes/interfaces that are serviced by this service provider. * * @var array */ protected $provides = array( LegacyProxy::class, ActionsProxy::class, ); /** * Register the classes. */ public function register() { $this->share( ActionsProxy::class ); $this->share_with_auto_arguments( LegacyProxy::class ); } } src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php 0000644 00000001364 15132754523 0027745 0 ustar 00 <?php /** * DownloadPermissionsAdjusterServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; /** * Service provider for the DownloadPermissionsAdjuster class. */ class DownloadPermissionsAdjusterServiceProvider extends AbstractServiceProvider { /** * The classes/interfaces that are serviced by this service provider. * * @var array */ protected $provides = array( DownloadPermissionsAdjuster::class, ); /** * Register the classes. */ public function register() { $this->share( DownloadPermissionsAdjuster::class ); } } src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php 0000644 00000001372 15132754523 0030032 0 ustar 00 <?php /** * RestockRefundedItemsAdjusterServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster; /** * Service provider for the RestockRefundedItemsAdjuster class. */ class RestockRefundedItemsAdjusterServiceProvider extends AbstractServiceProvider { /** * The classes/interfaces that are serviced by this service provider. * * @var array */ protected $provides = array( RestockRefundedItemsAdjuster::class, ); /** * Register the classes. */ public function register() { $this->share( RestockRefundedItemsAdjuster::class ); } } src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php 0000644 00000001320 15132754523 0026457 0 ustar 00 <?php /** * AssignDefaultCategoryServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Internal\AssignDefaultCategory; /** * Service provider for the AssignDefaultCategory class. */ class AssignDefaultCategoryServiceProvider extends AbstractServiceProvider { /** * The classes/interfaces that are serviced by this service provider. * * @var array */ protected $provides = array( AssignDefaultCategory::class, ); /** * Register the classes. */ public function register() { $this->share( AssignDefaultCategory::class ); } } src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php 0000644 00000002112 15132754523 0027111 0 ustar 00 <?php /** * ProductAttributesLookupServiceProvider class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; /** * Service provider for the ProductAttributesLookupServiceProvider namespace. */ class ProductAttributesLookupServiceProvider extends AbstractServiceProvider { /** * The classes/interfaces that are serviced by this service provider. * * @var array */ protected $provides = array( DataRegenerator::class, Filterer::class, LookupDataStore::class, ); /** * Register the classes. */ public function register() { $this->share( DataRegenerator::class )->addArgument( LookupDataStore::class ); $this->share( Filterer::class )->addArgument( LookupDataStore::class ); $this->share( LookupDataStore::class ); } } src/Internal/DependencyManagement/ContainerException.php 0000644 00000001252 15132754523 0017503 0 ustar 00 <?php /** * ExtendedContainer class file. */ namespace Automattic\WooCommerce\Internal\DependencyManagement; /** * Class ContainerException. * Used to signal error conditions related to the dependency injection container. */ class ContainerException extends \Exception { /** * Create a new instance of the class. * * @param null $message The exception message to throw. * @param int $code The error code. * @param Exception|null $previous The previous throwable used for exception chaining. */ public function __construct( $message = null, $code = 0, Exception $previous = null ) { parent::__construct( $message, $code, $previous ); } } src/Internal/WCCom/ConnectionHelper.php 0000644 00000001216 15132754523 0014036 0 ustar 00 <?php /** * Helpers for managing connection to WooCommerce.com. */ namespace Automattic\WooCommerce\Internal\WCCom; defined( 'ABSPATH' ) || exit; /** * Class WCConnectionHelper. * * Helpers for managing connection to WooCommerce.com. */ final class ConnectionHelper { /** * Check if WooCommerce.com account is connected. * * @since 4.4.0 * @return bool Whether account is connected. */ public static function is_connected() { $helper_options = get_option( 'woocommerce_helper_data', array() ); if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) { return true; } return false; } } src/Autoloader.php 0000644 00000003701 15132754523 0010153 0 ustar 00 <?php /** * Includes the composer Autoloader used for packages and classes in the src/ directory. */ namespace Automattic\WooCommerce; defined( 'ABSPATH' ) || exit; /** * Autoloader class. * * @since 3.7.0 */ class Autoloader { /** * Static-only class. */ private function __construct() {} /** * Require the autoloader and return the result. * * If the autoloader is not present, let's log the failure and display a nice admin notice. * * @return boolean */ public static function init() { $autoloader = dirname( __DIR__ ) . '/vendor/autoload_packages.php'; if ( ! is_readable( $autoloader ) ) { self::missing_autoloader(); return false; } $autoloader_result = require $autoloader; if ( ! $autoloader_result ) { return false; } return $autoloader_result; } /** * If the autoloader is missing, add an admin notice. */ protected static function missing_autoloader() { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' ) ); } add_action( 'admin_notices', function() { ?> <div class="notice notice-error"> <p> <?php printf( /* translators: 1: is a link to a support document. 2: closing link */ esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, %1$splease refer to this document%2$s to set up your development environment.', 'woocommerce' ), '<a href="' . esc_url( 'https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment' ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); ?> </p> </div> <?php } ); } } src/Container.php 0000644 00000007255 15132754523 0010006 0 ustar 00 <?php /** * Container class file. */ namespace Automattic\WooCommerce; use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductAttributesLookupServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\RestockRefundedItemsAdjusterServiceProvider; /** * PSR11 compliant dependency injection container for WooCommerce. * * Classes in the `src` directory should specify dependencies from that directory via an 'init' method having arguments * with type hints. If an instance of the container itself is needed, the type hint to use is \Psr\Container\ContainerInterface. * * Classes in the `src` directory should interact with anything outside (especially code in the `includes` directory * and WordPress functions) by using the classes in the `Proxies` directory. The exception is idempotent * functions (e.g. `wp_parse_url`), those can be used directly. * * Classes in the `includes` directory should use the `wc_get_container` function to get the instance of the container when * they need to get an instance of a class from the `src` directory. * * Class registration should be done via service providers that inherit from Automattic\WooCommerce\Internal\DependencyManagement * and those should go in the `src\Internal\DependencyManagement\ServiceProviders` folder unless there's a good reason * to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant. */ final class Container implements \Psr\Container\ContainerInterface { /** * The list of service provider classes to register. * * @var string[] */ private $service_providers = array( AssignDefaultCategoryServiceProvider::class, DownloadPermissionsAdjusterServiceProvider::class, ProductAttributesLookupServiceProvider::class, ProxiesServiceProvider::class, RestockRefundedItemsAdjusterServiceProvider::class, ); /** * The underlying container. * * @var \League\Container\Container */ private $container; /** * Class constructor. */ public function __construct() { $this->container = new ExtendedContainer(); // Add ourselves as the shared instance of ContainerInterface, // register everything else using service providers. $this->container->share( \Psr\Container\ContainerInterface::class, $this ); foreach ( $this->service_providers as $service_provider_class ) { $this->container->addServiceProvider( $service_provider_class ); } } /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws Psr\Container\ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get( $id ) { return $this->container->get( $id ); } /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has( $id ) { return $this->container->has( $id ); } } src/Packages.php 0000644 00000007617 15132754523 0007604 0 ustar 00 <?php /** * Loads WooCommece packages from the /packages directory. These are packages developed outside of core. */ namespace Automattic\WooCommerce; defined( 'ABSPATH' ) || exit; /** * Packages class. * * @since 3.7.0 */ class Packages { /** * Static-only class. */ private function __construct() {} /** * Array of package names and their main package classes. * * @var array Key is the package name/directory, value is the main package class which handles init. */ protected static $packages = array( 'woocommerce-blocks' => '\\Automattic\\WooCommerce\\Blocks\\Package', 'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package', ); /** * Init the package loader. * * @since 3.7.0 */ public static function init() { add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ) ); } /** * Callback for WordPress init hook. */ public static function on_init() { self::load_packages(); } /** * Checks a package exists by looking for it's directory. * * @param string $package Package name. * @return boolean */ public static function package_exists( $package ) { return file_exists( dirname( __DIR__ ) . '/packages/' . $package ); } /** * Loads packages after plugins_loaded hook. * * Each package should include an init file which loads the package so it can be used by core. */ protected static function load_packages() { foreach ( self::$packages as $package_name => $package_class ) { if ( ! self::package_exists( $package_name ) ) { self::missing_package( $package_name ); continue; } call_user_func( array( $package_class, 'init' ) ); } // Proxies "activated_plugin" hook for embedded packages listen on WC plugin activation // https://github.com/woocommerce/woocommerce/issues/28697. if ( is_admin() ) { $activated_plugin = get_transient( 'woocommerce_activated_plugin' ); if ( $activated_plugin ) { delete_transient( 'woocommerce_activated_plugin' ); /** * WooCommerce is activated hook. * * @since 5.0.0 * @param bool $activated_plugin Activated plugin path, * generally woocommerce/woocommerce.php. */ do_action( 'woocommerce_activated_plugin', $activated_plugin ); } } } /** * If a package is missing, add an admin notice. * * @param string $package Package name. */ protected static function missing_package( $package ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore sprintf( /* Translators: %s package name. */ esc_html__( 'Missing the WooCommerce %s package', 'woocommerce' ), '<code>' . esc_html( $package ) . '</code>' ) . ' - ' . esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' ) ); } add_action( 'admin_notices', function() use ( $package ) { ?> <div class="notice notice-error"> <p> <strong> <?php printf( /* Translators: %s package name. */ esc_html__( 'Missing the WooCommerce %s package', 'woocommerce' ), '<code>' . esc_html( $package ) . '</code>' ); ?> </strong> <br> <?php printf( /* translators: 1: is a link to a support document. 2: closing link */ esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, %1$splease refer to this document%2$s to set up your development environment.', 'woocommerce' ), '<a href="' . esc_url( 'https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment' ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); ?> </p> </div> <?php } ); } } src/Utilities/NumberUtil.php 0000644 00000002230 15132754523 0012111 0 ustar 00 <?php /** * A class of utilities for dealing with numbers. */ namespace Automattic\WooCommerce\Utilities; /** * A class of utilities for dealing with numbers. */ final class NumberUtil { /** * Round a number using the built-in `round` function, but unless the value to round is numeric * (a number or a string that can be parsed as a number), apply 'floatval' first to it * (so it will convert it to 0 in most cases). * * This is needed because in PHP 7 applying `round` to a non-numeric value returns 0, * but in PHP 8 it throws an error. Specifically, in WooCommerce we have a few places where * round('') is often executed. * * @param mixed $val The value to round. * @param int $precision The optional number of decimal digits to round to. * @param int $mode A constant to specify the mode in which rounding occurs. * * @return float The value rounded to the given precision as a float, or the supplied default value. */ public static function round( $val, int $precision = 0, int $mode = PHP_ROUND_HALF_UP ) : float { if ( ! is_numeric( $val ) ) { $val = floatval( $val ); } return round( $val, $precision, $mode ); } } src/Utilities/ArrayUtil.php 0000644 00000004351 15132754523 0011745 0 ustar 00 <?php /** * A class of utilities for dealing with arrays. */ namespace Automattic\WooCommerce\Utilities; /** * A class of utilities for dealing with arrays. */ class ArrayUtil { /** * Get a value from an nested array by specifying the entire key hierarchy with '::' as separator. * * E.g. for [ 'foo' => [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'. * * @param array $array The array to get the value from. * @param string $key The complete key hierarchy, using '::' as separator. * @param mixed $default The value to return if the key doesn't exist in the array. * * @return mixed The retrieved value, or the supplied default value. * @throws \Exception $array is not an array. */ public static function get_nested_value( array $array, string $key, $default = null ) { $key_stack = explode( '::', $key ); $subkey = array_shift( $key_stack ); if ( isset( $array[ $subkey ] ) ) { $value = $array[ $subkey ]; if ( count( $key_stack ) ) { foreach ( $key_stack as $subkey ) { if ( is_array( $value ) && isset( $value[ $subkey ] ) ) { $value = $value[ $subkey ]; } else { $value = $default; break; } } } } else { $value = $default; } return $value; } /** * Checks if a given key exists in an array and its value can be evaluated as 'true'. * * @param array $array The array to check. * @param string $key The key for the value to check. * @return bool True if the key exists in the array and the value can be evaluated as 'true'. */ public static function is_truthy( array $array, string $key ) { return isset( $array[ $key ] ) && $array[ $key ]; } /** * Gets the value for a given key from an array, or a default value if the key doesn't exist in the array. * * @param array $array The array to get the value from. * @param string $key The key to use to retrieve the value. * @param null $default The default value to return if the key doesn't exist in the array. * @return mixed|null The value for the key, or the default value passed. */ public static function get_value_or_default( array $array, string $key, $default = null ) { return isset( $array[ $key ] ) ? $array[ $key ] : $default; } } src/Utilities/StringUtil.php 0000644 00000003273 15132754523 0012137 0 ustar 00 <?php /** * A class of utilities for dealing with strings. */ namespace Automattic\WooCommerce\Utilities; /** * A class of utilities for dealing with strings. */ final class StringUtil { /** * Checks to see whether or not a string starts with another. * * @param string $string The string we want to check. * @param string $starts_with The string we're looking for at the start of $string. * @param bool $case_sensitive Indicates whether the comparison should be case-sensitive. * * @return bool True if the $string starts with $starts_with, false otherwise. */ public static function starts_with( string $string, string $starts_with, bool $case_sensitive = true ): bool { $len = strlen( $starts_with ); if ( $len > strlen( $string ) ) { return false; } $string = substr( $string, 0, $len ); if ( $case_sensitive ) { return strcmp( $string, $starts_with ) === 0; } return strcasecmp( $string, $starts_with ) === 0; } /** * Checks to see whether or not a string ends with another. * * @param string $string The string we want to check. * @param string $ends_with The string we're looking for at the end of $string. * @param bool $case_sensitive Indicates whether the comparison should be case-sensitive. * * @return bool True if the $string ends with $ends_with, false otherwise. */ public static function ends_with( string $string, string $ends_with, bool $case_sensitive = true ): bool { $len = strlen( $ends_with ); if ( $len > strlen( $string ) ) { return false; } $string = substr( $string, -$len ); if ( $case_sensitive ) { return strcmp( $string, $ends_with ) === 0; } return strcasecmp( $string, $ends_with ) === 0; } } src/Proxies/LegacyProxy.php 0000644 00000007251 15132754523 0011757 0 ustar 00 <?php /** * LegacyProxy class file. */ namespace Automattic\WooCommerce\Proxies; use Automattic\WooCommerce\Internal\DependencyManagement\Definition; use \Psr\Container\ContainerInterface; /** * Proxy class to access legacy WooCommerce functionality. * * This class should be used to interact with code outside the `src` directory, especially functions and classes * in the `includes` directory, unless a more specific proxy exists for the functionality at hand (e.g. `ActionsProxy`). * Idempotent functions can be executed directly. */ class LegacyProxy { /** * Gets an instance of a given legacy class. * This must not be used to get instances of classes in the `src` directory. * * If a given class needs a special procedure to get an instance of it, * please add a private get_instance_of_(lowercased_class_name) and it will be * automatically invoked. See also how objects of classes having a static `instance` * method are retrieved, similar approaches can be used as needed to make use * of existing factory methods such as e.g. 'load'. * * @param string $class_name The name of the class to get an instance for. * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. * * @return object The instance of the class. * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. */ public function get_instance_of( string $class_name, ...$args ) { if ( false !== strpos( $class_name, '\\' ) ) { throw new \Exception( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use ' . Definition::INJECTION_METHOD . ' method injection or the instance of ' . ContainerInterface::class . ' for that.' ); } // If a class has a dedicated method to obtain a instance, use it. $method = 'get_instance_of_' . strtolower( $class_name ); if ( method_exists( __CLASS__, $method ) ) { return $this->$method( ...$args ); } // If the class is a singleton, use the "instance" method. if ( method_exists( $class_name, 'instance' ) ) { return $class_name::instance( ...$args ); } // If the class has a "load" method, use it. if ( method_exists( $class_name, 'load' ) ) { return $class_name::load( ...$args ); } // Fallback to simply creating a new instance of the class. return new $class_name( ...$args ); } /** * Get an instance of a class implementing WC_Queue_Interface. * * @return \WC_Queue_Interface The instance. */ private function get_instance_of_wc_queue_interface() { return \WC_Queue::instance(); } /** * Call a user function. This should be used to execute any non-idempotent function, especially * those in the `includes` directory or provided by WordPress. * * @param string $function_name The function to execute. * @param mixed ...$parameters The parameters to pass to the function. * * @return mixed The result from the function. */ public function call_function( $function_name, ...$parameters ) { return call_user_func_array( $function_name, $parameters ); } /** * Call a static method in a class. This should be used to execute any non-idempotent method in classes * from the `includes` directory. * * @param string $class_name The name of the class containing the method. * @param string $method_name The name of the method. * @param mixed ...$parameters The parameters to pass to the method. * * @return mixed The result from the method. */ public function call_static( $class_name, $method_name, ...$parameters ) { return call_user_func_array( "$class_name::$method_name", $parameters ); } } src/Proxies/ActionsProxy.php 0000644 00000002120 15132754523 0012141 0 ustar 00 <?php /** * ActionsProxy class file. */ namespace Automattic\WooCommerce\Proxies; /** * Proxy for interacting with WordPress actions and filters. * * This class should be used instead of directly accessing the WordPress functions, to ease unit testing. */ class ActionsProxy { /** * Retrieve the number of times an action is fired. * * @param string $tag The name of the action hook. * * @return int The number of times action hook $tag is fired. */ public function did_action( $tag ) { return did_action( $tag ); } /** * Calls the callback functions that have been added to a filter hook. * * @param string $tag The name of the filter hook. * @param mixed $value The value to filter. * @param mixed ...$parameters Additional parameters to pass to the callback functions. * * @return mixed The filtered value after all hooked functions are applied to it. */ public function apply_filters( $tag, $value, ...$parameters ) { return apply_filters( $tag, $value, ...$parameters ); } // TODO: Add the rest of the actions and filters related methods. } src/Checkout/Helpers/ReserveStock.php 0000644 00000015446 15132754523 0013653 0 ustar 00 <?php /** * Handle product stock reservation during checkout. */ namespace Automattic\WooCommerce\Checkout\Helpers; defined( 'ABSPATH' ) || exit; /** * Stock Reservation class. */ final class ReserveStock { /** * Is stock reservation enabled? * * @var boolean */ private $enabled = true; /** * Constructor */ public function __construct() { // Table needed for this feature are added in 4.3. $this->enabled = get_option( 'woocommerce_schema_version', 0 ) >= 430; } /** * Is stock reservation enabled? * * @return boolean */ protected function is_enabled() { return $this->enabled; } /** * Query for any existing holds on stock for this item. * * @param \WC_Product $product Product to get reserved stock for. * @param integer $exclude_order_id Optional order to exclude from the results. * * @return integer Amount of stock already reserved. */ public function get_reserved_stock( $product, $exclude_order_id = 0 ) { global $wpdb; if ( ! $this->is_enabled() ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) ); } /** * Put a temporary hold on stock for an order if enough is available. * * @throws ReserveStockException If stock cannot be reserved. * * @param \WC_Order $order Order object. * @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes. */ public function reserve_stock_for_order( $order, $minutes = 0 ) { $minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 ); if ( ! $minutes || ! $this->is_enabled() ) { return; } try { $items = array_filter( $order->get_items(), function( $item ) { return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0; } ); $rows = array(); foreach ( $items as $item ) { $product = $item->get_product(); if ( ! $product->is_in_stock() ) { throw new ReserveStockException( 'woocommerce_product_out_of_stock', sprintf( /* translators: %s: product name */ __( '"%s" is out of stock and cannot be purchased.', 'woocommerce' ), $product->get_name() ), 403 ); } // If stock management is off, no need to reserve any stock here. if ( ! $product->managing_stock() || $product->backorders_allowed() ) { continue; } $managed_by_id = $product->get_stock_managed_by_id(); /** * Filter order item quantity. * * @param int|float $quantity Quantity. * @param WC_Order $order Order data. * @param WC_Order_Item_Product $item Order item data. */ $item_quantity = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); $rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item_quantity : $item_quantity; } if ( ! empty( $rows ) ) { foreach ( $rows as $product_id => $quantity ) { $this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes ); } } } catch ( ReserveStockException $e ) { $this->release_stock_for_order( $order ); throw $e; } } /** * Release a temporary hold on stock for an order. * * @param \WC_Order $order Order object. */ public function release_stock_for_order( $order ) { global $wpdb; if ( ! $this->is_enabled() ) { return; } $wpdb->delete( $wpdb->wc_reserved_stock, array( 'order_id' => $order->get_id(), ) ); } /** * Reserve stock for a product by inserting rows into the DB. * * @throws ReserveStockException If a row cannot be inserted. * * @param int $product_id Product ID which is having stock reserved. * @param int $stock_quantity Stock amount to reserve. * @param \WC_Order $order Order object which contains the product. * @param int $minutes How long to reserve stock in minutes. */ private function reserve_stock_for_product( $product_id, $stock_quantity, $order, $minutes ) { global $wpdb; $product_data_store = \WC_Data_Store::load( 'product' ); $query_for_stock = $product_data_store->get_query_for_stock( $product_id ); $query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared $result = $wpdb->query( $wpdb->prepare( " INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` ) SELECT %d, %d, %d, NOW(), ( NOW() + INTERVAL %d MINUTE ) FROM DUAL WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` ) ", $order->get_id(), $product_id, $stock_quantity, $minutes, $stock_quantity ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared if ( ! $result ) { $product = wc_get_product( $product_id ); throw new ReserveStockException( 'woocommerce_product_not_enough_stock', sprintf( /* translators: %s: product name */ __( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ), $product ? $product->get_name() : '#' . $product_id ), 403 ); } } /** * Returns query statement for getting reserved stock of a product. * * @param int $product_id Product ID. * @param integer $exclude_order_id Optional order to exclude from the results. * @return string|void Query statement. */ private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) { global $wpdb; $query = $wpdb->prepare( " SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' ) AND stock_table.`expires` > NOW() AND stock_table.`product_id` = %d AND stock_table.`order_id` != %d ", $product_id, $exclude_order_id ); /** * Filter: woocommerce_query_for_reserved_stock * Allows to filter the query for getting reserved stock of a product. * * @since 4.5.0 * @param string $query The query for getting reserved stock of a product. * @param int $product_id Product ID. * @param int $exclude_order_id Order to exclude from the results. */ return apply_filters( 'woocommerce_query_for_reserved_stock', $query, $product_id, $exclude_order_id ); } } src/Checkout/Helpers/ReserveStockException.php 0000644 00000002311 15132754523 0015515 0 ustar 00 <?php /** * Exceptions for stock reservation. */ namespace Automattic\WooCommerce\Checkout\Helpers; defined( 'ABSPATH' ) || exit; /** * ReserveStockException class. */ class ReserveStockException extends \Exception { /** * Sanitized error code. * * @var string */ protected $error_code; /** * Error extra data. * * @var array */ protected $error_data; /** * Setup exception. * * @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'. * @param int $http_status_code Proper HTTP status code to respond with, e.g. 400. * @param array $data Extra error data. */ public function __construct( $code, $message, $http_status_code = 400, $data = array() ) { $this->error_code = $code; $this->error_data = $data; parent::__construct( $message, $http_status_code ); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns error data. * * @return array */ public function getErrorData() { return $this->error_data; } } readme.txt 0000644 00000054166 15132754523 0006565 0 ustar 00 === WooCommerce === Contributors: automattic, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho, barryhughes-1 Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce Requires at least: 5.6 Tested up to: 5.8 Requires PHP: 7.0 Stable tag: 5.9.1 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html WooCommerce is the world’s most popular open-source eCommerce solution. == Description == WooCommerce is [the world’s most popular](https://trends.builtwith.com/shop) open-source eCommerce solution. Our core platform is free, flexible, and amplified by a global community. The freedom of open-source means you retain full ownership of your store’s content and data forever. Whether you’re launching a business, taking brick-and-mortar retail online, or developing sites for clients, use WooCommerce for a store that powerfully blends content and commerce. - **Create beautiful, enticing storefronts** with [themes](https://woocommerce.com/product-category/themes/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) suited to your brand and industry. - **Customize pages in minutes** using modular [product blocks](https://docs.woocommerce.com/document/woocommerce-blocks/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). - Showcase physical and digital goods, product variations, custom configurations, instant downloads, and affiliate items. [Bookings](https://woocommerce.com/products/woocommerce-bookings/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [memberships](https://woocommerce.com/products/woocommerce-memberships/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [dynamic pricing](https://woocommerce.com/products/dynamic-pricing/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) rules are only an extension away. - **Rise to the top of search results** by leveraging [WordPress’ SEO advantage](https://www.searchenginejournal.com/wordpress-best-cms-seo/). Built-in tools and popular integrations help you efficiently manage your business operations. Many services are free to add with a single click via the optional [Setup Wizard](https://docs.woocommerce.com/document/woocommerce-setup-wizard/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). - **Choose how you want to get paid**. Conveniently manage payments from the comfort of your store with [WooCommerce Payments](https://woocommerce.com/payments/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (Available in the U.S., U.K., Ireland, Australia, New Zealand, Canada, and now: Spain, France, Germany, and Italy). Securely accept cards, mobile wallets, bank transfers, and cash thanks to [100+ payment gateways](https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) – including [Stripe](https://woocommerce.com/products/stripe/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [PayPal](https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [Square](https://woocommerce.com/products/square/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). - **Configure your shipping options**. Print USPS labels right from your dashboard and even schedule a pickup with [WooCommerce Shipping](https://woocommerce.com/products/shipping/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (U.S.-only). Connect with [well-known carriers](https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) such as UPS, FedEx, and ShipStation – plus a wide variety of delivery, inventory, and fulfillment solutions for your locale. - **Simplify sales tax**. Add [WooCommerce Tax](https://woocommerce.com/products/tax/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) or [similar integrated services](https://woocommerce.com/product-category/woocommerce-extensions/tax?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to make automated calculations a reality. = Grow your business, add features, and monitor your store on the go = WooCommerce means business. Keep tabs on the performance metrics most important to you with [WooCommerce Admin](https://wordpress.org/plugins/woocommerce-admin/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) – a powerful, customizable central dashboard for your store. Expand your audience across marketing and social channels with [Google Ads](https://woocommerce.com/products/google-ads/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [HubSpot](https://woocommerce.com/products/hubspot-for-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [Mailchimp](https://woocommerce.com/products/mailchimp-for-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [Facebook](https://woocommerce.com/products/facebook/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) integrations. You can always check out the in-dashboard [Marketing Hub](https://docs.woocommerce.com/document/marketing-hub/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) for fresh ideas and tips to help you succeed. Enhance store functionality with hundreds of free and paid extensions from the [official WooCommerce Marketplace](https://woocommerce.com/products/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Our developers [vet each new extension](https://docs.woocommerce.com/document/marketplace-overview/#section-6?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and regularly review existing inventory to maintain Marketplace quality standards. We are actively [looking for products that help store builders create successful stores](https://docs.woocommerce.com/document/marketplace-overview/#section-2?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Manage your store from anywhere with the free WooCommerce [mobile app](https://woocommerce.com/mobile/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (Android and iOS). Spoiler alert: Keep an ear out for the slightly addictive "cha-ching" notification sound each time you make a new sale! = Own and control your store data – forever = With WooCommerce, your data belongs to you. Always. If you opt to share [usage data](https://woocommerce.com/usage-tracking/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) with us, you can feel confident knowing that it’s anonymized and kept secure. Choose to opt-out at any time without impacting your store. Unlike hosted eCommerce solutions, WooCommerce store data is future-proof; should you wish to migrate to a different platform, you’re free to export all your content and take your site wherever you choose. No restrictions. = Why developers choose (and love) WooCommerce = Developers can use WooCommerce to create, customize, and scale a store to meet a client’s exact specifications, making enhancements through extensions or custom solutions. - Leverage [hooks and filters](https://docs.woocommerce.com/document/introduction-to-hooks-actions-and-filters/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to modify or create functionality. - Integrate virtually any service using a robust [REST API](https://docs.woocommerce.com/document/woocommerce-rest-api/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and webhooks. - Design and build custom content blocks with React. - [Inspect and modify](https://docs.woocommerce.com/documentation/plugins/woocommerce/woocommerce-codex/extending/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) any aspect of the core plugin code. - Speed up development with a lightning-fast [CLI](https://woocommerce.github.io/code-reference/classes/wc-cli-rest-command.html?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). The core platform is tested rigorously and often, supported by a dedicated development team working across time zones. Comprehensive documentation is updated with each release, empowering you to build exactly the store required. = Be part of our growing international community = WooCommerce has a large, passionate community dedicated to helping merchants succeed, and it’s growing fast. There are [WooCommerce Meetups](https://woocommerce.com/meetups/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) in locations around the world that you can attend for free and even get involved in running. These events are a great way to learn from others, share your expertise, and connect with like-minded folks. WooCommerce also has a regular presence at WordCamps across the globe – we’d love to meet you. = Contribute and translate = WooCommerce is developed and supported by Automattic, the creators of WordPress.com and Jetpack. We also have hundreds of independent contributors, and there’s always room for more. Head to the [WooCommerce GitHub Repository](https://github.com/woocommerce/woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to find out how you can pitch in. WooCommerce is translated into multiple languages, including Danish, Ukrainian, and Persian. Help localize WooCommerce even further by adding your locale – visit [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). == Frequently Asked Questions == = Where can I find WooCommerce documentation and user guides? = For help setting up and configuring WooCommerce, please refer to [Getting Started](https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and the [New WooCommerce Store Owner Guide](https://woocommerce.com/guides/new-store/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). For extending or theming WooCommerce, see our [codex](https://docs.woocommerce.com/documentation/plugins/woocommerce/woocommerce-codex/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), as well as the [Plugin Developer Handbook](https://docs.woocommerce.com/document/create-a-plugin/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). = Where can I get help or talk to other users about WooCommerce Core? = If you get stuck, you can ask for help in the [WooCommerce Support Forum](https://wordpress.org/support/plugin/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) by following [these guidelines](https://wordpress.org/support/topic/guide-to-the-woocommerce-forum/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), reach out via the [WooCommerce Community Slack](https://woocommerce.com/community-slack/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), or post in the [WooCommerce Community group](https://www.facebook.com/groups/advanced.woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) on Facebook. = Where can I get help for extensions I have purchased on WooCommerce.com? = For assistance with paid extensions from the WooCommerce.com Marketplace: first, review our [self-service troubleshooting guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/). If the problem persists, kindly log a support ticket via [our helpdesk](https://woocommerce.com/my-account/create-a-ticket/). Our dedicated Happiness Engineers aim to respond within 24 hours. = I’m having trouble logging in to WooCommerce.com – what now? = First, troubleshoot common login issues using this helpful [step-by-step guide](https://docs.woocommerce.com/document/log-into-woocommerce-com-with-wordpress-com/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Still not working? [Get in touch with us](https://woocommerce.com/contact-us/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). = Will WooCommerce work with my theme? = Yes! WooCommerce will work with any theme but may require some additional styling. If you’re looking for a theme featuring deep WooCommerce integration, we recommend [Storefront](https://woocommerce.com/storefront/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). = How do I update WooCommerce? = We have a detailed guide on [How To Update WooCommerce](https://docs.woocommerce.com/document/how-to-update-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). = My site broke – what do I do? = Start by diagnosing the issue using our helpful [troubleshooting guide](https://docs.woocommerce.com/documentation/get-help/troubleshooting-get-help/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). If you noticed the error after updating a theme or plugin, there might be compatibility issues between it and WooCommerce. If the issue appeared after updating WooCommerce, there could be a conflict between WooCommerce and an outdated theme or plugin. In both instances, we recommend running a conflict test using [Health Check](https://docs.woocommerce.com/document/troubleshooting-using-health-check/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (which allows you to disable themes and plugins without affecting your visitors) or troubleshooting the issue using a [staging site](https://docs.woocommerce.com/document/how-to-test-for-conflicts/#section-3?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). = Where can I report bugs? = Report bugs on the [WooCommerce GitHub repository](https://github.com/woocommerce/woocommerce/issues?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). You can also notify us via our support forum – be sure to search the forums to confirm that the error has not already been reported. = Where can I request new features, themes, and extensions? = Request new features and extensions and vote on existing suggestions on our official [ideas board](https://ideas.woocommerce.com/forums/133476-woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Our Product teams regularly review requests and consider them valuable for product planning. = WooCommerce is awesome! Can I contribute? = Yes, you can! Join in on our [GitHub repository](https://github.com/woocommerce/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and follow the [development blog](https://woocommerce.wordpress.com/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to stay up-to-date with everything happening in the project. = Where can I find REST API documentation? = Extensive [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) is available on GitHub. = My question is not listed here. Where can I find more answers? = Check out [Frequently Asked Questions](https://docs.woocommerce.com/document/frequently-asked-questions/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) for more. == Installation == = Minimum Requirements = * PHP 7.2 or greater is recommended * MySQL 5.6 or greater is recommended Visit the [WooCommerce server requirements documentation](https://docs.woocommerce.com/document/server-requirements/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) for a detailed list of server requirements. = Automatic installation = Automatic installation is the easiest option -- WordPress will handles the file transfer, and you won’t need to leave your web browser. To do an automatic install of WooCommerce, log in to your WordPress dashboard, navigate to the Plugins menu, and click “Add New.” In the search field type “WooCommerce,” then click “Search Plugins.” Once you’ve found us, you can view details about it such as the point release, rating, and description. Most importantly of course, you can install it by! Click “Install Now,” and WordPress will take it from there. = Manual installation = Manual installation method requires downloading the WooCommerce plugin and uploading it to your web server via your favorite FTP application. The WordPress codex contains [instructions on how to do this here](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). = Updating = Automatic updates should work smoothly, but we still recommend you back up your site. If you encounter issues with the shop/category pages after an update, flush the permalinks by going to WordPress > Settings > Permalinks and hitting “Save.” That should return things to normal. = Sample data = WooCommerce comes with some sample data you can use to see how products look; import sample_products.xml via the [WordPress importer](https://wordpress.org/plugins/wordpress-importer/). You can also use the core [CSV importer](https://docs.woocommerce.com/document/product-csv-importer-exporter/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) or our [CSV Import Suite extension](https://woocommerce.com/products/product-csv-import-suite/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) to import sample_products.csv == Changelog == = 5.9.0 2021-11-09 = **WooCommerce** * Fix - Bug in the handling of remote file names for downloadable files. * Fix - Remove the absolute path to the currency-info.php from within locale-info.php. #31036 * Fix - wc_get_price_excluding_tax when an order with no customer is passed. #31015 * Fix - Rename transient used to cache data for Featured page of In-App Marketplace. #31002 * Fix - Variable product price caching bug with VAT exemption. #30889 * Fix - Allow to pass null as the email for billing addresses in REST API. #30850 * Fix - Ensure woocommerce_cancel_unpaid_orders event is always re-scheduled. #30830 * Fix - Use a more standard way to check if the product attributes lookup table exists. #30745 * Fix - Undefined variable notice when trying to add product in orders without specifying a product. #30739 * Fix - Use proper location for taxes when adding products via admin. #30692 * Dev - Add mobile data to WCTracker. #30415 * Tweak - Remove hardcode category banners in Settings > Marketplace and use the WooCommerce.com API instead. #30938 * Tweak - Show a search again message when marketplace results are empty. #30642 * Tweak - Add promoted cards styling to marketplace section. #30861 * Enhancement - Add ratings, reviews and icons into Marketplace's Product Cards. #30840 * Enhancement - Update Storefront banner width and track links in the marketplace page. #30882 * Enhancement - Revamp the WooCommerce Marketplace page. #30900 **WooCommerce Admin - 2.8.0 ** * Fix - Issue where stock activity panel was not rendering correctly. #7817 * Fix - Increase CSS specificity to avoid conflicts and broken panel styling. #7813 * Fix - Updated link to WooCommerce Developers Blog in readme.txt. #7824 * Fix - Fixed navigation menu text color after Gutenberg 11.6.0. #7771 * Fix - Add status param to notes/delete/all REST endpoint, to correctly delete all notes. #7743 * Fix - Allow already installed marketing extensions to be activated. #7740 * Fix - Add missing title text for marketing task. #7640 * Fix - Assign parent order status as children order status if refund order. #7253 * Fix - Fix category lookup logic to update children correctly. #7709 * Fix - Fixing an unwanted page refresh when using Woo Navigation. #7615 * Fix - Fix naming of event names and properties. #7677 * Fix - Fix white screen for variation analytic data without a name. #7686 * Add - Store Profiler and Product task - include Subscriptions. #7734 * Update - Update WC pay supported country list for the default free extensions. #7873 * Update - Update back up copy of free extension for Google Listing & Ads plugin. #7798 * Update - Update Eway payment gateway capitalization (was eWAY). #7678 * Update - Enable Square in France. #7679 * Enhancement - Only load tasks during rest api requests. #7856 * Enhancement - Add experiment for promoting WooCommerce Payments in payment methods table. #7666 **WooCommerce Blocks - 6.0.0 & 6.0.1 & 6.0.2 & 6.1.0** * Fix - Infinite recursion when removing an attribute filter from the Active filters block. #4816 * Fix - Update All Reviews block so it honors 'ratings enabled' and 'show avatars' preferences. #4764 * Fix - Products by Category: Moved renderEmptyResponsePlaceholder to separate method to prevent unnecessary rerender. #4751 * Fix - Calculation of number of reviews in the Reviews by Category block. #4729 * Fix - Dropdown list in Product Category List Block for nested categories #4920 * Fix - String translations within the All Products Block. #4897 * Fix - Filter By Price: Update aria values to be more representative of the actual values presented. #4839 * Fix - Filter button from Filter Products by Attribute block is not aligned with the input field. #4814 * Fix - Remove IntersectionObserver shim in favor of dropping IE11 support. #4808 * Enhancement - Added global styles to All Reviews, Reviews by Category and Reviews by Product blocks. Now it's possible to change the text color and font size of those blocks. #4323 [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). license.txt 0000644 00000106736 15132754523 0006753 0 ustar 00 WooCommerce - eCommerce for WordPress Copyright 2015 by the contributors This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA This program incorporates work covered by the following copyright and permission notices: Jigoshop is Copyright (c) 2011 Jigowatt Ltd. http://jigowatt.com - http://jigoshop.com Jigoshop is released under the GPL and WooCommerce - eCommerce for WordPress WooCommerce is Copyright (c) 2015 WooThemes WooCommerce is released under the GPL =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright © <year> <name of author> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright © <year> <name of author> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>. woocommerce.php 0000644 00000003201 15132754523 0007577 0 ustar 00 <?php /** * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. * Version: 5.9.1 * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce * Domain Path: /i18n/languages/ * Requires at least: 5.6 * Requires PHP: 7.0 * * @package WooCommerce */ defined( 'ABSPATH' ) || exit; if ( ! defined( 'WC_PLUGIN_FILE' ) ) { define( 'WC_PLUGIN_FILE', __FILE__ ); } // Load core packages and the autoloader. require __DIR__ . '/src/Autoloader.php'; require __DIR__ . '/src/Packages.php'; if ( ! \Automattic\WooCommerce\Autoloader::init() ) { return; } \Automattic\WooCommerce\Packages::init(); // Include the main WooCommerce class. if ( ! class_exists( 'WooCommerce', false ) ) { include_once dirname( WC_PLUGIN_FILE ) . '/includes/class-woocommerce.php'; } // Initialize dependency injection. $GLOBALS['wc_container'] = new Automattic\WooCommerce\Container(); /** * Returns the main instance of WC. * * @since 2.1 * @return WooCommerce */ function WC() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid return WooCommerce::instance(); } /** * Returns the WooCommerce PSR11-compatible object container. * Code in the `includes` directory should use the container to get instances of classes in the `src` directory. * * @since 4.4.0 * @return \Psr\Container\ContainerInterface The WooCommerce PSR11 container. */ function wc_get_container() : \Psr\Container\ContainerInterface { return $GLOBALS['wc_container']; } // Global for backwards compatibility. $GLOBALS['woocommerce'] = WC(); includes/payment-tokens/class-wc-payment-token-cc.php 0000644 00000010715 15132754523 0016744 0 ustar 00 <?php /** * Class WC_Payment_Token_CC file. * * @package WooCommerce\PaymentTokens */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WooCommerce Credit Card Payment Token. * * Representation of a payment token for credit cards. * * @class WC_Payment_Token_CC * @version 3.0.0 * @since 2.6.0 * @package WooCommerce\PaymentTokens */ class WC_Payment_Token_CC extends WC_Payment_Token { /** * Token Type String. * * @var string */ protected $type = 'CC'; /** * Stores Credit Card payment token data. * * @var array */ protected $extra_data = array( 'last4' => '', 'expiry_year' => '', 'expiry_month' => '', 'card_type' => '', ); /** * Get type to display to user. * * @since 2.6.0 * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string */ public function get_display_name( $deprecated = '' ) { $display = sprintf( /* translators: 1: credit card type 2: last 4 digits 3: expiry month 4: expiry year */ __( '%1$s ending in %2$s (expires %3$s/%4$s)', 'woocommerce' ), wc_get_credit_card_type_label( $this->get_card_type() ), $this->get_last4(), $this->get_expiry_month(), substr( $this->get_expiry_year(), 2 ) ); return $display; } /** * Hook prefix * * @since 3.0.0 */ protected function get_hook_prefix() { return 'woocommerce_payment_token_cc_get_'; } /** * Validate credit card payment tokens. * * These fields are required by all credit card payment tokens: * expiry_month - string Expiration date (MM) for the card * expiry_year - string Expiration date (YYYY) for the card * last4 - string Last 4 digits of the card * card_type - string Card type (visa, mastercard, etc) * * @since 2.6.0 * @return boolean True if the passed data is valid */ public function validate() { if ( false === parent::validate() ) { return false; } if ( ! $this->get_last4( 'edit' ) ) { return false; } if ( ! $this->get_expiry_year( 'edit' ) ) { return false; } if ( ! $this->get_expiry_month( 'edit' ) ) { return false; } if ( ! $this->get_card_type( 'edit' ) ) { return false; } if ( 4 !== strlen( $this->get_expiry_year( 'edit' ) ) ) { return false; } if ( 2 !== strlen( $this->get_expiry_month( 'edit' ) ) ) { return false; } return true; } /** * Returns the card type (mastercard, visa, ...). * * @since 2.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string Card type */ public function get_card_type( $context = 'view' ) { return $this->get_prop( 'card_type', $context ); } /** * Set the card type (mastercard, visa, ...). * * @since 2.6.0 * @param string $type Credit card type (mastercard, visa, ...). */ public function set_card_type( $type ) { $this->set_prop( 'card_type', $type ); } /** * Returns the card expiration year (YYYY). * * @since 2.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string Expiration year */ public function get_expiry_year( $context = 'view' ) { return $this->get_prop( 'expiry_year', $context ); } /** * Set the expiration year for the card (YYYY format). * * @since 2.6.0 * @param string $year Credit card expiration year. */ public function set_expiry_year( $year ) { $this->set_prop( 'expiry_year', $year ); } /** * Returns the card expiration month (MM). * * @since 2.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string Expiration month */ public function get_expiry_month( $context = 'view' ) { return $this->get_prop( 'expiry_month', $context ); } /** * Set the expiration month for the card (formats into MM format). * * @since 2.6.0 * @param string $month Credit card expiration month. */ public function set_expiry_month( $month ) { $this->set_prop( 'expiry_month', str_pad( $month, 2, '0', STR_PAD_LEFT ) ); } /** * Returns the last four digits. * * @since 2.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string Last 4 digits */ public function get_last4( $context = 'view' ) { return $this->get_prop( 'last4', $context ); } /** * Set the last four digits. * * @since 2.6.0 * @param string $last4 Credit card last four digits. */ public function set_last4( $last4 ) { $this->set_prop( 'last4', $last4 ); } } includes/payment-tokens/class-wc-payment-token-echeck.php 0000644 00000003771 15132754523 0017605 0 ustar 00 <?php /** * Class WC_Payment_Token_eCheck file. * * @package WooCommerce\PaymentTokens */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WooCommerce eCheck Payment Token. * * Representation of a payment token for eChecks. * * @class WC_Payment_Token_ECheck * @version 3.0.0 * @since 2.6.0 * @package WooCommerce\PaymentTokens */ class WC_Payment_Token_ECheck extends WC_Payment_Token { /** * Token Type String. * * @var string */ protected $type = 'eCheck'; /** * Stores eCheck payment token data. * * @var array */ protected $extra_data = array( 'last4' => '', ); /** * Get type to display to user. * * @since 2.6.0 * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string */ public function get_display_name( $deprecated = '' ) { $display = sprintf( /* translators: 1: last 4 digits */ __( 'eCheck ending in %1$s', 'woocommerce' ), $this->get_last4() ); return $display; } /** * Hook prefix * * @since 3.0.0 */ protected function get_hook_prefix() { return 'woocommerce_payment_token_echeck_get_'; } /** * Validate eCheck payment tokens. * * These fields are required by all eCheck payment tokens: * last4 - string Last 4 digits of the check * * @since 2.6.0 * @return boolean True if the passed data is valid */ public function validate() { if ( false === parent::validate() ) { return false; } if ( ! $this->get_last4( 'edit' ) ) { return false; } return true; } /** * Returns the last four digits. * * @since 2.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string Last 4 digits */ public function get_last4( $context = 'view' ) { return $this->get_prop( 'last4', $context ); } /** * Set the last four digits. * * @since 2.6.0 * @param string $last4 eCheck last four digits. */ public function set_last4( $last4 ) { $this->set_prop( 'last4', $last4 ); } } includes/class-wc-product-variable.php 0000644 00000053006 15132754523 0014053 0 ustar 00 <?php /** * Variable Product * * The WooCommerce product class handles individual product data. * * @version 3.0.0 * @package WooCommerce\Classes\Products */ defined( 'ABSPATH' ) || exit; /** * Variable product class. */ class WC_Product_Variable extends WC_Product { /** * Array of children variation IDs. Determined by children. * * @var array */ protected $children = null; /** * Array of visible children variation IDs. Determined by children. * * @var array */ protected $visible_children = null; /** * Array of variation attributes IDs. Determined by children. * * @var array */ protected $variation_attributes = null; /** * Get internal type. * * @return string */ public function get_type() { return 'variable'; } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get the add to cart button text. * * @return string */ public function add_to_cart_text() { return apply_filters( 'woocommerce_product_add_to_cart_text', $this->is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Select options for “%s”', 'woocommerce' ), $this->get_name() ), $this ); } /** * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. * * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). * @return array Array of RAW prices, regular prices, and sale prices with keys set to variation ID. */ public function get_variation_prices( $for_display = false ) { $prices = $this->data_store->read_price_data( $this, $for_display ); foreach ( $prices as $price_key => $variation_prices ) { $prices[ $price_key ] = $this->sort_variation_prices( $variation_prices ); } return $prices; } /** * Get the min or max variation regular price. * * @param string $min_or_max Min or max price. * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). * @return string */ public function get_variation_regular_price( $min_or_max = 'min', $for_display = false ) { $prices = $this->get_variation_prices( $for_display ); $price = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] ); return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $for_display ); } /** * Get the min or max variation sale price. * * @param string $min_or_max Min or max price. * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). * @return string */ public function get_variation_sale_price( $min_or_max = 'min', $for_display = false ) { $prices = $this->get_variation_prices( $for_display ); $price = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] ); return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $for_display ); } /** * Get the min or max variation (active) price. * * @param string $min_or_max Min or max price. * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). * @return string */ public function get_variation_price( $min_or_max = 'min', $for_display = false ) { $prices = $this->get_variation_prices( $for_display ); $price = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] ); return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $for_display ); } /** * Returns the price in html format. * * Note: Variable prices do not show suffixes like other product types. This * is due to some things like tax classes being set at variation level which * could differ from the parent price. The only way to show accurate prices * would be to load the variation and get it's price, which adds extra * overhead and still has edge cases where the values would be inaccurate. * * Additionally, ranges of prices no longer show 'striked out' sale prices * due to the strings being very long and unclear/confusing. A single range * is shown instead. * * @param string $price Price (default: ''). * @return string */ public function get_price_html( $price = '' ) { $prices = $this->get_variation_prices( true ); if ( empty( $prices['price'] ) ) { $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this ); } else { $min_price = current( $prices['price'] ); $max_price = end( $prices['price'] ); $min_reg_price = current( $prices['regular_price'] ); $max_reg_price = end( $prices['regular_price'] ); if ( $min_price !== $max_price ) { $price = wc_format_price_range( $min_price, $max_price ); } elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) { $price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) ); } else { $price = wc_price( $min_price ); } $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } /** * Get the suffix to display after prices > 0. * * This is skipped if the suffix * has dynamic values such as {price_excluding_tax} for variable products. * * @see get_price_html for an explanation as to why. * @param string $price Price to calculate, left blank to just use get_price(). * @param integer $qty Quantity passed on to get_price_including_tax() or get_price_excluding_tax(). * @return string */ public function get_price_suffix( $price = '', $qty = 1 ) { $suffix = get_option( 'woocommerce_price_display_suffix' ); if ( strstr( $suffix, '{' ) ) { return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty ); } else { return parent::get_price_suffix( $price, $qty ); } } /** * Return a products child ids. * * This is lazy loaded as it's not used often and does require several queries. * * @param bool|string $visible_only Visible only. * @return array Children ids */ public function get_children( $visible_only = '' ) { if ( is_bool( $visible_only ) ) { wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' ); return $visible_only ? $this->get_visible_children() : $this->get_children(); } if ( null === $this->children ) { $children = $this->data_store->read_children( $this ); $this->set_children( $children['all'] ); $this->set_visible_children( $children['visible'] ); } return apply_filters( 'woocommerce_get_children', $this->children, $this, false ); } /** * Return a products child ids - visible only. * * This is lazy loaded as it's not used often and does require several queries. * * @since 3.0.0 * @return array Children ids */ public function get_visible_children() { if ( null === $this->visible_children ) { $children = $this->data_store->read_children( $this ); $this->set_children( $children['all'] ); $this->set_visible_children( $children['visible'] ); } return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true ); } /** * Return an array of attributes used for variations, as well as their possible values. * * This is lazy loaded as it's not used often and does require several queries. * * @return array Attributes and their available values */ public function get_variation_attributes() { if ( null === $this->variation_attributes ) { $this->variation_attributes = $this->data_store->read_variation_attributes( $this ); } return $this->variation_attributes; } /** * If set, get the default attributes for a variable product. * * @param string $attribute_name Attribute name. * @return string */ public function get_variation_default_attribute( $attribute_name ) { $defaults = $this->get_default_attributes(); $attribute_name = sanitize_title( $attribute_name ); return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : ''; } /** * Variable products themselves cannot be downloadable. * * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_downloadable( $context = 'view' ) { return false; } /** * Variable products themselves cannot be virtual. * * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_virtual( $context = 'view' ) { return false; } /** * Get an array of available variations for the current product. * * @param string $return Optional. The format to return the results in. Can be 'array' to return an array of variation data or 'objects' for the product objects. Default 'array'. * * @return array[]|WC_Product_Variation[] */ public function get_available_variations( $return = 'array' ) { $variation_ids = $this->get_children(); $available_variations = array(); if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $variation_ids ); } foreach ( $variation_ids as $variation_id ) { $variation = wc_get_product( $variation_id ); // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { continue; } // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { continue; } if ( 'array' === $return ) { $available_variations[] = $this->get_available_variation( $variation ); } else { $available_variations[] = $variation; } } if ( 'array' === $return ) { $available_variations = array_values( array_filter( $available_variations ) ); } return $available_variations; } /** * Check if a given variation is currently available. * * @param WC_Product_Variation $variation Variation to check. * * @return bool True if the variation is available, false otherwise. */ private function variation_is_available( WC_Product_Variation $variation ) { // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { return false; } // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { return false; } return true; } /** * Returns an array of data for a variation. Used in the add to cart form. * * @since 2.4.0 * @param WC_Product $variation Variation product object or ID. * @return array|bool */ public function get_available_variation( $variation ) { if ( is_numeric( $variation ) ) { $variation = wc_get_product( $variation ); } if ( ! $variation instanceof WC_Product_Variation ) { return false; } // See if prices should be shown for each variation after selection. $show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation ); return apply_filters( 'woocommerce_available_variation', array( 'attributes' => $variation->get_variation_attributes(), 'availability_html' => wc_get_stock_html( $variation ), 'backorders_allowed' => $variation->backorders_allowed(), 'dimensions' => $variation->get_dimensions( false ), 'dimensions_html' => wc_format_dimensions( $variation->get_dimensions( false ) ), 'display_price' => wc_get_price_to_display( $variation ), 'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ), 'image' => wc_get_product_attachment_props( $variation->get_image_id() ), 'image_id' => $variation->get_image_id(), 'is_downloadable' => $variation->is_downloadable(), 'is_in_stock' => $variation->is_in_stock(), 'is_purchasable' => $variation->is_purchasable(), 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', 'is_virtual' => $variation->is_virtual(), 'max_qty' => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '', 'min_qty' => $variation->get_min_purchase_quantity(), 'price_html' => $show_variation_price ? '<span class="price">' . $variation->get_price_html() . '</span>' : '', 'sku' => $variation->get_sku(), 'variation_description' => wc_format_content( $variation->get_description() ), 'variation_id' => $variation->get_id(), 'variation_is_active' => $variation->variation_is_active(), 'variation_is_visible' => $variation->variation_is_visible(), 'weight' => $variation->get_weight(), 'weight_html' => wc_format_weight( $variation->get_weight() ), ), $this, $variation ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Sets an array of variation attributes. * * @since 3.0.0 * @param array $variation_attributes Attributes list. */ public function set_variation_attributes( $variation_attributes ) { $this->variation_attributes = $variation_attributes; } /** * Sets an array of children for the product. * * @since 3.0.0 * @param array $children Children products. */ public function set_children( $children ) { $this->children = array_filter( wp_parse_id_list( (array) $children ) ); } /** * Sets an array of visible children only. * * @since 3.0.0 * @param array $visible_children List of visible children products. */ public function set_visible_children( $visible_children ) { $this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) ); } /* |-------------------------------------------------------------------------- | CRUD methods |-------------------------------------------------------------------------- */ /** * Ensure properties are set correctly before save. * * @since 3.0.0 */ public function validate_props() { parent::validate_props(); if ( ! $this->get_manage_stock() ) { $this->data_store->sync_stock_status( $this ); } } /** * Do any extra processing needed before the actual product save * (but after triggering the 'woocommerce_before_..._object_save' action) * * @return mixed A state value that will be passed to after_data_store_save_or_update. */ protected function before_data_store_save_or_update() { // Get names before save. $previous_name = $this->data['name']; $new_name = $this->get_name( 'edit' ); return array( 'previous_name' => $previous_name, 'new_name' => $new_name, ); } /** * Do any extra processing needed after the actual product save * (but before triggering the 'woocommerce_after_..._object_save' action) * * @param mixed $state The state object that was returned by before_data_store_save_or_update. */ protected function after_data_store_save_or_update( $state ) { $this->data_store->sync_variation_names( $this, $state['previous_name'], $state['new_name'] ); $this->data_store->sync_managed_variation_stock_status( $this ); } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- */ /** * Returns whether or not the product is on sale. * * @param string $context What the value is for. Valid values are view and edit. What the value is for. Valid values are view and edit. * @return bool */ public function is_on_sale( $context = 'view' ) { $prices = $this->get_variation_prices(); $on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price']; return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; } /** * Is a child in stock? * * @return boolean */ public function child_is_in_stock() { return $this->data_store->child_is_in_stock( $this ); } /** * Is a child on backorder? * * @since 3.3.0 * @return boolean */ public function child_is_on_backorder() { return $this->data_store->child_has_stock_status( $this, 'onbackorder' ); } /** * Does a child have a weight set? * * @return boolean */ public function child_has_weight() { $transient_name = 'wc_child_has_weight_' . $this->get_id(); $has_weight = get_transient( $transient_name ); if ( false === $has_weight ) { $has_weight = $this->data_store->child_has_weight( $this ); set_transient( $transient_name, (int) $has_weight, DAY_IN_SECONDS * 30 ); } return (bool) $has_weight; } /** * Does a child have dimensions set? * * @return boolean */ public function child_has_dimensions() { $transient_name = 'wc_child_has_dimensions_' . $this->get_id(); $has_dimension = get_transient( $transient_name ); if ( false === $has_dimension ) { $has_dimension = $this->data_store->child_has_dimensions( $this ); set_transient( $transient_name, (int) $has_dimension, DAY_IN_SECONDS * 30 ); } return (bool) $has_dimension; } /** * Returns whether or not the product has dimensions set. * * @return bool */ public function has_dimensions() { return parent::has_dimensions() || $this->child_has_dimensions(); } /** * Returns whether or not the product has weight set. * * @return bool */ public function has_weight() { return parent::has_weight() || $this->child_has_weight(); } /** * Returns whether or not the product has additional options that need * selecting before adding to cart. * * @since 3.0.0 * @return boolean */ public function has_options() { return apply_filters( 'woocommerce_product_has_options', true, $this ); } /* |-------------------------------------------------------------------------- | Sync with child variations. |-------------------------------------------------------------------------- */ /** * Sync a variable product with it's children. These sync functions sync * upwards (from child to parent) when the variation is saved. * * @param WC_Product|int $product Product object or ID for which you wish to sync. * @param bool $save If true, the product object will be saved to the DB before returning it. * @return WC_Product Synced product object. */ public static function sync( $product, $save = true ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = wc_get_product( $product ); } if ( is_a( $product, 'WC_Product_Variable' ) ) { $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); $data_store->sync_price( $product ); $data_store->sync_stock_status( $product ); self::sync_attributes( $product ); // Legacy update of attributes. do_action( 'woocommerce_variable_product_sync_data', $product ); if ( $save ) { $product->save(); } wc_do_deprecated_action( 'woocommerce_variable_product_sync', array( $product->get_id(), $product->get_visible_children(), ), '3.0', 'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product' ); } return $product; } /** * Sync parent stock status with the status of all children and save. * * @param WC_Product|int $product Product object or ID for which you wish to sync. * @param bool $save If true, the product object will be saved to the DB before returning it. * @return WC_Product Synced product object. */ public static function sync_stock_status( $product, $save = true ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = wc_get_product( $product ); } if ( is_a( $product, 'WC_Product_Variable' ) ) { $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); $data_store->sync_stock_status( $product ); if ( $save ) { $product->save(); } } return $product; } /** * Sort an associative array of $variation_id => $price pairs in order of min and max prices. * * @param array $prices associative array of $variation_id => $price pairs. * @return array */ protected function sort_variation_prices( $prices ) { asort( $prices ); return $prices; } } includes/class-woocommerce.php 0000644 00000104142 15132754523 0012516 0 ustar 00 <?php /** * WooCommerce setup * * @package WooCommerce * @since 3.2.0 */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\AssignDefaultCategory; use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster; use Automattic\WooCommerce\Proxies\LegacyProxy; /** * Main WooCommerce Class. * * @class WooCommerce */ final class WooCommerce { /** * WooCommerce version. * * @var string */ public $version = '5.9.1'; /** * WooCommerce Schema version. * * @since 4.3 started with version string 430. * * @var string */ public $db_version = '430'; /** * The single instance of the class. * * @var WooCommerce * @since 2.1 */ protected static $_instance = null; /** * Session instance. * * @var WC_Session|WC_Session_Handler */ public $session = null; /** * Query instance. * * @var WC_Query */ public $query = null; /** * Product factory instance. * * @var WC_Product_Factory */ public $product_factory = null; /** * Countries instance. * * @var WC_Countries */ public $countries = null; /** * Integrations instance. * * @var WC_Integrations */ public $integrations = null; /** * Cart instance. * * @var WC_Cart */ public $cart = null; /** * Customer instance. * * @var WC_Customer */ public $customer = null; /** * Order factory instance. * * @var WC_Order_Factory */ public $order_factory = null; /** * Structured data instance. * * @var WC_Structured_Data */ public $structured_data = null; /** * Array of deprecated hook handlers. * * @var array of WC_Deprecated_Hooks */ public $deprecated_hook_handlers = array(); /** * Main WooCommerce Instance. * * Ensures only one instance of WooCommerce is loaded or can be loaded. * * @since 2.1 * @static * @see WC() * @return WooCommerce - Main instance. */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Cloning is forbidden. * * @since 2.1 */ public function __clone() { wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. * * @since 2.1 */ public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' ); } /** * Auto-load in-accessible properties on demand. * * @param mixed $key Key name. * @return mixed */ public function __get( $key ) { if ( in_array( $key, array( 'payment_gateways', 'shipping', 'mailer', 'checkout' ), true ) ) { return $this->$key(); } } /** * WooCommerce Constructor. */ public function __construct() { $this->define_constants(); $this->define_tables(); $this->includes(); $this->init_hooks(); } /** * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook. * * This ensures `woocommerce_loaded` is called only after all other plugins * are loaded, to avoid issues caused by plugin directory naming changing * the load order. See #21524 for details. * * @since 3.6.0 */ public function on_plugins_loaded() { do_action( 'woocommerce_loaded' ); } /** * Hook into actions and filters. * * @since 2.3 */ private function init_hooks() { register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) ); register_shutdown_function( array( $this, 'log_errors' ) ); add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 ); add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) ); add_action( 'after_setup_theme', array( $this, 'setup_environment' ) ); add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 ); add_action( 'init', array( $this, 'init' ), 0 ); add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); add_action( 'init', array( $this, 'add_image_sizes' ) ); add_action( 'init', array( $this, 'load_rest_api' ) ); add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) ); add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) ); // These classes set up hooks on instantiation. wc_get_container()->get( DownloadPermissionsAdjuster::class ); wc_get_container()->get( AssignDefaultCategory::class ); wc_get_container()->get( DataRegenerator::class ); wc_get_container()->get( LookupDataStore::class ); wc_get_container()->get( RestockRefundedItemsAdjuster::class ); } /** * Add woocommerce_inbox_variant for the Remote Inbox Notification. * * P2 post can be found at https://wp.me/paJDYF-1uJ. */ public function add_woocommerce_inbox_variant() { $config_name = 'woocommerce_inbox_variant_assignment'; if ( false === get_option( $config_name, false ) ) { update_option( $config_name, wp_rand( 1, 12 ) ); } } /** * Ensures fatal errors are logged so they can be picked up in the status report. * * @since 3.2.0 */ public function log_errors() { $error = error_get_last(); if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { $logger = wc_get_logger(); $logger->critical( /* translators: 1: error message 2: file name and path 3: line number */ sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL, array( 'source' => 'fatal-errors', ) ); do_action( 'woocommerce_shutdown_error', $error ); } } /** * Define WC Constants. */ private function define_constants() { $upload_dir = wp_upload_dir( null, false ); $this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' ); $this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) ); $this->define( 'WC_VERSION', $this->version ); $this->define( 'WOOCOMMERCE_VERSION', $this->version ); $this->define( 'WC_ROUNDING_PRECISION', 6 ); $this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 ); $this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 ); $this->define( 'WC_DELIMITER', '|' ); $this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' ); $this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' ); $this->define( 'WC_TEMPLATE_DEBUG_MODE', false ); $this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' ); $this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' ); $this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION ); /** Define if we're checking against major, minor or no versions in the following places: * - plugin screen in WP Admin (displaying extra warning when updating to new major versions) * - System Status Report ('Installed version not tested with active version of WooCommerce' warning) * - core update screen in WP Admin (displaying extra warning when updating to new major versions) * - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins * that don't declare compatibility, the auto-update is disabled) * * We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed. * The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report. */ $this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' ); } /** * Register custom tables within $wpdb object. */ private function define_tables() { global $wpdb; // List of tables without prefixes. $tables = array( 'payment_tokenmeta' => 'woocommerce_payment_tokenmeta', 'order_itemmeta' => 'woocommerce_order_itemmeta', 'wc_product_meta_lookup' => 'wc_product_meta_lookup', 'wc_tax_rate_classes' => 'wc_tax_rate_classes', 'wc_reserved_stock' => 'wc_reserved_stock', ); foreach ( $tables as $name => $table ) { $wpdb->$name = $wpdb->prefix . $table; $wpdb->tables[] = $table; } } /** * Define constant if not already set. * * @param string $name Constant name. * @param string|bool $value Constant value. */ private function define( $name, $value ) { if ( ! defined( $name ) ) { define( $name, $value ); } } /** * Returns true if the request is a non-legacy REST API request. * * Legacy REST requests should still run some extra code for backwards compatibility. * * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061. * * @return bool */ public function is_rest_api_request() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); } /** * Load REST API. */ public function load_rest_api() { \Automattic\WooCommerce\RestApi\Server::instance()->init(); } /** * What type of request is this? * * @param string $type admin, ajax, cron or frontend. * @return bool */ private function is_request( $type ) { switch ( $type ) { case 'admin': return is_admin(); case 'ajax': return defined( 'DOING_AJAX' ); case 'cron': return defined( 'DOING_CRON' ); case 'frontend': return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); } } /** * Include required core files used in admin and on the frontend. */ public function includes() { /** * Class autoloader. */ include_once WC_ABSPATH . 'includes/class-wc-autoloader.php'; /** * Interfaces. */ include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php'; /** * Core traits. */ include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php'; /** * Abstract classes. */ include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php'; /** * Core classes. */ include_once WC_ABSPATH . 'includes/wc-core-functions.php'; include_once WC_ABSPATH . 'includes/class-wc-datetime.php'; include_once WC_ABSPATH . 'includes/class-wc-post-types.php'; include_once WC_ABSPATH . 'includes/class-wc-install.php'; include_once WC_ABSPATH . 'includes/class-wc-geolocation.php'; include_once WC_ABSPATH . 'includes/class-wc-download-handler.php'; include_once WC_ABSPATH . 'includes/class-wc-comments.php'; include_once WC_ABSPATH . 'includes/class-wc-post-data.php'; include_once WC_ABSPATH . 'includes/class-wc-ajax.php'; include_once WC_ABSPATH . 'includes/class-wc-emails.php'; include_once WC_ABSPATH . 'includes/class-wc-data-exception.php'; include_once WC_ABSPATH . 'includes/class-wc-query.php'; include_once WC_ABSPATH . 'includes/class-wc-meta-data.php'; include_once WC_ABSPATH . 'includes/class-wc-order-factory.php'; include_once WC_ABSPATH . 'includes/class-wc-order-query.php'; include_once WC_ABSPATH . 'includes/class-wc-product-factory.php'; include_once WC_ABSPATH . 'includes/class-wc-product-query.php'; include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php'; include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php'; include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php'; include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php'; include_once WC_ABSPATH . 'includes/class-wc-countries.php'; include_once WC_ABSPATH . 'includes/class-wc-integrations.php'; include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php'; include_once WC_ABSPATH . 'includes/class-wc-https.php'; include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php'; include_once WC_ABSPATH . 'includes/class-wc-discounts.php'; include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php'; include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php'; include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php'; include_once WC_ABSPATH . 'includes/class-wc-privacy.php'; include_once WC_ABSPATH . 'includes/class-wc-structured-data.php'; include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php'; include_once WC_ABSPATH . 'includes/class-wc-logger.php'; include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php'; include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php'; include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php'; include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php'; /** * Data stores - used to store and retrieve CRUD object data from the database. */ include_once WC_ABSPATH . 'includes/class-wc-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php'; /** * REST API. */ include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php'; include_once WC_ABSPATH . 'includes/class-wc-api.php'; include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php'; include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php'; include_once WC_ABSPATH . 'includes/class-wc-auth.php'; include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php'; /** * WCCOM Site. */ include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php'; /** * Libraries and packages. */ include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php'; if ( defined( 'WP_CLI' ) && WP_CLI ) { include_once WC_ABSPATH . 'includes/class-wc-cli.php'; } if ( $this->is_request( 'admin' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; } if ( $this->is_request( 'frontend' ) ) { $this->frontend_includes(); } if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { include_once WC_ABSPATH . 'includes/class-wc-tracker.php'; } $this->theme_support_includes(); $this->query = new WC_Query(); $this->api = new WC_API(); $this->api->init(); } /** * Include classes for theme support. * * @since 3.3.0 */ private function theme_support_includes() { if ( wc_is_wp_default_theme_active() ) { switch ( get_template() ) { case 'twentyten': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php'; break; case 'twentyeleven': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php'; break; case 'twentytwelve': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php'; break; case 'twentythirteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php'; break; case 'twentyfourteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php'; break; case 'twentyfifteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php'; break; case 'twentysixteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php'; break; case 'twentyseventeen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php'; break; case 'twentynineteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php'; break; case 'twentytwenty': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php'; break; case 'twentytwentyone': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php'; break; } } } /** * Include required frontend files. */ public function frontend_includes() { include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; include_once WC_ABSPATH . 'includes/wc-template-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-template-loader.php'; include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php'; include_once WC_ABSPATH . 'includes/class-wc-form-handler.php'; include_once WC_ABSPATH . 'includes/class-wc-cart.php'; include_once WC_ABSPATH . 'includes/class-wc-tax.php'; include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php'; include_once WC_ABSPATH . 'includes/class-wc-customer.php'; include_once WC_ABSPATH . 'includes/class-wc-embed.php'; include_once WC_ABSPATH . 'includes/class-wc-session-handler.php'; } /** * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes. */ public function include_template_functions() { include_once WC_ABSPATH . 'includes/wc-template-functions.php'; } /** * Init WooCommerce when WordPress Initialises. */ public function init() { // Before init action. do_action( 'before_woocommerce_init' ); // Set up localisation. $this->load_plugin_textdomain(); // Load class instances. $this->product_factory = new WC_Product_Factory(); $this->order_factory = new WC_Order_Factory(); $this->countries = new WC_Countries(); $this->integrations = new WC_Integrations(); $this->structured_data = new WC_Structured_Data(); $this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks(); $this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks(); // Classes/actions loaded for the frontend and for ajax requests. if ( $this->is_request( 'frontend' ) ) { wc_load_cart(); } $this->load_webhooks(); // Init action. do_action( 'woocommerce_init' ); } /** * Load Localisation files. * * Note: the first-loaded translation file overrides any following ones if the same translation is present. * * Locales found in: * - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo * - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo */ public function load_plugin_textdomain() { $locale = determine_locale(); $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' ); unload_textdomain( 'woocommerce' ); load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' ); load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' ); } /** * Ensure theme and server variable compatibility and setup image sizes. */ public function setup_environment() { /** * WC_TEMPLATE_PATH constant. * * @deprecated 2.2 Use WC()->template_path() instead. */ $this->define( 'WC_TEMPLATE_PATH', $this->template_path() ); $this->add_thumbnail_support(); } /** * Ensure post thumbnail support is turned on. */ private function add_thumbnail_support() { if ( ! current_theme_supports( 'post-thumbnails' ) ) { add_theme_support( 'post-thumbnails' ); } add_post_type_support( 'product', 'thumbnail' ); } /** * Add WC Image sizes to WP. * * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce * and defining an array of args. If these are not defined, we will use defaults. This is * handled in wc_get_image_size function. * * 3.3 sizes: * * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout. * woocommerce_single - Used on single product pages for the main image. * * @since 2.3 */ public function add_image_sizes() { $thumbnail = wc_get_image_size( 'thumbnail' ); $single = wc_get_image_size( 'single' ); $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); /** * Legacy image sizes. * * @deprecated 3.3.0 These sizes will be removed in 4.6.0. */ add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] ); add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); } /** * Get the plugin url. * * @return string */ public function plugin_url() { return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) ); } /** * Get the plugin path. * * @return string */ public function plugin_path() { return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ); } /** * Get the template path. * * @return string */ public function template_path() { return apply_filters( 'woocommerce_template_path', 'woocommerce/' ); } /** * Get Ajax URL. * * @return string */ public function ajax_url() { return admin_url( 'admin-ajax.php', 'relative' ); } /** * Return the WC API URL for a given request. * * @param string $request Requested endpoint. * @param bool|null $ssl If should use SSL, null if should auto detect. Default: null. * @return string */ public function api_request_url( $request, $ssl = null ) { if ( is_null( $ssl ) ) { $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); } elseif ( $ssl ) { $scheme = 'https'; } else { $scheme = 'http'; } if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) { $api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) ); } elseif ( get_option( 'permalink_structure' ) ) { $api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) ); } else { $api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) ); } return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) ); } /** * Load & enqueue active webhooks. * * @since 2.2 */ private function load_webhooks() { if ( ! is_blog_installed() ) { return; } /** * Hook: woocommerce_load_webhooks_limit. * * @since 3.6.0 * @param int $limit Used to limit how many webhooks are loaded. Default: no limit. */ $limit = apply_filters( 'woocommerce_load_webhooks_limit', null ); wc_load_webhooks( 'active', $limit ); } /** * Initialize the customer and cart objects and setup customer saving on shutdown. * * @since 3.6.4 * @return void */ public function initialize_cart() { // Cart needs customer info. if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) { $this->customer = new WC_Customer( get_current_user_id(), true ); // Customer should be saved during shutdown. add_action( 'shutdown', array( $this->customer, 'save' ), 10 ); } if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) { $this->cart = new WC_Cart(); } } /** * Initialize the session class. * * @since 3.6.4 * @return void */ public function initialize_session() { // Session class, handles session data for users - can be overwritten if custom handler is needed. $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) { $this->session = new $session_class(); $this->session->init(); } } /** * Set tablenames inside WPDB object. */ public function wpdb_table_fix() { $this->define_tables(); } /** * Ran when any plugin is activated. * * @since 3.6.0 * @param string $filename The filename of the activated plugin. */ public function activated_plugin( $filename ) { include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; if ( '/woocommerce.php' === substr( $filename, -16 ) ) { set_transient( 'woocommerce_activated_plugin', $filename ); } WC_Helper::activated_plugin( $filename ); } /** * Ran when any plugin is deactivated. * * @since 3.6.0 * @param string $filename The filename of the deactivated plugin. */ public function deactivated_plugin( $filename ) { include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; WC_Helper::deactivated_plugin( $filename ); } /** * Get queue instance. * * @return WC_Queue_Interface */ public function queue() { return WC_Queue::instance(); } /** * Get Checkout Class. * * @return WC_Checkout */ public function checkout() { return WC_Checkout::instance(); } /** * Get gateways class. * * @return WC_Payment_Gateways */ public function payment_gateways() { return WC_Payment_Gateways::instance(); } /** * Get shipping class. * * @return WC_Shipping */ public function shipping() { return WC_Shipping::instance(); } /** * Email Class. * * @return WC_Emails */ public function mailer() { return WC_Emails::instance(); } /** * Check if plugin assets are built and minified * * @return bool */ public function build_dependencies_satisfied() { // Check if we have compiled CSS. if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) { return false; } // Check if we have minified JS. if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) { return false; } return true; } /** * Output a admin notice when build dependencies not met. * * @return void */ public function build_dependencies_notice() { if ( $this->build_dependencies_satisfied() ) { return; } $message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run <code>grunt assets</code> to build and minify assets.', 'woocommerce' ); $message_two = sprintf( /* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */ __( 'Or you can download a pre-built version of the plugin from the <a href="%1$s">WordPress.org repository</a> or by visiting <a href="%2$s">the releases page in the GitHub repository</a>.', 'woocommerce' ), 'https://wordpress.org/plugins/woocommerce/', 'https://github.com/woocommerce/woocommerce/releases' ); printf( '<div class="error"><p>%s %s</p></div>', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Is the WooCommerce Admin actively included in the WooCommerce core? * Based on presence of a basic WC Admin function. * * @return boolean */ public function is_wc_admin_active() { return function_exists( 'wc_admin_url' ); } /** * Call a user function. This should be used to execute any non-idempotent function, especially * those in the `includes` directory or provided by WordPress. * * This method can be useful for unit tests, since functions called using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. * * @param string $function_name The function to execute. * @param mixed ...$parameters The parameters to pass to the function. * * @return mixed The result from the function. * * @since 4.4 */ public function call_function( $function_name, ...$parameters ) { return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); } /** * Call a static method in a class. This should be used to execute any non-idempotent method in classes * from the `includes` directory. * * This method can be useful for unit tests, since methods called using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. * * @param string $class_name The name of the class containing the method. * @param string $method_name The name of the method. * @param mixed ...$parameters The parameters to pass to the method. * * @return mixed The result from the method. * * @since 4.4 */ public function call_static( $class_name, $method_name, ...$parameters ) { return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); } /** * Gets an instance of a given legacy class. * This must not be used to get instances of classes in the `src` directory. * * This method can be useful for unit tests, since objects obtained using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. * * @param string $class_name The name of the class to get an instance for. * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. * * @return object The instance of the class. * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. * * @since 4.4 */ public function get_instance_of( string $class_name, ...$args ) { return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); } } includes/cli/class-wc-cli-tracker-command.php 0000644 00000002353 15132754523 0015172 0 ustar 00 <?php /** * WC_CLI_Tracker_Command class file. * * @package WooCommerce\CLI */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Allows access to tracker snapshot for transparency and debugging. * * @since 5.5.0 * @package WooCommerce */ class WC_CLI_Tracker_Command { /** * Registers a command for showing WooCommerce Tracker snapshot data. */ public static function register_commands() { WP_CLI::add_command( 'wc tracker snapshot', array( 'WC_CLI_Tracker_Command', 'show_tracker_snapshot' ) ); } /** * Dump tracker snapshot data to screen. * * ## EXAMPLES * * wp wc tracker snapshot --format=yaml * wp wc tracker snapshot --format=json * * ## OPTIONS * * [--format=<format>] * : Render output in a particular format, see WP_CLI\Formatter for details. * * @see \WP_CLI\Formatter * @see WC_Tracker::get_tracking_data() * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public static function show_tracker_snapshot( $args, $assoc_args ) { $snapshot_data = WC_Tracker::get_tracking_data(); $formatter = new \WP_CLI\Formatter( $assoc_args, array_keys( $snapshot_data ) ); $formatter->display_items( array( $snapshot_data ) ); } } includes/cli/class-wc-cli-tool-command.php 0000644 00000005546 15132754523 0014523 0 ustar 00 <?php /** * WC_CLI_Tool_Command class file. * * @package WooCommerce\CLI */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Hooks up our system status tools to the CLI. * * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT). * https://github.com/wp-cli/restful * * @version 3.0.0 * @package WooCommerce */ class WC_CLI_Tool_Command { /** * Registers just a 'list' and 'run' command to the WC CLI * since we only want to enable certain actions on the system status * tools endpoints. */ public static function register_commands() { global $wp_rest_server; $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/system_status/tools' ); $response = $wp_rest_server->dispatch( $request ); $response_data = $response->get_data(); if ( empty( $response_data ) ) { return; } $parent = 'wc tool'; $supported_commands = array( 'list', 'run' ); foreach ( $supported_commands as $command ) { $synopsis = array(); if ( 'run' === $command ) { $synopsis[] = array( 'name' => 'id', 'type' => 'positional', 'description' => __( 'The id for the resource.', 'woocommerce' ), 'optional' => false, ); $method = 'update_item'; $route = '/wc/v2/system_status/tools/(?P<id>[\w-]+)'; } elseif ( 'list' === $command ) { $synopsis[] = array( 'name' => 'fields', 'type' => 'assoc', 'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ), 'optional' => true, ); $synopsis[] = array( 'name' => 'field', 'type' => 'assoc', 'description' => __( 'Get the value of an individual field.', 'woocommerce' ), 'optional' => true, ); $synopsis[] = array( 'name' => 'format', 'type' => 'assoc', 'description' => __( 'Render response in a particular format.', 'woocommerce' ), 'optional' => true, 'default' => 'table', 'options' => array( 'table', 'json', 'csv', 'ids', 'yaml', 'count', 'headers', 'body', 'envelope', ), ); $method = 'list_items'; $route = '/wc/v2/system_status/tools'; } $before_invoke = null; if ( empty( $command_args['when'] ) && WP_CLI::get_config( 'debug' ) ) { $before_invoke = function() { wc_maybe_define_constant( 'SAVEQUERIES', true ); }; } $rest_command = new WC_CLI_REST_Command( 'system_status_tool', $route, $response_data['schema'] ); WP_CLI::add_command( "{$parent} {$command}", array( $rest_command, $method ), array( 'synopsis' => $synopsis, 'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '', 'before_invoke' => $before_invoke, ) ); } } } includes/cli/class-wc-cli-rest-command.php 0000644 00000032267 15132754523 0014523 0 ustar 00 <?php /** * WP_CLI_Rest_Command class file. * * @package WooCommerce\CLI */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Main Command for WooCommere CLI. * * Since a lot of WC operations can be handled via the REST API, we base our CLI * off of Restful to generate commands for each WooCommerce REST API endpoint * so most of the logic is shared. * * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT). * https://github.com/wp-cli/restful * * @version 3.0.0 * @package WooCommerce */ class WC_CLI_REST_Command { /** * Endpoints that have a parent ID. * Ex: Product reviews, which has a product ID and a review ID. * * @var array */ protected $routes_with_parent_id = array( 'customer_download', 'product_review', 'order_note', 'shop_order_refund', ); /** * Name of command/endpoint object. * * @var string */ private $name; /** * Endpoint route. * * @var string */ private $route; /** * Main resource ID. * * @var int */ private $resource_identifier; /** * Schema for command. * * @var array */ private $schema; /** * List of supported IDs and their description (name => desc). * * @var array */ private $supported_ids = array(); /** * Sets up REST Command. * * @param string $name Name of endpoint object (comes from schema). * @param string $route Path to route of this endpoint. * @param array $schema Schema object. */ public function __construct( $name, $route, $schema ) { $this->name = $name; preg_match_all( '#\([^\)]+\)#', $route, $matches ); $first_match = $matches[0]; $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; $this->route = rtrim( $route ); $this->schema = $schema; $this->resource_identifier = $resource_id; if ( in_array( $name, $this->routes_with_parent_id, true ) ) { $is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id; if ( ! $is_singular ) { $this->resource_identifier = $first_match[0]; } } } /** * Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. * * @param array $supported_ids List of supported IDs. */ public function set_supported_ids( $supported_ids = array() ) { $this->supported_ids = $supported_ids; } /** * Returns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. * * @return array */ public function get_supported_ids() { return $this->supported_ids; } /** * Create a new item. * * @subcommand create * * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public function create_item( $args, $assoc_args ) { $assoc_args = self::decode_json( $assoc_args ); list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $body['id'] ); } else { WP_CLI::success( "Created {$this->name} {$body['id']}." ); } } /** * Delete an existing item. * * @subcommand delete * * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public function delete_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); $object_id = isset( $body['id'] ) ? $body['id'] : ''; if ( ! $object_id && isset( $body['slug'] ) ) { $object_id = $body['slug']; } if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $object_id ); } else { if ( empty( $assoc_args['force'] ) ) { WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$object_id}" ); } else { WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$object_id}." ); } } } /** * Get a single item. * * @subcommand get * * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public function get_item( $args, $assoc_args ) { $route = $this->get_filled_route( $args ); list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args ); if ( ! empty( $assoc_args['fields'] ) ) { $body = self::limit_item_to_fields( $body, $assoc_args['fields'] ); } if ( empty( $assoc_args['format'] ) ) { $assoc_args['format'] = 'table'; } if ( 'headers' === $assoc_args['format'] ) { echo wp_json_encode( $headers ); } elseif ( 'body' === $assoc_args['format'] ) { echo wp_json_encode( $body ); } elseif ( 'envelope' === $assoc_args['format'] ) { echo wp_json_encode( array( 'body' => $body, 'headers' => $headers, 'status' => $status, ) ); } else { $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $body ); } } /** * List all items. * * @subcommand list * * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public function list_items( $args, $assoc_args ) { if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { $method = 'HEAD'; } else { $method = 'GET'; } if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) { $assoc_args['per_page'] = '100'; } list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_column( $body, 'id' ); } else { $items = $body; } if ( ! empty( $assoc_args['fields'] ) ) { foreach ( $items as $key => $item ) { $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] ); } } if ( empty( $assoc_args['format'] ) ) { $assoc_args['format'] = 'table'; } if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { echo (int) $headers['X-WP-Total']; } elseif ( 'headers' === $assoc_args['format'] ) { echo wp_json_encode( $headers ); } elseif ( 'body' === $assoc_args['format'] ) { echo wp_json_encode( $body ); } elseif ( 'envelope' === $assoc_args['format'] ) { echo wp_json_encode( array( 'body' => $body, 'headers' => $headers, 'status' => $status, 'api_url' => $this->api_url, ) ); } else { $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $items ); } } /** * Update an existing item. * * @subcommand update * * @param array $args WP-CLI positional arguments. * @param array $assoc_args WP-CLI associative arguments. */ public function update_item( $args, $assoc_args ) { $assoc_args = self::decode_json( $assoc_args ); list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $body['id'] ); } else { WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." ); } } /** * Do a REST Request * * @param string $method Request method. Examples: 'POST', 'PUT', 'DELETE' or 'GET'. * @param string $route Resource route. * @param array $assoc_args Associative arguments passed to the originating WP-CLI command. * * @return array */ private function do_request( $method, $route, $assoc_args ) { wc_maybe_define_constant( 'REST_REQUEST', true ); $request = new WP_REST_Request( $method, $route ); if ( in_array( $method, array( 'POST', 'PUT' ), true ) ) { $request->set_body_params( $assoc_args ); } else { foreach ( $assoc_args as $key => $value ) { $request->set_param( $key, $value ); } } if ( Constants::is_true( 'SAVEQUERIES' ) ) { $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); } $response = rest_do_request( $request ); if ( Constants::is_true( 'SAVEQUERIES' ) ) { $performed_queries = array(); foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { if ( in_array( $key, $original_queries, true ) ) { continue; } $performed_queries[] = $query; } usort( $performed_queries, function( $a, $b ) { if ( $a[1] === $b[1] ) { return 0; } return ( $a[1] > $b[1] ) ? -1 : 1; } ); $query_count = count( $performed_queries ); $query_total_time = 0; foreach ( $performed_queries as $query ) { $query_total_time += $query[1]; } $slow_query_message = ''; if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) { $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; foreach ( $performed_queries as $i => $query ) { $i++; $bits = explode( ', ', $query[2] ); $backtrace = implode( ', ', array_slice( $bits, 13 ) ); $seconds = NumberUtil::round( $query[1], 6 ); $slow_query_message .= <<<EOT {$i}: - {$seconds} seconds - {$backtrace} - {$query[0]} EOT; $slow_query_message .= PHP_EOL; } } elseif ( 'wc' !== WP_CLI::get_config( 'debug' ) ) { $slow_query_message = '. Use --debug=wc to see all queries.'; } $query_total_time = NumberUtil::round( $query_total_time, 6 ); WP_CLI::debug( "wc command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'wc' ); } $error = $response->as_error(); if ( $error ) { // For authentication errors (status 401), include a reminder to set the --user flag. // WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead. if ( 401 === $response->get_status() ) { $errors = $error->get_error_messages(); $errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}'; $error = implode( "\n", $errors ); } WP_CLI::error( $error ); } return array( $response->get_status(), $response->get_data(), $response->get_headers() ); } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return \WP_CLI\Formatter */ protected function get_formatter( &$assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { if ( is_string( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } else { $fields = $assoc_args['fields']; } } else { if ( ! empty( $assoc_args['context'] ) ) { $fields = $this->get_context_fields( $assoc_args['context'] ); } else { $fields = $this->get_context_fields( 'view' ); } } return new \WP_CLI\Formatter( $assoc_args, $fields ); } /** * Get a list of fields present in a given context * * @param string $context Scope under which the request is made. Determines fields present in response. * @return array */ private function get_context_fields( $context ) { $fields = array(); foreach ( $this->schema['properties'] as $key => $args ) { if ( empty( $args['context'] ) || in_array( $context, $args['context'], true ) ) { $fields[] = $key; } } return $fields; } /** * Get the route for this resource * * @param array $args Positional arguments passed to the originating WP-CLI command. * @return string */ private function get_filled_route( $args = array() ) { $supported_id_matched = false; $route = $this->route; foreach ( $this->get_supported_ids() as $id_name => $id_desc ) { if ( 'id' !== $id_name && strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) { $route = str_replace( array( '(?P<' . $id_name . '>[\d]+)', '(?P<' . $id_name . '>\w[\w\s\-]*)' ), $args[0], $route ); $supported_id_matched = true; } } if ( ! empty( $args ) ) { $id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0]; $route = str_replace( array( '(?P<id>[\d]+)', '(?P<id>[\w-]+)' ), $id_replacement, $route ); } return rtrim( $route ); } /** * Reduce an item to specific fields. * * @param array $item Item to reduce. * @param array $fields Fields to keep. * @return array */ private static function limit_item_to_fields( $item, $fields ) { if ( empty( $fields ) ) { return $item; } if ( is_string( $fields ) ) { $fields = explode( ',', $fields ); } foreach ( $item as $i => $field ) { if ( ! in_array( $i, $fields, true ) ) { unset( $item[ $i ] ); } } return $item; } /** * JSON can be passed in some more complicated objects, like the payment gateway settings array. * This function decodes the json (if present) and tries to get it's value. * * @param array $arr Array that will be scanned for JSON encoded values. * * @return array */ protected function decode_json( $arr ) { foreach ( $arr as $key => $value ) { if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) { $arr[ $key ] = json_decode( $value, true ); } else { continue; } } return $arr; } } includes/cli/class-wc-cli-update-command.php 0000644 00000004716 15132754523 0015026 0 ustar 00 <?php /** * WC_CLI_Update_Command class file. * * @package WooCommerce\CLI */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Allows updates via CLI. * * @version 3.0.0 * @package WooCommerce */ class WC_CLI_Update_Command { /** * Registers the update command. */ public static function register_commands() { WP_CLI::add_command( 'wc update', array( 'WC_CLI_Update_Command', 'update' ) ); } /** * Runs all pending WooCommerce database updates. */ public static function update() { global $wpdb; $wpdb->hide_errors(); include_once WC_ABSPATH . 'includes/class-wc-install.php'; include_once WC_ABSPATH . 'includes/wc-update-functions.php'; $current_db_version = get_option( 'woocommerce_db_version' ); $update_count = 0; $callbacks = WC_Install::get_db_update_callbacks(); $callbacks_to_run = array(); foreach ( $callbacks as $version => $update_callbacks ) { if ( version_compare( $current_db_version, $version, '<' ) ) { foreach ( $update_callbacks as $update_callback ) { $callbacks_to_run[] = $update_callback; } } } if ( empty( $callbacks_to_run ) ) { // Ensure DB version is set to the current WC version to match WP-Admin update routine. WC_Install::update_db_version(); /* translators: %s Database version number */ WP_CLI::success( sprintf( __( 'No updates required. Database version is %s', 'woocommerce' ), get_option( 'woocommerce_db_version' ) ) ); return; } /* translators: 1: Number of database updates 2: List of update callbacks */ WP_CLI::log( sprintf( __( 'Found %1$d updates (%2$s)', 'woocommerce' ), count( $callbacks_to_run ), implode( ', ', $callbacks_to_run ) ) ); $progress = \WP_CLI\Utils\make_progress_bar( __( 'Updating database', 'woocommerce' ), count( $callbacks_to_run ) ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound foreach ( $callbacks_to_run as $update_callback ) { call_user_func( $update_callback ); $result = false; while ( $result ) { $result = (bool) call_user_func( $update_callback ); } $update_count ++; $progress->tick(); } $progress->finish(); WC_Admin_Notices::remove_notice( 'update', true ); /* translators: 1: Number of database updates performed 2: Database version number */ WP_CLI::success( sprintf( __( '%1$d update functions completed. Database version is %2$s', 'woocommerce' ), absint( $update_count ), get_option( 'woocommerce_db_version' ) ) ); } } includes/cli/class-wc-cli-runner.php 0000644 00000020354 15132754523 0013435 0 ustar 00 <?php /** * WP_CLI_Runner class file. * * @package WooCommerce\CLI */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC API to WC CLI Bridge. * * Hooks into the REST API, figures out which endpoints come from WC, * and registers them as CLI commands. * * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT). * https://github.com/wp-cli/restful * * @version 3.0.0 * @package WooCommerce */ class WC_CLI_Runner { /** * Endpoints to disable (meaning they will not be available as CLI commands). * Some of these can either be done via WP already, or are offered with * some other changes (like tools). * * @var array */ private static $disabled_endpoints = array( 'settings', 'settings/(?P<group_id>[\w-]+)', 'settings/(?P<group_id>[\w-]+)/batch', 'settings/(?P<group_id>[\w-]+)/(?P<id>[\w-]+)', 'system_status', 'system_status/tools', 'system_status/tools/(?P<id>[\w-]+)', 'reports', 'reports/sales', 'reports/top_sellers', ); /** * The version of the REST API we should target to * generate commands. * * @var string */ private static $target_rest_version = 'v2'; /** * Register's all endpoints as commands once WP and WC have all loaded. */ public static function after_wp_load() { global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); $request = new WP_REST_Request( 'GET', '/' ); $request->set_param( 'context', 'help' ); $response = $wp_rest_server->dispatch( $request ); $response_data = $response->get_data(); if ( empty( $response_data ) ) { return; } // Loop through all of our endpoints and register any valid WC endpoints. foreach ( $response_data['routes'] as $route => $route_data ) { // Only register endpoints for WC and our target version. if ( substr( $route, 0, 4 + strlen( self::$target_rest_version ) ) !== '/wc/' . self::$target_rest_version ) { continue; } // Only register endpoints with schemas. if ( empty( $route_data['schema']['title'] ) ) { /* translators: %s: Route to a given WC-API endpoint */ WP_CLI::debug( sprintf( __( 'No schema title found for %s, skipping REST command registration.', 'woocommerce' ), $route ), 'wc' ); continue; } // Ignore batch endpoints. if ( 'batch' === $route_data['schema']['title'] ) { continue; } // Disable specific endpoints. $route_pieces = explode( '/', $route ); $endpoint_piece = str_replace( '/wc/' . $route_pieces[2] . '/', '', $route ); if ( in_array( $endpoint_piece, self::$disabled_endpoints, true ) ) { continue; } self::register_route_commands( new WC_CLI_REST_Command( $route_data['schema']['title'], $route, $route_data['schema'] ), $route, $route_data ); } } /** * Generates command information and tells WP CLI about all * commands available from a route. * * @param string $rest_command WC-API command. * @param string $route Path to route endpoint. * @param array $route_data Command data. * @param array $command_args WP-CLI command arguments. */ private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) { // Define IDs that we are looking for in the routes (in addition to id) // so that we can pass it to the rest command, and use it here to generate documentation. $supported_ids = array( 'product_id' => __( 'Product ID.', 'woocommerce' ), 'customer_id' => __( 'Customer ID.', 'woocommerce' ), 'order_id' => __( 'Order ID.', 'woocommerce' ), 'refund_id' => __( 'Refund ID.', 'woocommerce' ), 'attribute_id' => __( 'Attribute ID.', 'woocommerce' ), 'zone_id' => __( 'Zone ID.', 'woocommerce' ), 'instance_id' => __( 'Instance ID.', 'woocommerce' ), 'id' => __( 'The ID for the resource.', 'woocommerce' ), 'slug' => __( 'The slug for the resource.', 'woocommerce' ), ); $rest_command->set_supported_ids( $supported_ids ); $positional_args = array_keys( $supported_ids ); $parent = "wc {$route_data['schema']['title']}"; $supported_commands = array(); // Get a list of supported commands for each route. foreach ( $route_data['endpoints'] as $endpoint ) { preg_match_all( '#\([^\)]+\)#', $route, $matches ); $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; $trimmed_route = rtrim( $route ); $is_singular = substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id; // List a collection. if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) { $supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); } // Create a specific resource. if ( array( 'POST' ) === $endpoint['methods'] && ! $is_singular ) { $supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); } // Get a specific resource. if ( array( 'GET' ) === $endpoint['methods'] && $is_singular ) { $supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); } // Update a specific resource. if ( in_array( 'POST', $endpoint['methods'], true ) && $is_singular ) { $supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); } // Delete a specific resource. if ( array( 'DELETE' ) === $endpoint['methods'] && $is_singular ) { $supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); } } foreach ( $supported_commands as $command => $endpoint_args ) { $synopsis = array(); $arg_regs = array(); $ids = array(); foreach ( $supported_ids as $id_name => $id_desc ) { if ( strpos( $route, '<' . $id_name . '>' ) !== false ) { $synopsis[] = array( 'name' => $id_name, 'type' => 'positional', 'description' => $id_desc, 'optional' => false, ); $ids[] = $id_name; } } foreach ( $endpoint_args as $name => $args ) { if ( ! in_array( $name, $positional_args, true ) || strpos( $route, '<' . $id_name . '>' ) === false ) { $arg_regs[] = array( 'name' => $name, 'type' => 'assoc', 'description' => ! empty( $args['description'] ) ? $args['description'] : '', 'optional' => empty( $args['required'] ), ); } } foreach ( $arg_regs as $arg_reg ) { $synopsis[] = $arg_reg; } if ( in_array( $command, array( 'list', 'get' ), true ) ) { $synopsis[] = array( 'name' => 'fields', 'type' => 'assoc', 'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ), 'optional' => true, ); $synopsis[] = array( 'name' => 'field', 'type' => 'assoc', 'description' => __( 'Get the value of an individual field.', 'woocommerce' ), 'optional' => true, ); $synopsis[] = array( 'name' => 'format', 'type' => 'assoc', 'description' => __( 'Render response in a particular format.', 'woocommerce' ), 'optional' => true, 'default' => 'table', 'options' => array( 'table', 'json', 'csv', 'ids', 'yaml', 'count', 'headers', 'body', 'envelope', ), ); } if ( in_array( $command, array( 'create', 'update', 'delete' ), true ) ) { $synopsis[] = array( 'name' => 'porcelain', 'type' => 'flag', 'description' => __( 'Output just the id when the operation is successful.', 'woocommerce' ), 'optional' => true, ); } $methods = array( 'list' => 'list_items', 'create' => 'create_item', 'delete' => 'delete_item', 'get' => 'get_item', 'update' => 'update_item', ); $before_invoke = null; if ( empty( $command_args['when'] ) && \WP_CLI::get_config( 'debug' ) ) { $before_invoke = function() { wc_maybe_define_constant( 'SAVEQUERIES', true ); }; } WP_CLI::add_command( "{$parent} {$command}", array( $rest_command, $methods[ $command ] ), array( 'synopsis' => $synopsis, 'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '', 'before_invoke' => $before_invoke, ) ); } } } includes/class-wc-product-grouped.php 0000644 00000012345 15132754523 0013734 0 ustar 00 <?php /** * Grouped Product * * Grouped products cannot be purchased - they are wrappers for other products. * * @package WooCommerce\Classes\Products * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Product grouped class. */ class WC_Product_Grouped extends WC_Product { /** * Stores product data. * * @var array */ protected $extra_data = array( 'children' => array(), ); /** * Get internal type. * * @return string */ public function get_type() { return 'grouped'; } /** * Get the add to cart button text. * * @return string */ public function add_to_cart_text() { return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'View products', 'woocommerce' ), $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'View products in the “%s” group', 'woocommerce' ), $this->get_name() ), $this ); } /** * Returns whether or not the product is on sale. * * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function is_on_sale( $context = 'view' ) { $children = array_filter( array_map( 'wc_get_product', $this->get_children( $context ) ), 'wc_products_array_filter_visible_grouped' ); $on_sale = false; foreach ( $children as $child ) { if ( $child->is_purchasable() && ! $child->has_child() && $child->is_on_sale() ) { $on_sale = true; break; } } return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; } /** * Returns false if the product cannot be bought. * * @return bool */ public function is_purchasable() { return apply_filters( 'woocommerce_is_purchasable', false, $this ); } /** * Returns the price in html format. * * @param string $price (default: ''). * @return string */ public function get_price_html( $price = '' ) { $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); $child_prices = array(); $children = array_filter( array_map( 'wc_get_product', $this->get_children() ), 'wc_products_array_filter_visible_grouped' ); foreach ( $children as $child ) { if ( '' !== $child->get_price() ) { $child_prices[] = 'incl' === $tax_display_mode ? wc_get_price_including_tax( $child ) : wc_get_price_excluding_tax( $child ); } } if ( ! empty( $child_prices ) ) { $min_price = min( $child_prices ); $max_price = max( $child_prices ); } else { $min_price = ''; $max_price = ''; } if ( '' !== $min_price ) { if ( $min_price !== $max_price ) { $price = wc_format_price_range( $min_price, $max_price ); } else { $price = wc_price( $min_price ); } $is_free = 0 === $min_price && 0 === $max_price; if ( $is_free ) { $price = apply_filters( 'woocommerce_grouped_free_price_html', __( 'Free!', 'woocommerce' ), $this ); } else { $price = apply_filters( 'woocommerce_grouped_price_html', $price . $this->get_price_suffix(), $this, $child_prices ); } } else { $price = apply_filters( 'woocommerce_grouped_empty_price_html', '', $this ); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- | | Methods for getting data from the product object. */ /** * Return the children of this product. * * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_children( $context = 'view' ) { return $this->get_prop( 'children', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Methods for getting data from the product object. */ /** * Return the children of this product. * * @param array $children List of product children. */ public function set_children( $children ) { $this->set_prop( 'children', array_filter( wp_parse_id_list( (array) $children ) ) ); } /* |-------------------------------------------------------------------------- | Sync with children. |-------------------------------------------------------------------------- */ /** * Sync a grouped product with it's children. These sync functions sync * upwards (from child to parent) when the variation is saved. * * @param WC_Product|int $product Product object or ID for which you wish to sync. * @param bool $save If true, the product object will be saved to the DB before returning it. * @return WC_Product Synced product object. */ public static function sync( $product, $save = true ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = wc_get_product( $product ); } if ( is_a( $product, 'WC_Product_Grouped' ) ) { $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); $data_store->sync_price( $product ); if ( $save ) { $product->save(); } } return $product; } } includes/class-wc-product-simple.php 0000644 00000003622 15132754523 0013556 0 ustar 00 <?php /** * Simple Product Class. * * The default product type kinda product. * * @package WooCommerce\Classes\Products */ defined( 'ABSPATH' ) || exit; /** * Simple product class. */ class WC_Product_Simple extends WC_Product { /** * Initialize simple product. * * @param WC_Product|int $product Product instance or ID. */ public function __construct( $product = 0 ) { $this->supports[] = 'ajax_add_to_cart'; parent::__construct( $product ); } /** * Get internal type. * * @return string */ public function get_type() { return 'simple'; } /** * Get the add to url used mainly in loops. * * @return string */ public function add_to_cart_url() { $url = $this->is_purchasable() && $this->is_in_stock() ? remove_query_arg( 'added-to-cart', add_query_arg( array( 'add-to-cart' => $this->get_id(), ), ( function_exists( 'is_feed' ) && is_feed() ) || ( function_exists( 'is_404' ) && is_404() ) ? $this->get_permalink() : '' ) ) : $this->get_permalink(); return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); } /** * Get the add to cart button text. * * @return string */ public function add_to_cart_text() { $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read more', 'woocommerce' ); return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add “%s” to your cart', 'woocommerce' ) : __( 'Read more about “%s”', 'woocommerce' ); return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this ); } } includes/class-wc-customer-download.php 0000644 00000024553 15132754523 0014263 0 ustar 00 <?php /** * Class for customer download permissions. * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Customer download class. */ class WC_Customer_Download extends WC_Data implements ArrayAccess { /** * This is the name of this object type. * * @var string */ protected $object_type = 'customer_download'; /** * Download Data array. * * @since 3.0.0 * @var array */ protected $data = array( 'download_id' => '', 'product_id' => 0, 'user_id' => 0, 'user_email' => '', 'order_id' => 0, 'order_key' => '', 'downloads_remaining' => '', 'access_granted' => null, 'access_expires' => null, 'download_count' => 0, ); /** * Constructor. * * @param int|object|array $download Download ID, instance or data. */ public function __construct( $download = 0 ) { parent::__construct( $download ); if ( is_numeric( $download ) && $download > 0 ) { $this->set_id( $download ); } elseif ( $download instanceof self ) { $this->set_id( $download->get_id() ); } elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) { $this->set_id( $download->permission_id ); $this->set_props( (array) $download ); $this->set_object_read( true ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'customer-download' ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get download id. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_download_id( $context = 'view' ) { return $this->get_prop( 'download_id', $context ); } /** * Get product id. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_product_id( $context = 'view' ) { return $this->get_prop( 'product_id', $context ); } /** * Get user id. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_user_id( $context = 'view' ) { return $this->get_prop( 'user_id', $context ); } /** * Get user_email. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_user_email( $context = 'view' ) { return $this->get_prop( 'user_email', $context ); } /** * Get order_id. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_order_id( $context = 'view' ) { return $this->get_prop( 'order_id', $context ); } /** * Get order_key. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_order_key( $context = 'view' ) { return $this->get_prop( 'order_key', $context ); } /** * Get downloads_remaining. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer|string */ public function get_downloads_remaining( $context = 'view' ) { return $this->get_prop( 'downloads_remaining', $context ); } /** * Get access_granted. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|null Object if the date is set or null if there is no date. */ public function get_access_granted( $context = 'view' ) { return $this->get_prop( 'access_granted', $context ); } /** * Get access_expires. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|null Object if the date is set or null if there is no date. */ public function get_access_expires( $context = 'view' ) { return $this->get_prop( 'access_expires', $context ); } /** * Get download_count. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_download_count( $context = 'view' ) { // Check for count of download logs. $data_store = WC_Data_Store::load( 'customer-download-log' ); $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() ); $download_log_count = 0; if ( ! empty( $download_log_ids ) ) { $download_log_count = count( $download_log_ids ); } // Check download count in prop. $download_count_prop = $this->get_prop( 'download_count', $context ); // Return the larger of the two in case they differ. // If logs are removed for some reason, we should still respect the // count stored in the prop. return max( $download_log_count, $download_count_prop ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set download id. * * @param string $value Download ID. */ public function set_download_id( $value ) { $this->set_prop( 'download_id', $value ); } /** * Set product id. * * @param int $value Product ID. */ public function set_product_id( $value ) { $this->set_prop( 'product_id', absint( $value ) ); } /** * Set user id. * * @param int $value User ID. */ public function set_user_id( $value ) { $this->set_prop( 'user_id', absint( $value ) ); } /** * Set user_email. * * @param int $value User email. */ public function set_user_email( $value ) { $this->set_prop( 'user_email', sanitize_email( $value ) ); } /** * Set order_id. * * @param int $value Order ID. */ public function set_order_id( $value ) { $this->set_prop( 'order_id', absint( $value ) ); } /** * Set order_key. * * @param string $value Order key. */ public function set_order_key( $value ) { $this->set_prop( 'order_key', $value ); } /** * Set downloads_remaining. * * @param integer|string $value Amount of downloads remaining. */ public function set_downloads_remaining( $value ) { $this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) ); } /** * Set access_granted. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_access_granted( $date = null ) { $this->set_date_prop( 'access_granted', $date ); } /** * Set access_expires. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_access_expires( $date = null ) { $this->set_date_prop( 'access_expires', $date ); } /** * Set download_count. * * @param int $value Download count. */ public function set_download_count( $value ) { $this->set_prop( 'download_count', absint( $value ) ); } /** * Track a download on this permission. * * @since 3.3.0 * @throws Exception When permission ID is invalid. * @param int $user_id Id of the user performing the download. * @param string $user_ip_address IP Address of the user performing the download. */ public function track_download( $user_id = null, $user_ip_address = null ) { global $wpdb; // Must have a permission_id to track download log. if ( ! ( $this->get_id() > 0 ) ) { throw new Exception( __( 'Invalid permission ID.', 'woocommerce' ) ); } // Increment download count, and decrement downloads remaining. // Use SQL to avoid possible issues with downloads in quick succession. // If downloads_remaining is blank, leave it blank (unlimited). // Also, ensure downloads_remaining doesn't drop below zero. $query = $wpdb->prepare( " UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_count = download_count + 1, downloads_remaining = IF( downloads_remaining = '', '', GREATEST( 0, downloads_remaining - 1 ) ) WHERE permission_id = %d", $this->get_id() ); $wpdb->query( $query ); // WPCS: unprepared SQL ok. // Re-read this download from the data store to pull updated counts. $this->data_store->read( $this ); // Track download in download log. $download_log = new WC_Customer_Download_Log(); $download_log->set_timestamp( current_time( 'timestamp', true ) ); $download_log->set_permission_id( $this->get_id() ); if ( ! is_null( $user_id ) ) { $download_log->set_user_id( $user_id ); } if ( ! is_null( $user_ip_address ) ) { $download_log->set_user_ip_address( $user_ip_address ); } $download_log->save(); } /* |-------------------------------------------------------------------------- | ArrayAccess/Backwards compatibility. |-------------------------------------------------------------------------- */ /** * OffsetGet. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { if ( is_callable( array( $this, "get_$offset" ) ) ) { return $this->{"get_$offset"}(); } } /** * OffsetSet. * * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { if ( is_callable( array( $this, "set_$offset" ) ) ) { $this->{"set_$offset"}( $value ); } } /** * OffsetUnset * * @param string $offset Offset. */ public function offsetUnset( $offset ) { if ( is_callable( array( $this, "set_$offset" ) ) ) { $this->{"set_$offset"}( '' ); } } /** * OffsetExists. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { return in_array( $offset, array_keys( $this->data ), true ); } /** * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. * * @param string $key Key name. * @return bool */ public function __isset( $key ) { return in_array( $key, array_keys( $this->data ), true ); } /** * Magic __get method for backwards compatibility. Maps legacy vars to new getters. * * @param string $key Key name. * @return mixed */ public function __get( $key ) { if ( is_callable( array( $this, "get_$key" ) ) ) { return $this->{"get_$key"}( '' ); } } } includes/blocks/class-wc-blocks-utils.php 0000644 00000004112 15132754523 0014472 0 ustar 00 <?php /** * Blocks Utils * * Used by core components that need to work with blocks. * * @package WooCommerce\Blocks\Utils * @version 5.0.0 */ defined( 'ABSPATH' ) || exit; /** * Blocks Utility class. */ class WC_Blocks_Utils { /** * Get blocks from a woocommerce page. * * @param string $woo_page_name A woocommerce page e.g. `checkout` or `cart`. * @return array Array of blocks as returned by parse_blocks(). */ private static function get_all_blocks_from_page( $woo_page_name ) { $page_id = wc_get_page_id( $woo_page_name ); $page = get_post( $page_id ); if ( ! $page ) { return array(); } $blocks = parse_blocks( $page->post_content ); if ( ! $blocks ) { return array(); } return $blocks; } /** * Get all instances of the specified block on a specific woo page * (e.g. `cart` or `checkout` page). * * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. * @param string $woo_page_name The woo page to search, e.g. `cart`. * @return array Array of blocks as returned by parse_blocks(). */ public static function get_blocks_from_page( $block_name, $woo_page_name ) { $page_blocks = self::get_all_blocks_from_page( $woo_page_name ); // Get any instances of the specified block. return array_values( array_filter( $page_blocks, function ( $block ) use ( $block_name ) { return ( $block_name === $block['blockName'] ); } ) ); } /** * Check if a given page contains a particular block. * * @param int|WP_Post $page Page post ID or post object. * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. * @return bool Boolean value if the page contains the block or not. Null in case the page does not exist. */ public static function has_block_in_page( $page, $block_name ) { $page_to_check = get_post( $page ); if ( null === $page_to_check ) { return false; } $blocks = parse_blocks( $page_to_check->post_content ); foreach ( $blocks as $block ) { if ( $block_name === $block['blockName'] ) { return true; } } return false; } } includes/class-wc-breadcrumb.php 0000644 00000022772 15132754523 0012724 0 ustar 00 <?php /** * WC_Breadcrumb class. * * @package WooCommerce\Classes * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Breadcrumb class. */ class WC_Breadcrumb { /** * Breadcrumb trail. * * @var array */ protected $crumbs = array(); /** * Add a crumb so we don't get lost. * * @param string $name Name. * @param string $link Link. */ public function add_crumb( $name, $link = '' ) { $this->crumbs[] = array( wp_strip_all_tags( $name ), $link, ); } /** * Reset crumbs. */ public function reset() { $this->crumbs = array(); } /** * Get the breadcrumb. * * @return array */ public function get_breadcrumb() { return apply_filters( 'woocommerce_get_breadcrumb', $this->crumbs, $this ); } /** * Generate breadcrumb trail. * * @return array of breadcrumbs */ public function generate() { $conditionals = array( 'is_home', 'is_404', 'is_attachment', 'is_single', 'is_product_category', 'is_product_tag', 'is_shop', 'is_page', 'is_post_type_archive', 'is_category', 'is_tag', 'is_author', 'is_date', 'is_tax', ); if ( ( ! is_front_page() && ! ( is_post_type_archive() && intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) ) || is_paged() ) { foreach ( $conditionals as $conditional ) { if ( call_user_func( $conditional ) ) { call_user_func( array( $this, 'add_crumbs_' . substr( $conditional, 3 ) ) ); break; } } $this->search_trail(); $this->paged_trail(); return $this->get_breadcrumb(); } return array(); } /** * Prepend the shop page to shop breadcrumbs. */ protected function prepend_shop_page() { $permalinks = wc_get_permalink_structure(); $shop_page_id = wc_get_page_id( 'shop' ); $shop_page = get_post( $shop_page_id ); // If permalinks contain the shop page in the URI prepend the breadcrumb with shop. if ( $shop_page_id && $shop_page && isset( $permalinks['product_base'] ) && strstr( $permalinks['product_base'], '/' . $shop_page->post_name ) && intval( get_option( 'page_on_front' ) ) !== $shop_page_id ) { $this->add_crumb( get_the_title( $shop_page ), get_permalink( $shop_page ) ); } } /** * Is home trail.. */ protected function add_crumbs_home() { $this->add_crumb( single_post_title( '', false ) ); } /** * 404 trail. */ protected function add_crumbs_404() { $this->add_crumb( __( 'Error 404', 'woocommerce' ) ); } /** * Attachment trail. */ protected function add_crumbs_attachment() { global $post; $this->add_crumbs_single( $post->post_parent, get_permalink( $post->post_parent ) ); $this->add_crumb( get_the_title(), get_permalink() ); } /** * Single post trail. * * @param int $post_id Post ID. * @param string $permalink Post permalink. */ protected function add_crumbs_single( $post_id = 0, $permalink = '' ) { if ( ! $post_id ) { global $post; } else { $post = get_post( $post_id ); // WPCS: override ok. } if ( ! $permalink ) { $permalink = get_permalink( $post ); } if ( 'product' === get_post_type( $post ) ) { $this->prepend_shop_page(); $terms = wc_get_product_terms( $post->ID, 'product_cat', apply_filters( 'woocommerce_breadcrumb_product_terms_args', array( 'orderby' => 'parent', 'order' => 'DESC', ) ) ); if ( $terms ) { $main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms ); $this->term_ancestors( $main_term->term_id, 'product_cat' ); $this->add_crumb( $main_term->name, get_term_link( $main_term ) ); } } elseif ( 'post' !== get_post_type( $post ) ) { $post_type = get_post_type_object( get_post_type( $post ) ); if ( ! empty( $post_type->has_archive ) ) { $this->add_crumb( $post_type->labels->singular_name, get_post_type_archive_link( get_post_type( $post ) ) ); } } else { $cat = current( get_the_category( $post ) ); if ( $cat ) { $this->term_ancestors( $cat->term_id, 'category' ); $this->add_crumb( $cat->name, get_term_link( $cat ) ); } } $this->add_crumb( get_the_title( $post ), $permalink ); } /** * Page trail. */ protected function add_crumbs_page() { global $post; if ( $post->post_parent ) { $parent_crumbs = array(); $parent_id = $post->post_parent; while ( $parent_id ) { $page = get_post( $parent_id ); $parent_id = $page->post_parent; $parent_crumbs[] = array( get_the_title( $page->ID ), get_permalink( $page->ID ) ); } $parent_crumbs = array_reverse( $parent_crumbs ); foreach ( $parent_crumbs as $crumb ) { $this->add_crumb( $crumb[0], $crumb[1] ); } } $this->add_crumb( get_the_title(), get_permalink() ); $this->endpoint_trail(); } /** * Product category trail. */ protected function add_crumbs_product_category() { $current_term = $GLOBALS['wp_query']->get_queried_object(); $this->prepend_shop_page(); $this->term_ancestors( $current_term->term_id, 'product_cat' ); $this->add_crumb( $current_term->name, get_term_link( $current_term, 'product_cat' ) ); } /** * Product tag trail. */ protected function add_crumbs_product_tag() { $current_term = $GLOBALS['wp_query']->get_queried_object(); $this->prepend_shop_page(); /* translators: %s: product tag */ $this->add_crumb( sprintf( __( 'Products tagged “%s”', 'woocommerce' ), $current_term->name ), get_term_link( $current_term, 'product_tag' ) ); } /** * Shop breadcrumb. */ protected function add_crumbs_shop() { if ( intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) { return; } $_name = wc_get_page_id( 'shop' ) ? get_the_title( wc_get_page_id( 'shop' ) ) : ''; if ( ! $_name ) { $product_post_type = get_post_type_object( 'product' ); $_name = $product_post_type->labels->name; } $this->add_crumb( $_name, get_post_type_archive_link( 'product' ) ); } /** * Post type archive trail. */ protected function add_crumbs_post_type_archive() { $post_type = get_post_type_object( get_post_type() ); if ( $post_type ) { $this->add_crumb( $post_type->labels->name, get_post_type_archive_link( get_post_type() ) ); } } /** * Category trail. */ protected function add_crumbs_category() { $this_category = get_category( $GLOBALS['wp_query']->get_queried_object() ); if ( 0 !== intval( $this_category->parent ) ) { $this->term_ancestors( $this_category->term_id, 'category' ); } $this->add_crumb( single_cat_title( '', false ), get_category_link( $this_category->term_id ) ); } /** * Tag trail. */ protected function add_crumbs_tag() { $queried_object = $GLOBALS['wp_query']->get_queried_object(); /* translators: %s: tag name */ $this->add_crumb( sprintf( __( 'Posts tagged “%s”', 'woocommerce' ), single_tag_title( '', false ) ), get_tag_link( $queried_object->term_id ) ); } /** * Add crumbs for date based archives. */ protected function add_crumbs_date() { if ( is_year() || is_month() || is_day() ) { $this->add_crumb( get_the_time( 'Y' ), get_year_link( get_the_time( 'Y' ) ) ); } if ( is_month() || is_day() ) { $this->add_crumb( get_the_time( 'F' ), get_month_link( get_the_time( 'Y' ), get_the_time( 'm' ) ) ); } if ( is_day() ) { $this->add_crumb( get_the_time( 'd' ) ); } } /** * Add crumbs for taxonomies */ protected function add_crumbs_tax() { $this_term = $GLOBALS['wp_query']->get_queried_object(); $taxonomy = get_taxonomy( $this_term->taxonomy ); $this->add_crumb( $taxonomy->labels->name ); if ( 0 !== intval( $this_term->parent ) ) { $this->term_ancestors( $this_term->term_id, $this_term->taxonomy ); } $this->add_crumb( single_term_title( '', false ), get_term_link( $this_term->term_id, $this_term->taxonomy ) ); } /** * Add a breadcrumb for author archives. */ protected function add_crumbs_author() { global $author; $userdata = get_userdata( $author ); /* translators: %s: author name */ $this->add_crumb( sprintf( __( 'Author: %s', 'woocommerce' ), $userdata->display_name ) ); } /** * Add crumbs for a term. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy. */ protected function term_ancestors( $term_id, $taxonomy ) { $ancestors = get_ancestors( $term_id, $taxonomy ); $ancestors = array_reverse( $ancestors ); foreach ( $ancestors as $ancestor ) { $ancestor = get_term( $ancestor, $taxonomy ); if ( ! is_wp_error( $ancestor ) && $ancestor ) { $this->add_crumb( $ancestor->name, get_term_link( $ancestor ) ); } } } /** * Endpoints. */ protected function endpoint_trail() { $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; $endpoint = is_wc_endpoint_url() ? WC()->query->get_current_endpoint() : ''; $endpoint_title = $endpoint ? WC()->query->get_endpoint_title( $endpoint, $action ) : ''; if ( $endpoint_title ) { $this->add_crumb( $endpoint_title ); } } /** * Add a breadcrumb for search results. */ protected function search_trail() { if ( is_search() ) { /* translators: %s: search term */ $this->add_crumb( sprintf( __( 'Search results for “%s”', 'woocommerce' ), get_search_query() ), remove_query_arg( 'paged' ) ); } } /** * Add a breadcrumb for pagination. */ protected function paged_trail() { if ( get_query_var( 'paged' ) && 'subcategories' !== woocommerce_get_loop_display_mode() ) { /* translators: %d: page number */ $this->add_crumb( sprintf( __( 'Page %d', 'woocommerce' ), get_query_var( 'paged' ) ) ); } } } includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php 0000644 00000133621 15132754523 0023353 0 ustar 00 <?php /** * REST API Products controller * * Handles requests to the /products endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Products controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Products_V2_Controller */ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get the images for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * @return array */ protected function get_images( $product ) { $images = array(); $attachment_ids = array(); // Add featured image. if ( $product->get_image_id() ) { $attachment_ids[] = $product->get_image_id(); } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), 'src' => current( $attachment ), 'name' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), ); } return $images; } /** * Make extra product orderby features supported by WooCommerce available to the WC API. * This includes 'price', 'popularity', and 'rating'. * * @param WP_REST_Request $request Request data. * @return array */ protected function prepare_objects_query( $request ) { $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); // Set post_status. $args['post_status'] = $request['status']; // Taxonomy query to filter products by type, category, // tag, shipping class, and attribute. $tax_query = array(); // Map between taxonomy name and arg's key. $taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', 'product_shipping_class' => 'shipping_class', ); // Set tax_query for each passed arg. foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], ); } } // Filter product type by slug. if ( ! empty( $request['type'] ) ) { $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], ); } // Filter by attribute and term. if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $tax_query[] = array( 'taxonomy' => $request['attribute'], 'field' => 'term_id', 'terms' => $request['attribute_term'], ); } } // Build tax_query if taxonomies are set. if ( ! empty( $tax_query ) ) { if ( ! empty( $args['tax_query'] ) ) { $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. } else { $args['tax_query'] = $tax_query; // WPCS: slow query ok. } } // Filter featured. if ( is_bool( $request['featured'] ) ) { $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', ); } // Filter by sku. if ( ! empty( $request['sku'] ) ) { $skus = explode( ',', $request['sku'] ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $request['sku']; } $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_sku', 'value' => $skus, 'compare' => 'IN', ) ); } // Filter by tax class. if ( ! empty( $request['tax_class'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_tax_class', 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', ) ); } // Price filter. if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. } // Filter product by stock_status. if ( ! empty( $request['stock_status'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_stock_status', 'value' => $request['stock_status'], ) ); } // Filter by on sale products. if ( is_bool( $request['on_sale'] ) ) { $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = array( 'product', 'product_variation' ); } else { $args['post_type'] = $this->post_type; } $orderby = $request->get_param( 'orderby' ); $order = $request->get_param( 'order' ); $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); $args['orderby'] = $ordering_args['orderby']; $args['order'] = $ordering_args['order']; if ( $ordering_args['meta_key'] ) { $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. } return $args; } /** * Set product images. * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param array $images Images data. * @return WC_Product */ protected function set_product_images( $product, $images ) { $images = is_array( $images ) ? array_filter( $images ) : array(); if ( ! empty( $images ) ) { $gallery = array(); foreach ( $images as $index => $image ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } else { continue; } } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); } if ( ! wp_attachment_is_image( $attachment_id ) ) { /* translators: %s: image ID */ throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } $featured_image = $product->get_image_id(); if ( 0 === $index ) { $product->set_image_id( $attachment_id ); } else { $gallery[] = $attachment_id; } // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'], ) ); } } $product->set_gallery_image_ids( $gallery ); } else { $product->set_image_id( '' ); $product->set_gallery_image_ids( array() ); } return $product; } /** * Prepare a single product for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; // Type is the most important part here because we need to be using the correct class and methods. if ( isset( $request['type'] ) ) { $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); if ( ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } $product = new $classname( $id ); } elseif ( isset( $request['id'] ) ) { $product = wc_get_product( $id ); } else { $product = new WC_Product_Simple(); } if ( 'variation' === $product->get_type() ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404, ) ); } // Post title. if ( isset( $request['name'] ) ) { $product->set_name( wp_filter_post_kses( $request['name'] ) ); } // Post content. if ( isset( $request['description'] ) ) { $product->set_description( wp_filter_post_kses( $request['description'] ) ); } // Post excerpt. if ( isset( $request['short_description'] ) ) { $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); } // Post status. if ( isset( $request['status'] ) ) { $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); } // Post slug. if ( isset( $request['slug'] ) ) { $product->set_slug( $request['slug'] ); } // Menu order. if ( isset( $request['menu_order'] ) ) { $product->set_menu_order( $request['menu_order'] ); } // Comment status. if ( isset( $request['reviews_allowed'] ) ) { $product->set_reviews_allowed( $request['reviews_allowed'] ); } // Virtual. if ( isset( $request['virtual'] ) ) { $product->set_virtual( $request['virtual'] ); } // Tax status. if ( isset( $request['tax_status'] ) ) { $product->set_tax_status( $request['tax_status'] ); } // Tax Class. if ( isset( $request['tax_class'] ) ) { $product->set_tax_class( $request['tax_class'] ); } // Catalog Visibility. if ( isset( $request['catalog_visibility'] ) ) { $product->set_catalog_visibility( $request['catalog_visibility'] ); } // Purchase Note. if ( isset( $request['purchase_note'] ) ) { $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); } // Featured Product. if ( isset( $request['featured'] ) ) { $product->set_featured( $request['featured'] ); } // Shipping data. $product = $this->save_product_shipping_data( $product, $request ); // SKU. if ( isset( $request['sku'] ) ) { $product->set_sku( wc_clean( $request['sku'] ) ); } // Attributes. if ( isset( $request['attributes'] ) ) { $attributes = array(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = wc_clean( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( $attribute_id ) { if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names. $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Custom attribute - Add attribute to array and set the values. if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; } else { $values = explode( WC_DELIMITER, $attribute['options'] ); } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $request['regular_price'] ) ) { $product->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $product->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_from_gmt'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); } if ( isset( $request['date_on_sale_to'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to'] ); } if ( isset( $request['date_on_sale_to_gmt'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); } } // Product parent ID. if ( isset( $request['parent_id'] ) ) { $product->set_parent_id( $request['parent_id'] ); } // Sold individually. if ( isset( $request['sold_individually'] ) ) { $product->set_sold_individually( $request['sold_individually'] ); } // Stock status; stock_status has priority over in_stock. if ( isset( $request['stock_status'] ) ) { $stock_status = $request['stock_status']; } else { $stock_status = $product->get_stock_status(); } // Stock data. if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $product->set_manage_stock( $request['manage_stock'] ); } // Backorders. if ( isset( $request['backorders'] ) ) { $product->set_backorders( $request['backorders'] ); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( $product->get_manage_stock() ) { // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity. if ( isset( $request['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); } // Low stock amount. // isset() returns false for value null, thus we need to check whether the value has been sent by the request. if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { if ( null === $request['low_stock_amount'] ) { $product->set_low_stock_amount( '' ); } else { $product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); } } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); $product->set_low_stock_amount( '' ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells. if ( isset( $request['upsell_ids'] ) ) { $upsells = array(); $ids = $request['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } } $product->set_upsell_ids( $upsells ); } // Cross sells. if ( isset( $request['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $request['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } } $product->set_cross_sell_ids( $crosssells ); } // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { $new_tags = array(); foreach ( $request['tags'] as $tag ) { if ( ! isset( $tag['name'] ) ) { $new_tags[] = $tag; continue; } if ( ! term_exists( $tag['name'], 'product_tag' ) ) { // Create the tag if it doesn't exist. $term = wp_insert_term( $tag['name'], 'product_tag' ); if ( ! is_wp_error( $term ) ) { $new_tags[] = array( 'id' => $term['term_id'], ); continue; } } else { // Tag exists, assume user wants to set the product with this tag. $new_tags[] = array( 'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id, ); } } $product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' ); } // Downloadable. if ( isset( $request['downloadable'] ) ) { $product->set_downloadable( $request['downloadable'] ); } // Downloadable options. if ( $product->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $product->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $product->set_download_expiry( $request['download_expiry'] ); } } // Product url and button text for external products. if ( $product->is_type( 'external' ) ) { if ( isset( $request['external_url'] ) ) { $product->set_product_url( $request['external_url'] ); } if ( isset( $request['button_text'] ) ) { $product->set_button_text( $request['button_text'] ); } } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $request ); } // Set children for a grouped product. if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { $product->set_children( $request['grouped_products'] ); } // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $product = $this->set_product_images( $product, $request['images'] ); } // Allow set meta_data. if ( is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } if ( ! empty( $request['date_created'] ) ) { $date = rest_parse_date( $request['date_created'] ); if ( $date ) { $product->set_date_created( $date ); } } if ( ! empty( $request['date_created_gmt'] ) ) { $date = rest_parse_date( $request['date_created_gmt'], true ); if ( $date ) { $product->set_date_created( $date ); } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $product Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); } /** * Get the Product's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'slug' => array( 'description' => __( 'Product slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_created_gmt' => array( 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_modified' => array( 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'default' => 'simple', 'enum' => array_keys( wc_get_product_types() ), 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Product status (post status).', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), 'context' => array( 'view', 'edit' ), ), 'featured' => array( 'description' => __( 'Featured product.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'catalog_visibility' => array( 'description' => __( 'Catalog visibility.', 'woocommerce' ), 'type' => 'string', 'default' => 'visible', 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Product description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'short_description' => array( 'description' => __( 'Product short description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Product regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Product sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from_gmt' => array( 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to_gmt' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'price_html' => array( 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'on_sale' => array( 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_sales' => array( 'description' => __( 'Amount of sales.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the product is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the product is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'external_url' => array( 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'button_text' => array( 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'stock_status' => array( 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), 'type' => 'string', 'default' => 'instock', 'enum' => array_keys( wc_get_product_stock_status_options() ), 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'low_stock_amount' => array( 'description' => __( 'Low Stock amount for the product.', 'woocommerce' ), 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), ), 'sold_individually' => array( 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Product dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_taxable' => array( 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reviews_allowed' => array( 'description' => __( 'Allow reviews.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'average_rating' => array( 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rating_count' => array( 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'related_ids' => array( 'description' => __( 'List of related products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'upsell_ids' => array( 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'cross_sell_ids' => array( 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'parent_id' => array( 'description' => __( 'Product parent ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'purchase_note' => array( 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'categories' => array( 'description' => __( 'List of categories.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Category ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Category slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'tags' => array( 'description' => __( 'List of tags.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tag ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Tag slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'images' => array( 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Attribute position.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'visible' => array( 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'variation' => array( 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'options' => array( 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ), ), ), ), 'default_attributes' => array( 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'variations' => array( 'description' => __( 'List of variations IDs.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'integer', ), 'readonly' => true, ), 'grouped_products' => array( 'description' => __( 'List of grouped products ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Add new options for 'orderby' to the collection params. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); unset( $params['in_stock'] ); $params['stock_status'] = array( 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_stock_status_options() ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } /** * Get product data. * * @param WC_Product $product Product instance. * @param string $context Request context. Options: 'view' and 'edit'. * * @return array */ protected function get_product_data( $product, $context = 'view' ) { $data = parent::get_product_data( ...func_get_args() ); // Add stock_status if needed. if ( isset( $this->request ) ) { $fields = $this->get_fields_for_response( $this->request ); if ( in_array( 'stock_status', $fields ) ) { $data['stock_status'] = $product->get_stock_status( $context ); } } return $data; } } includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php 0000644 00000002100 15132754523 0025726 0 ustar 00 <?php /** * REST API Shipping Zone Methods controller * * Handles requests to the /shipping/zones/<id>/methods endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zone Methods class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zone_Methods_V2_Controller */ class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get the settings schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { // Get parent schema to append additional supported settings types for shipping zone method. $schema = parent::get_item_schema(); // Append additional settings supported types (class, order). $schema['properties']['settings']['properties']['type']['enum'][] = 'class'; $schema['properties']['settings']['properties']['type']['enum'][] = 'order'; return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php 0000644 00000000772 15132754523 0024475 0 ustar 00 <?php /** * REST API Network Orders controller * * Handles requests to the /orders/network endpoint * * @package WooCommerce\RestApi * @since 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Network Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Network_Orders_V2_Controller */ class WC_REST_Network_Orders_Controller extends WC_REST_Network_Orders_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php 0000644 00000011343 15132754523 0022415 0 ustar 00 <?php /** * REST API Data controller. * * Handles requests to the /data endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Data controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Data_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'data'; /** * Register routes. * * @since 3.5.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read site data. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to read site settings. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Return the list of data resources. * * @since 3.5.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $data = array(); $resources = array( array( 'slug' => 'continents', 'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ), ), array( 'slug' => 'countries', 'description' => __( 'List of supported states in a given country.', 'woocommerce' ), ), array( 'slug' => 'currencies', 'description' => __( 'List of supported currencies.', 'woocommerce' ), ), ); foreach ( $resources as $resource ) { $item = $this->prepare_item_for_response( (object) $resource, $request ); $data[] = $this->prepare_response_for_collection( $item ); } return rest_ensure_response( $data ); } /** * Prepare a data resource object for serialization. * * @param stdClass $resource Resource data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $resource, $request ) { $data = array( 'slug' => $resource->slug, 'description' => $resource->description, ); $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, 'view' ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $resource ) ); return $response; } /** * Prepare links for the request. * * @param object $item Data object. * @return array Links for the given country. */ protected function prepare_links( $item ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the data index schema, conforming to JSON Schema. * * @since 3.5.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'data_index', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'Data resource ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Data resource description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php 0000644 00000056526 15132754523 0022670 0 ustar 00 <?php /** * Abstract Rest Posts Controller Class * * @class WC_REST_Posts_Controller * @package WooCommerce\RestApi */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_REST_Posts_Controller * * @package WooCommerce\RestApi * @version 2.6.0 */ abstract class WC_REST_Posts_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = ''; /** * Post type. * * @var string */ protected $post_type = ''; /** * Controls visibility on frontend. * * @var string */ protected $public = false; /** * Check if a given request has access to read items. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $post = get_post( (int) $request['id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $post = get_post( (int) $request['id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete an item. * * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { $post = get_post( (int) $request['id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return boolean|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $data ); if ( $this->public ) { $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); } return $response; } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } $post->post_type = $this->post_type; $post_id = wp_insert_post( $post, true ); if ( is_wp_error( $post_id ) ) { if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $post->ID = $post_id; $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); // Add meta fields. $meta_fields = $this->add_post_meta_fields( $post, $request ); if ( is_wp_error( $meta_fields ) ) { // Remove post. $this->delete_post( $post ); return $meta_fields; } /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); return $response; } /** * Add post meta fields. * * @param WP_Post $post Post Object. * @param WP_REST_Request $request WP_REST_Request Object. * @return bool|WP_Error */ protected function add_post_meta_fields( $post, $request ) { return true; } /** * Delete post. * * @param WP_Post $post Post object. */ protected function delete_post( $post ) { wp_delete_post( $post->ID, true ); } /** * Update a single post. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. $post_id = wp_update_post( (array) $post, true ); if ( is_wp_error( $post_id ) ) { if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); // Update meta fields. $meta_fields = $this->update_post_meta_fields( $post, $request ); if ( is_wp_error( $meta_fields ) ) { return $meta_fields; } /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } /** * Get a collection of posts. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $args = array(); $args['offset'] = $request['offset']; $args['order'] = $request['order']; $args['orderby'] = $request['orderby']; $args['paged'] = $request['page']; $args['post__in'] = $request['include']; $args['post__not_in'] = $request['exclude']; $args['posts_per_page'] = $request['per_page']; $args['name'] = $request['slug']; $args['post_parent__in'] = $request['parent']; $args['post_parent__not_in'] = $request['parent_exclude']; $args['s'] = $request['search']; $args['date_query'] = array(); // Set before into date query. Date query must be specified as an array of an array. if ( isset( $request['before'] ) ) { $args['date_query'][0]['before'] = $request['before']; } // Set after into date query. Date query must be specified as an array of an array. if ( isset( $request['after'] ) ) { $args['date_query'][0]['after'] = $request['after']; } if ( 'wc/v1' === $this->namespace ) { if ( is_array( $request['filter'] ) ) { $args = array_merge( $args, $request['filter'] ); unset( $args['filter'] ); } } // Force the post_type argument, since it's not a user input variable. $args['post_type'] = $this->post_type; /** * Filter the query arguments for a request. * * Enables adding extra arguments or setting defaults for a post * collection request. * * @param array $args Key value array of query var to query value. * @param WP_REST_Request $request The request used. */ $args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request ); $query_args = $this->prepare_items_query( $args, $request ); $posts_query = new WP_Query(); $query_result = $posts_query->query( $query_args ); $posts = array(); foreach ( $query_result as $post ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { continue; } $data = $this->prepare_item_for_response( $post, $request ); $posts[] = $this->prepare_response_for_collection( $data ); } $page = (int) $query_args['paged']; $total_posts = $posts_query->found_posts; if ( $total_posts < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $query_args['paged'] ); $count_query = new WP_Query(); $count_query->query( $query_args ); $total_posts = $count_query->found_posts; } $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] ); $response = rest_ensure_response( $posts ); $response->header( 'X-WP-Total', (int) $total_posts ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $request_params = $request->get_query_params(); if ( ! empty( $request_params['filter'] ) ) { // Normalize the pagination params. unset( $request_params['filter']['posts_per_page'] ); unset( $request_params['filter']['paged'] ); } $base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = (bool) $request['force']; $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0; /** * Filter whether an item is trashable. * * Return false to disable trash support for the item. * * @param boolean $supports_trash Whether the item type support trashing. * @param WP_Post $post The Post object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); // If we're forcing, then delete permanently. if ( $force ) { $result = wp_delete_post( $id, true ); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); } // Otherwise, only trash if we haven't already. if ( 'trash' === $post->post_status ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); } // (Note that internally this falls through to `wp_delete_post` if // the trash is disabled.) $result = wp_trash_post( $id ); } if ( ! $result ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } /** * Fires after a single item is deleted or trashed via the REST API. * * @param object $post The deleted or trashed item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); return $response; } /** * Prepare links for the request. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return array Links for the given post. */ protected function prepare_links( $post, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Determine the allowed query_vars for a get_items() response and * prepare for WP_Query. * * @param array $prepared_args Prepared arguments. * @param WP_REST_Request $request Request object. * @return array $query_args */ protected function prepare_items_query( $prepared_args = array(), $request = null ) { $valid_vars = array_flip( $this->get_allowed_query_vars() ); $query_args = array(); foreach ( $valid_vars as $var => $index ) { if ( isset( $prepared_args[ $var ] ) ) { /** * Filter the query_vars used in `get_items` for the constructed query. * * The dynamic portion of the hook name, $var, refers to the query_var key. * * @param mixed $prepared_args[ $var ] The query_var value. */ $query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] ); } } $query_args['ignore_sticky_posts'] = true; if ( 'include' === $query_args['orderby'] ) { $query_args['orderby'] = 'post__in'; } elseif ( 'id' === $query_args['orderby'] ) { $query_args['orderby'] = 'ID'; // ID must be capitalized. } elseif ( 'slug' === $query_args['orderby'] ) { $query_args['orderby'] = 'name'; } return $query_args; } /** * Get all the WP Query vars that are allowed for the API request. * * @return array */ protected function get_allowed_query_vars() { global $wp; /** * Filter the publicly allowed query vars. * * Allows adjusting of the default query vars that are made public. * * @param array Array of allowed WP_Query query vars. */ $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars ); $post_type_obj = get_post_type_object( $this->post_type ); if ( current_user_can( $post_type_obj->cap->edit_posts ) ) { /** * Filter the allowed 'private' query vars for authorized users. * * If the user has the `edit_posts` capability, we also allow use of * private query parameters, which are only undesirable on the * frontend, but are safe for use in query strings. * * To disable anyway, use * `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );` * * @param array $private_query_vars Array of allowed query vars for authorized users. * } */ $private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars ); $valid_vars = array_merge( $valid_vars, $private ); } // Define our own in addition to WP's normal vars. $rest_valid = array( 'date_query', 'ignore_sticky_posts', 'offset', 'post__in', 'post__not_in', 'post_parent', 'post_parent__in', 'post_parent__not_in', 'posts_per_page', 'meta_query', 'tax_query', 'meta_key', 'meta_value', 'meta_compare', 'meta_value_num', ); $valid_vars = array_merge( $valid_vars, $rest_valid ); /** * Filter allowed query vars for the REST API. * * This filter allows you to add or remove query vars from the final allowed * list for all requests, including unauthenticated ones. To alter the * vars for editors only. * * @param array { * Array of allowed WP_Query query vars. * * @param string $allowed_query_var The query var to allow. * } */ $valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars ); return $valid_vars; } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['after'] = array( 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['before'] = array( 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'id', 'include', 'title', 'slug', 'modified', ), 'validate_callback' => 'rest_validate_request_arg', ); $post_type_obj = get_post_type_object( $this->post_type ); if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) { $params['parent'] = array( 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), ); $params['parent_exclude'] = array( 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), ); } if ( 'wc/v1' === $this->namespace ) { $params['filter'] = array( 'type' => 'object', 'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ), ); } return $params; } /** * Update post meta fields. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return bool|WP_Error */ protected function update_post_meta_fields( $post, $request ) { return true; } } includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php 0000644 00000000753 15132754523 0023736 0 ustar 00 <?php /** * REST API Tax Classes controller * * Handles requests to the /taxes/classes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Tax Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Tax_Classes_V2_Controller */ class WC_REST_Tax_Classes_Controller extends WC_REST_Tax_Classes_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php 0000644 00000000757 15132754523 0024127 0 ustar 00 <?php /** * REST API Product Tags controller * * Handles requests to the products/tags endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Tags controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Tags_V2_Controller */ class WC_REST_Product_Tags_Controller extends WC_REST_Product_Tags_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php 0000644 00000001067 15132754523 0026440 0 ustar 00 <?php /** * REST API Product Shipping Classes controller * * Handles requests to the products/shipping_classes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Shipping Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Shipping_Classes_V2_Controller */ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Product_Shipping_Classes_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php 0000644 00000013614 15132754523 0023750 0 ustar 00 <?php /** * REST API Order Notes controller * * Handles requests to the /orders/<order_id>/notes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Order Notes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Order_Notes_V2_Controller */ class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Prepare a single order note output for response. * * @param WP_Comment $note Order note object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => (int) $note->comment_ID, 'author' => __( 'woocommerce', 'woocommerce' ) === $note->comment_author ? 'system' : $note->comment_author, 'date_created' => wc_rest_prepare_date_response( $note->comment_date ), 'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $note ) ); /** * Filter order note object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $note Order note object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); } /** * Create a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } // Create the note. $note_id = $order->add_order_note( $request['note'], $request['customer_note'], $request['added_by_user'] ); if ( ! $note_id ) { return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); } $note = get_comment( $note_id ); $this->update_additional_fields_for_object( $note, $request ); /** * Fires after a order note is created or updated via the REST API. * * @param WP_Comment $note New order note object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $note, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) ); return $response; } /** * Get the Order Notes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'order_note', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'author' => array( 'description' => __( 'Order note author.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'note' => array( 'description' => __( 'Order note content.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_note' => array( 'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'added_by_user' => array( 'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php 0000644 00000000760 15132754523 0024462 0 ustar 00 <?php /** * REST API Shipping Zones controller * * Handles requests to the /shipping/zones endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zones class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zones_V2_Controller */ class WC_REST_Shipping_Zones_Controller extends WC_REST_Shipping_Zones_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php 0000644 00000006356 15132754523 0026174 0 ustar 00 <?php /** * REST API Reports Products Totals controller * * Handles requests to the /reports/products/count endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports Products Totals controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_Controller */ class WC_REST_Report_Products_Totals_Controller extends WC_REST_Reports_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'reports/products/totals'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { $types = wc_get_product_types(); $terms = get_terms( array( 'taxonomy' => 'product_type', 'hide_empty' => false, ) ); $data = array(); foreach ( $terms as $product_type ) { if ( ! isset( $types[ $product_type->name ] ) ) { continue; } $data[] = array( 'slug' => $product_type->name, 'name' => $types[ $product_type->name ], 'total' => (int) $product_type->count, ); } return $data; } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'name' => $report->name, 'total' => $report->total, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_products_count', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_product_total', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product type name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Amount of products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php 0000644 00000007335 15132754523 0024271 0 ustar 00 <?php /** * REST API Order Refunds controller * * Handles requests to the /orders/<order_id>/refunds endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\RestApiUtil; /** * REST API Order Refunds controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Order_Refunds_V2_Controller */ class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Prepares one object for create or update operation. * * @since 3.0.0 * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. */ protected function prepare_object_for_database( $request, $creating = false ) { RestApiUtil::adjust_create_refund_request_parameters( $request ); $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order ) { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } if ( 0 > $request['amount'] ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); } // Create the refund. $refund = wc_create_refund( array( 'order_id' => $order->get_id(), 'amount' => $request['amount'], 'reason' => $request['reason'], 'line_items' => $request['line_items'], 'refund_payment' => $request['api_refund'], 'restock_items' => $request['api_restock'], ) ); if ( is_wp_error( $refund ) ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); } if ( ! $refund ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } $refund->save_meta_data(); } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $coupon Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); } /** * Get the refund schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = parent::get_item_schema(); $schema['properties']['line_items']['items']['properties']['refund_total'] = array( 'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ), 'type' => 'number', 'context' => array( 'edit' ), 'readonly' => true, ); $schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array( 'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ), 'type' => 'number', 'context' => array( 'edit' ), 'readonly' => true, ); $schema['properties']['api_restock'] = array( 'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'edit' ), 'default' => true, ); return $schema; } } includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php 0000644 00000000757 15132754523 0024360 0 ustar 00 <?php /** * REST API WC System Status controller * * Handles requests to the /system_status endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * System status controller class. * * @package WooCommerce\RestApi * @extends WC_REST_System_Status_V2_Controller */ class WC_REST_System_Status_Controller extends WC_REST_System_Status_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php 0000644 00000007254 15132754523 0025377 0 ustar 00 <?php /** * REST API Shipping Zones Controller base * * Houses common functionality between Shipping Zones and Locations. * * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Shipping Zones base class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ abstract class WC_REST_Shipping_Zones_Controller_Base extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'shipping/zones'; /** * Retrieve a Shipping Zone by it's ID. * * @param int $zone_id Shipping Zone ID. * @return WC_Shipping_Zone|WP_Error */ protected function get_zone( $zone_id ) { $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); if ( false === $zone ) { return new WP_Error( 'woocommerce_rest_shipping_zone_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } return $zone; } /** * Check whether a given request has permission to read Shipping Zones. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_shipping_enabled() ) { return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create Shipping Zones. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_shipping_enabled() ) { return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to edit Shipping Zones. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_items_permissions_check( $request ) { if ( ! wc_shipping_enabled() ) { return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to delete Shipping Zones. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_items_permissions_check( $request ) { if ( ! wc_shipping_enabled() ) { return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } } includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php 0000644 00000017437 15132754523 0025015 0 ustar 00 <?php /** * REST API WC Payment gateways controller * * Handles requests to the /payment_gateways endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Paymenga gateways controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Payment_Gateways_V2_Controller */ class WC_REST_Payment_Gateways_Controller extends WC_REST_Payment_Gateways_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Prepare a payment gateway for response. * * @param WC_Payment_Gateway $gateway Payment gateway object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $gateway, $request ) { $order = (array) get_option( 'woocommerce_gateway_order' ); $item = array( 'id' => $gateway->id, 'title' => $gateway->title, 'description' => $gateway->description, 'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '', 'enabled' => ( 'yes' === $gateway->enabled ), 'method_title' => $gateway->get_method_title(), 'method_description' => $gateway->get_method_description(), 'method_supports' => $gateway->supports, 'settings' => $this->get_settings( $gateway ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $gateway, $request ) ); /** * Filter payment gateway objects returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Payment_Gateway $gateway Payment gateway object. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request ); } /** * Return settings associated with this payment gateway. * * @param WC_Payment_Gateway $gateway Gateway instance. * * @return array */ public function get_settings( $gateway ) { $settings = array(); $gateway->init_form_fields(); foreach ( $gateway->form_fields as $id => $field ) { // Make sure we at least have a title and type. if ( empty( $field['title'] ) || empty( $field['type'] ) ) { continue; } // Ignore 'enabled' and 'description' which get included elsewhere. if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { continue; } $data = array( 'id' => $id, 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], 'description' => empty( $field['description'] ) ? '' : $field['description'], 'type' => $field['type'], 'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ], 'default' => empty( $field['default'] ) ? '' : $field['default'], 'tip' => empty( $field['description'] ) ? '' : $field['description'], 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], ); if ( ! empty( $field['options'] ) ) { $data['options'] = $field['options']; } $settings[ $id ] = $data; } return $settings; } /** * Get the payment gateway schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'payment_gateway', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Payment gateway ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'title' => array( 'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'order' => array( 'description' => __( 'Payment gateway sort order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'absint', ), ), 'enabled' => array( 'description' => __( 'Payment gateway enabled status.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), ), 'method_title' => array( 'description' => __( 'Payment gateway method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_description' => array( 'description' => __( 'Payment gateway method description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_supports' => array( 'description' => __( 'Supported features for this payment gateway.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'settings' => array( 'description' => __( 'Payment gateway settings.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Type of setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Setting value.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'default' => array( 'description' => __( 'Default value for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tip' => array( 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'placeholder' => array( 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php 0000644 00000001054 15132754523 0026265 0 ustar 00 <?php /** * REST API Shipping Zone Locations controller * * Handles requests to the /shipping/zones/<id>/locations endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zone Locations class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zone_Locations_V2_Controller */ class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zone_Locations_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php 0000644 00000000752 15132754523 0024126 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/sales endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Report Sales controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Report_Sales_V2_Controller */ class WC_REST_Report_Sales_Controller extends WC_REST_Report_Sales_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php 0000644 00000016522 15132754523 0024656 0 ustar 00 <?php /** * REST API Setting Options controller * * Handles requests to the /settings/$group/$setting endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Setting Options controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Setting_Options_V2_Controller */ class WC_REST_Setting_Options_Controller extends WC_REST_Setting_Options_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get setting data. * * @param string $group_id Group ID. * @param string $setting_id Setting ID. * @return stdClass|WP_Error */ public function get_setting( $group_id, $setting_id ) { $setting = parent::get_setting( $group_id, $setting_id ); if ( is_wp_error( $setting ) ) { return $setting; } $setting['group_id'] = $group_id; return $setting; } /** * Callback for allowed keys for each setting response. * * @param string $key Key to check. * @return boolean */ public function allowed_setting_keys( $key ) { return in_array( $key, array( 'id', 'group_id', 'label', 'description', 'default', 'tip', 'placeholder', 'type', 'options', 'value', 'option_key', ), true ); } /** * Get all settings in a group. * * @param string $group_id Group ID. * @return array|WP_Error */ public function get_group_settings( $group_id ) { if ( empty( $group_id ) ) { return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); } $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores if ( empty( $settings ) ) { return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); } $filtered_settings = array(); foreach ( $settings as $setting ) { $option_key = $setting['option_key']; $setting = $this->filter_setting( $setting ); $default = isset( $setting['default'] ) ? $setting['default'] : ''; // Get the option value. if ( is_array( $option_key ) ) { $option = get_option( $option_key[0] ); $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default; } else { $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default ); $setting['value'] = $admin_setting_value; } if ( 'multi_select_countries' === $setting['type'] ) { $setting['options'] = WC()->countries->get_countries(); $setting['type'] = 'multiselect'; } elseif ( 'single_select_country' === $setting['type'] ) { $setting['type'] = 'select'; $setting['options'] = $this->get_countries_and_states(); } elseif ( 'single_select_page' === $setting['type'] ) { $pages = get_pages( array( 'sort_column' => 'menu_order', 'sort_order' => 'ASC', 'hierarchical' => 0, ) ); $options = array(); foreach ( $pages as $page ) { $options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID; } $setting['type'] = 'select'; $setting['options'] = $options; } $filtered_settings[] = $setting; } return $filtered_settings; } /** * Returns a list of countries and states for use in the base location setting. * * @since 3.0.7 * @return array Array of states and countries. */ private function get_countries_and_states() { $countries = WC()->countries->get_countries(); if ( ! $countries ) { return array(); } $output = array(); foreach ( $countries as $key => $value ) { $states = WC()->countries->get_states( $key ); if ( $states ) { foreach ( $states as $state_key => $state_value ) { $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; } } else { $output[ $key ] = $value; } } return $output; } /** * Get the settings schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'setting', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'group_id' => array( 'description' => __( 'An identifier for the group this setting belongs to.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Setting value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'default' => array( 'description' => __( 'Default value for the setting.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tip' => array( 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'placeholder' => array( 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Type of setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), 'readonly' => true, ), 'options' => array( 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php 0000644 00000062731 15132754523 0022645 0 ustar 00 <?php /** * Abstract Rest Terms Controller * * @package WooCommerce\RestApi * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } use Automattic\WooCommerce\Internal\AssignDefaultCategory; /** * Terms controller class. */ abstract class WC_REST_Terms_Controller extends WC_REST_Controller { /** * Route base. * * @var string */ protected $rest_base = ''; /** * Taxonomy. * * @var string */ protected $taxonomy = ''; /** * Cached taxonomies by attribute id. * * @var array */ protected $taxonomies_by_id = array(); /** * Register the routes for terms. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'type' => 'string', 'description' => __( 'Name for the resource.', 'woocommerce' ), 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check if a given request has access to read the terms. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'read' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create a term. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'create' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a term. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'read' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update a term. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'edit' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete a term. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'delete' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return boolean|WP_Error */ public function batch_items_permissions_check( $request ) { $permissions = $this->check_permissions( $request, 'batch' ); if ( is_wp_error( $permissions ) ) { return $permissions; } if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check permissions. * * @param WP_REST_Request $request Full details about the request. * @param string $context Request context. * @return bool|WP_Error */ protected function check_permissions( $request, $context = 'read' ) { // Get taxonomy. $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } // Check permissions for a single term. $id = intval( $request['id'] ); if ( $id ) { $term = get_term( $id, $taxonomy ); if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) { return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); } return wc_rest_check_product_term_permissions( $taxonomy, $context ); } /** * Get terms associated with a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { $taxonomy = $this->get_taxonomy( $request ); $prepared_args = array( 'exclude' => $request['exclude'], 'include' => $request['include'], 'order' => $request['order'], 'orderby' => $request['orderby'], 'product' => $request['product'], 'hide_empty' => $request['hide_empty'], 'number' => $request['per_page'], 'search' => $request['search'], 'slug' => $request['slug'], ); if ( ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } $taxonomy_obj = get_taxonomy( $taxonomy ); if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { if ( 0 === $request['parent'] ) { // Only query top-level terms. $prepared_args['parent'] = 0; } else { if ( $request['parent'] ) { $prepared_args['parent'] = $request['parent']; } } } /** * Filter the query arguments, before passing them to `get_terms()`. * * Enables adding extra arguments or setting defaults for a terms * collection request. * * @see https://developer.wordpress.org/reference/functions/get_terms/ * * @param array $prepared_args Array of arguments to be * passed to get_terms. * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); if ( ! empty( $prepared_args['product'] ) ) { $query_result = $this->get_terms_for_product( $prepared_args, $request ); $total_terms = $this->total_terms; } else { $query_result = get_terms( $taxonomy, $prepared_args ); $count_args = $prepared_args; unset( $count_args['number'] ); unset( $count_args['offset'] ); $total_terms = wp_count_terms( $taxonomy, $count_args ); // Ensure we don't return results when offset is out of bounds. // See https://core.trac.wordpress.org/ticket/35935. if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) { $query_result = array(); } // wp_count_terms can return a falsy value when the term has no children. if ( ! $total_terms ) { $total_terms = 0; } } $response = array(); foreach ( $query_result as $term ) { $data = $this->prepare_item_for_response( $term, $request ); $response[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $response ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $response->header( 'X-WP-Total', (int) $total_terms ); $max_pages = ceil( $total_terms / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = str_replace( '(?P<attribute_id>[\d]+)', $request['attribute_id'], $this->rest_base ); $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Create a single term for a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); $name = $request['name']; $args = array(); $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $args['description'] = $request['description']; } if ( isset( $request['slug'] ) ) { $args['slug'] = $request['slug']; } if ( isset( $request['parent'] ) ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); } $args['parent'] = $request['parent']; } $term = wp_insert_term( $name, $taxonomy, $args ); if ( is_wp_error( $term ) ) { $error_data = array( 'status' => 400 ); // If we're going to inform the client that the term exists, // give them the identifier they can actually use. $term_id = $term->get_error_data( 'term_exists' ); if ( $term_id ) { $error_data['resource_id'] = $term_id; } return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data ); } $term = get_term( $term['term_id'], $taxonomy ); $this->update_additional_fields_for_object( $term, $request ); // Add term data. $meta_fields = $this->update_term_meta_fields( $term, $request ); if ( is_wp_error( $meta_fields ) ) { wp_delete_term( $term->term_id, $taxonomy ); return $meta_fields; } /** * Fires after a single term is created or updated via the REST API. * * @param WP_Term $term Inserted Term object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating term, false when updating. */ do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $base = '/' . $this->namespace . '/' . $this->rest_base; if ( ! empty( $request['attribute_id'] ) ) { $base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base ); } $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); return $response; } /** * Get a single term from a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function get_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); $term = get_term( (int) $request['id'], $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $response = $this->prepare_item_for_response( $term, $request ); return rest_ensure_response( $response ); } /** * Update a single term from a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); $term = get_term( (int) $request['id'], $taxonomy ); $schema = $this->get_item_schema(); $prepared_args = array(); if ( isset( $request['name'] ) ) { $prepared_args['name'] = $request['name']; } if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $prepared_args['description'] = $request['description']; } if ( isset( $request['slug'] ) ) { $prepared_args['slug'] = $request['slug']; } if ( isset( $request['parent'] ) ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); } $prepared_args['parent'] = $request['parent']; } // Only update the term if we haz something to update. if ( ! empty( $prepared_args ) ) { $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); if ( is_wp_error( $update ) ) { return $update; } } $term = get_term( (int) $request['id'], $taxonomy ); $this->update_additional_fields_for_object( $term, $request ); // Update term data. $meta_fields = $this->update_term_meta_fields( $term, $request ); if ( is_wp_error( $meta_fields ) ) { return $meta_fields; } /** * Fires after a single term is created or updated via the REST API. * * @param WP_Term $term Inserted Term object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating term, false when updating. */ do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); return rest_ensure_response( $response ); } /** * Delete a single term from a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $term = get_term( (int) $request['id'], $taxonomy ); // Get default category id. $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); // Prevent deleting the default product category. if ( $default_category_id === (int) $request['id'] ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); $retval = wp_delete_term( $term->term_id, $term->taxonomy ); if ( ! $retval ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } // Schedule action to assign default category. wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); /** * Fires after a single term is deleted via the REST API. * * @param WP_Term $term The deleted term. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); return $response; } /** * Prepare links for the request. * * @param object $term Term object. * @param WP_REST_Request $request Full details about the request. * @return array Links for the given term. */ protected function prepare_links( $term, $request ) { $base = '/' . $this->namespace . '/' . $this->rest_base; if ( ! empty( $request['attribute_id'] ) ) { $base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base ); } $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); if ( $term->parent ) { $parent_term = get_term( (int) $term->parent, $term->taxonomy ); if ( $parent_term ) { $links['up'] = array( 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), ); } } return $links; } /** * Update term meta fields. * * @param WP_Term $term Term object. * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ protected function update_term_meta_fields( $term, $request ) { return true; } /** * Get the terms attached to a product. * * This is an alternative to `get_terms()` that uses `get_the_terms()` * instead, which hits the object cache. There are a few things not * supported, notably `include`, `exclude`. In `self::get_items()` these * are instead treated as a full query. * * @param array $prepared_args Arguments for `get_terms()`. * @param WP_REST_Request $request Full details about the request. * @return array List of term objects. (Total count in `$this->total_terms`). */ protected function get_terms_for_product( $prepared_args, $request ) { $taxonomy = $this->get_taxonomy( $request ); $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); if ( empty( $query_result ) ) { $this->total_terms = 0; return array(); } // get_items() verifies that we don't have `include` set, and default. // ordering is by `name`. if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) { switch ( $prepared_args['orderby'] ) { case 'id': $this->sort_column = 'term_id'; break; case 'slug': case 'term_group': case 'description': case 'count': $this->sort_column = $prepared_args['orderby']; break; } usort( $query_result, array( $this, 'compare_terms' ) ); } if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { $query_result = array_reverse( $query_result ); } // Pagination. $this->total_terms = count( $query_result ); $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); return $query_result; } /** * Comparison function for sorting terms by a column. * * Uses `$this->sort_column` to determine field to sort by. * * @param stdClass $left Term object. * @param stdClass $right Term object. * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. */ protected function compare_terms( $left, $right ) { $col = $this->sort_column; $left_val = $left->$col; $right_val = $right->$col; if ( is_int( $left_val ) && is_int( $right_val ) ) { return $left_val - $right_val; } return strcmp( $left_val, $right_val ); } /** * Get the query params for collections * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'default' => 'asc', 'enum' => array( 'asc', 'desc', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'default' => 'name', 'enum' => array( 'id', 'include', 'name', 'slug', 'term_group', 'description', 'count', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['hide_empty'] = array( 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'validate_callback' => 'rest_validate_request_arg', ); $params['parent'] = array( 'description' => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['product'] = array( 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'validate_callback' => 'rest_validate_request_arg', ); $params['slug'] = array( 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } /** * Get taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function get_taxonomy( $request ) { $attribute_id = $request['attribute_id']; if ( empty( $attribute_id ) ) { return $this->taxonomy; } if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { return $this->taxonomies_by_id[ $attribute_id ]; } $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] ); if ( ! empty( $taxonomy ) ) { $this->taxonomy = $taxonomy; $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; } return $taxonomy; } } includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php 0000644 00000114607 15132754523 0024655 0 ustar 00 <?php /** * REST API Product Reviews Controller * * Handles requests to /products/reviews. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Reviews Controller Class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'products/reviews'; /** * Register the routes for product reviews. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'product_id' => array( 'required' => true, 'description' => __( 'Unique identifier for the product.', 'woocommerce' ), 'type' => 'integer', ), 'review' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Review content.', 'woocommerce' ), ), 'reviewer' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Name of the reviewer.', 'woocommerce' ), ), 'reviewer_email' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Email of the reviewer.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read webhook deliveries. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $id = (int) $request['id']; $review = get_comment( $id ); if ( $review && ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create a new product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $id = (int) $request['id']; $review = get_comment( $id ); if ( $review && ! wc_rest_check_product_reviews_permissions( 'edit', $review->comment_ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { $id = (int) $request['id']; $review = get_comment( $id ); if ( $review && ! wc_rest_check_product_reviews_permissions( 'delete', $review->comment_ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return boolean|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all reviews. * * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error */ public function get_items( $request ) { // Retrieve the list of registered collection query parameters. $registered = $this->get_collection_params(); /* * This array defines mappings between public API query parameters whose * values are accepted as-passed, and their internal WP_Query parameter * name equivalents (some are the same). Only values which are also * present in $registered will be set. */ $parameter_mappings = array( 'reviewer' => 'author__in', 'reviewer_email' => 'author_email', 'reviewer_exclude' => 'author__not_in', 'exclude' => 'comment__not_in', 'include' => 'comment__in', 'offset' => 'offset', 'order' => 'order', 'per_page' => 'number', 'product' => 'post__in', 'search' => 'search', 'status' => 'status', ); $prepared_args = array(); /* * For each known parameter which is both registered and present in the request, * set the parameter's value on the query $prepared_args. */ foreach ( $parameter_mappings as $api_param => $wp_param ) { if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { $prepared_args[ $wp_param ] = $request[ $api_param ]; } } // Ensure certain parameter values default to empty strings. foreach ( array( 'author_email', 'search' ) as $param ) { if ( ! isset( $prepared_args[ $param ] ) ) { $prepared_args[ $param ] = ''; } } if ( isset( $registered['orderby'] ) ) { $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); } if ( isset( $prepared_args['status'] ) ) { $prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status']; } $prepared_args['no_found_rows'] = false; $prepared_args['date_query'] = array(); // Set before into date query. Date query must be specified as an array of an array. if ( isset( $registered['before'], $request['before'] ) ) { $prepared_args['date_query'][0]['before'] = $request['before']; } // Set after into date query. Date query must be specified as an array of an array. if ( isset( $registered['after'], $request['after'] ) ) { $prepared_args['date_query'][0]['after'] = $request['after']; } if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); } /** * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. * * @since 3.5.0 * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ * @param array $prepared_args Array of arguments for WP_Comment_Query. * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); // Make sure that returns only reviews. $prepared_args['type'] = 'review'; // Query reviews. $query = new WP_Comment_Query(); $query_result = $query->query( $prepared_args ); $reviews = array(); foreach ( $query_result as $review ) { if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { continue; } $data = $this->prepare_item_for_response( $review, $request ); $reviews[] = $this->prepare_response_for_collection( $data ); } $total_reviews = (int) $query->found_comments; $max_pages = (int) $query->max_num_pages; if ( $total_reviews < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); $query = new WP_Comment_Query(); $prepared_args['count'] = true; $total_reviews = $query->query( $prepared_args ); $max_pages = ceil( $total_reviews / $request['per_page'] ); } $response = rest_ensure_response( $reviews ); $response->header( 'X-WP-Total', $total_reviews ); $response->header( 'X-WP-TotalPages', $max_pages ); $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $request['page'] > 1 ) { $prev_page = $request['page'] - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $request['page'] ) { $next_page = $request['page'] + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Create a single review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) ); } $product_id = (int) $request['product_id']; if ( 'product' !== get_post_type( $product_id ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $prepared_review = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_review ) ) { return $prepared_review; } $prepared_review['comment_type'] = 'review'; /* * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission(). */ if ( empty( $prepared_review['comment_content'] ) ) { return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); } // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). if ( ! isset( $prepared_review['comment_date_gmt'] ) ) { $prepared_review['comment_date_gmt'] = current_time( 'mysql', true ); } if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok. $prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok. } else { $prepared_review['comment_author_IP'] = '127.0.0.1'; } if ( ! empty( $request['author_user_agent'] ) ) { $prepared_review['comment_agent'] = $request['author_user_agent']; } elseif ( $request->get_header( 'user_agent' ) ) { $prepared_review['comment_agent'] = $request->get_header( 'user_agent' ); } else { $prepared_review['comment_agent'] = ''; } $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review ); if ( is_wp_error( $check_comment_lengths ) ) { $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); } $prepared_review['comment_parent'] = 0; $prepared_review['comment_author_url'] = ''; $prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true ); if ( is_wp_error( $prepared_review['comment_approved'] ) ) { $error_code = $prepared_review['comment_approved']->get_error_code(); $error_message = $prepared_review['comment_approved']->get_error_message(); if ( 'comment_duplicate' === $error_code ) { return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) ); } if ( 'comment_flood' === $error_code ) { return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) ); } return $prepared_review['comment_approved']; } /** * Filters a review before it is inserted via the REST API. * * Allows modification of the review right before it is inserted via wp_insert_comment(). * Returning a WP_Error value from the filter will shortcircuit insertion and allow * skipping further processing. * * @since 3.5.0 * @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment(). * @param WP_REST_Request $request Request used to insert the review. */ $prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request ); if ( is_wp_error( $prepared_review ) ) { return $prepared_review; } $review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) ); if ( ! $review_id ) { return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); } if ( isset( $request['status'] ) ) { $this->handle_status_param( $request['status'], $review_id ); } update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' ); $review = get_comment( $review_id ); /** * Fires after a comment is created or updated via the REST API. * * @param WP_Comment $review Inserted or updated comment object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a comment, false when updating. */ do_action( 'woocommerce_rest_insert_product_review', $review, $request, true ); $fields_update = $this->update_additional_fields_for_object( $review, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; $request->set_param( 'context', $context ); $response = $this->prepare_item_for_response( $review, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) ); return $response; } /** * Get a single product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $review = $this->get_review( $request['id'] ); if ( is_wp_error( $review ) ) { return $review; } $data = $this->prepare_item_for_response( $review, $request ); $response = rest_ensure_response( $data ); return $response; } /** * Updates a review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. */ public function update_item( $request ) { $review = $this->get_review( $request['id'] ); if ( is_wp_error( $review ) ) { return $review; } $id = (int) $review->comment_ID; if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) { return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) ); } $prepared_args = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_args ) ) { return $prepared_args; } if ( ! empty( $prepared_args['comment_post_ID'] ) ) { if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } } if ( empty( $prepared_args ) && isset( $request['status'] ) ) { // Only the comment status is being changed. $change = $this->handle_status_param( $request['status'], $id ); if ( ! $change ) { return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) ); } } elseif ( ! empty( $prepared_args ) ) { if ( is_wp_error( $prepared_args ) ) { return $prepared_args; } if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); } $prepared_args['comment_ID'] = $id; $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); if ( is_wp_error( $check_comment_lengths ) ) { $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); } $updated = wp_update_comment( wp_slash( (array) $prepared_args ) ); if ( false === $updated ) { return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) ); } if ( isset( $request['status'] ) ) { $this->handle_status_param( $request['status'], $id ); } } if ( ! empty( $request['rating'] ) ) { update_comment_meta( $id, 'rating', $request['rating'] ); } $review = get_comment( $id ); /** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */ do_action( 'woocommerce_rest_insert_product_review', $review, $request, false ); $fields_update = $this->update_additional_fields_for_object( $review, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $review, $request ); return rest_ensure_response( $response ); } /** * Deletes a review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. */ public function delete_item( $request ) { $review = $this->get_review( $request['id'] ); if ( is_wp_error( $review ) ) { return $review; } $force = isset( $request['force'] ) ? (bool) $request['force'] : false; /** * Filters whether a review can be trashed. * * Return false to disable trash support for the post. * * @since 3.5.0 * @param bool $supports_trash Whether the post type support trashing. * @param WP_Comment $review The review object being considered for trashing support. */ $supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review ); $request->set_param( 'context', 'edit' ); if ( $force ) { $previous = $this->prepare_item_for_response( $review, $request ); $result = wp_delete_comment( $review->comment_ID, true ); $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); } else { // If this type doesn't support trashing, error out. if ( ! $supports_trash ) { /* translators: %s: force=true */ return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) ); } if ( 'trash' === $review->comment_approved ) { return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); } $result = wp_trash_comment( $review->comment_ID ); $review = get_comment( $review->comment_ID ); $response = $this->prepare_item_for_response( $review, $request ); } if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a review is deleted via the REST API. * * @param WP_Comment $review The deleted review data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); return $response; } /** * Prepare a single product review output for response. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $review, $request ) { $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $fields = $this->get_fields_for_response( $request ); $data = array(); if ( in_array( 'id', $fields, true ) ) { $data['id'] = (int) $review->comment_ID; } if ( in_array( 'date_created', $fields, true ) ) { $data['date_created'] = wc_rest_prepare_date_response( $review->comment_date ); } if ( in_array( 'date_created_gmt', $fields, true ) ) { $data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt ); } if ( in_array( 'product_id', $fields, true ) ) { $data['product_id'] = (int) $review->comment_post_ID; } if ( in_array( 'status', $fields, true ) ) { $data['status'] = $this->prepare_status_response( (string) $review->comment_approved ); } if ( in_array( 'reviewer', $fields, true ) ) { $data['reviewer'] = $review->comment_author; } if ( in_array( 'reviewer_email', $fields, true ) ) { $data['reviewer_email'] = $review->comment_author_email; } if ( in_array( 'review', $fields, true ) ) { $data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content; } if ( in_array( 'rating', $fields, true ) ) { $data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true ); } if ( in_array( 'verified', $fields, true ) ) { $data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID ); } if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) { $data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email ); } $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $review ) ); /** * Filter product reviews object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); } /** * Prepare a single product review to be inserted into the database. * * @param WP_REST_Request $request Request object. * @return array|WP_Error $prepared_review */ protected function prepare_item_for_database( $request ) { if ( isset( $request['id'] ) ) { $prepared_review['comment_ID'] = (int) $request['id']; } if ( isset( $request['review'] ) ) { $prepared_review['comment_content'] = $request['review']; } if ( isset( $request['product_id'] ) ) { $prepared_review['comment_post_ID'] = (int) $request['product_id']; } if ( isset( $request['reviewer'] ) ) { $prepared_review['comment_author'] = $request['reviewer']; } if ( isset( $request['reviewer_email'] ) ) { $prepared_review['comment_author_email'] = $request['reviewer_email']; } if ( ! empty( $request['date_created'] ) ) { $date_data = rest_get_date_with_gmt( $request['date_created'] ); if ( ! empty( $date_data ) ) { list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; } } elseif ( ! empty( $request['date_created_gmt'] ) ) { $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); if ( ! empty( $date_data ) ) { list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; } } /** * Filters a review after it is prepared for the database. * * Allows modification of the review right after it is prepared for the database. * * @since 3.5.0 * @param array $prepared_review The prepared review data for `wp_insert_comment`. * @param WP_REST_Request $request The current request. */ return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request ); } /** * Prepare links for the request. * * @param WP_Comment $review Product review object. * @return array Links for the given product review. */ protected function prepare_links( $review ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( 0 !== (int) $review->comment_post_ID ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ), ); } if ( 0 !== (int) $review->user_id ) { $links['reviewer'] = array( 'href' => rest_url( 'wp/v2/users/' . $review->user_id ), 'embeddable' => true, ); } return $links; } /** * Get the Product Review's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_review', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Status of the review.', 'woocommerce' ), 'type' => 'string', 'default' => 'approved', 'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ), 'context' => array( 'view', 'edit' ), ), 'reviewer' => array( 'description' => __( 'Reviewer name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'reviewer_email' => array( 'description' => __( 'Reviewer email.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'review' => array( 'description' => __( 'The content of the review.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'rating' => array( 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'verified' => array( 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); if ( get_option( 'show_avatars' ) ) { $avatar_properties = array(); $avatar_sizes = rest_get_avatar_sizes(); foreach ( $avatar_sizes as $size ) { $avatar_properties[ $size ] = array( /* translators: %d: avatar image size in pixels */ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ); } $schema['properties']['reviewer_avatar_urls'] = array( 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => $avatar_properties, ); } return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['after'] = array( 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', ); $params['before'] = array( 'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $params['include'] = array( 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc', ), ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date_gmt', 'enum' => array( 'date', 'date_gmt', 'id', 'include', 'product', ), ); $params['reviewer'] = array( 'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $params['reviewer_exclude'] = array( 'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $params['reviewer_email'] = array( 'default' => null, 'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ), 'format' => 'email', 'type' => 'string', ); $params['product'] = array( 'default' => array(), 'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $params['status'] = array( 'default' => 'approved', 'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'enum' => array( 'all', 'hold', 'approved', 'spam', 'trash', ), ); /** * Filter collection parameters for the reviews controller. * * This filter registers the collection parameter, but does not map the * collection parameter to an internal WP_Comment_Query parameter. Use the * `wc_rest_review_query` filter to set WP_Comment_Query parameters. * * @since 3.5.0 * @param array $params JSON Schema-formatted collection parameters. */ return apply_filters( 'woocommerce_rest_product_review_collection_params', $params ); } /** * Get the reivew, if the ID is valid. * * @since 3.5.0 * @param int $id Supplied ID. * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. */ protected function get_review( $id ) { $id = (int) $id; $error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) ); if ( 0 >= $id ) { return $error; } $review = get_comment( $id ); if ( empty( $review ) ) { return $error; } if ( ! empty( $review->comment_post_ID ) ) { $post = get_post( (int) $review->comment_post_ID ); if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } } return $review; } /** * Prepends internal property prefix to query parameters to match our response fields. * * @since 3.5.0 * @param string $query_param Query parameter. * @return string */ protected function normalize_query_param( $query_param ) { $prefix = 'comment_'; switch ( $query_param ) { case 'id': $normalized = $prefix . 'ID'; break; case 'product': $normalized = $prefix . 'post_ID'; break; case 'include': $normalized = 'comment__in'; break; default: $normalized = $prefix . $query_param; break; } return $normalized; } /** * Checks comment_approved to set comment status for single comment output. * * @since 3.5.0 * @param string|int $comment_approved comment status. * @return string Comment status. */ protected function prepare_status_response( $comment_approved ) { switch ( $comment_approved ) { case 'hold': case '0': $status = 'hold'; break; case 'approve': case '1': $status = 'approved'; break; case 'spam': case 'trash': default: $status = $comment_approved; break; } return $status; } /** * Sets the comment_status of a given review object when creating or updating a review. * * @since 3.5.0 * @param string|int $new_status New review status. * @param int $id Review ID. * @return bool Whether the status was changed. */ protected function handle_status_param( $new_status, $id ) { $old_status = wp_get_comment_status( $id ); if ( $new_status === $old_status ) { return false; } switch ( $new_status ) { case 'approved': case 'approve': case '1': $changed = wp_set_comment_status( $id, 'approve' ); break; case 'hold': case '0': $changed = wp_set_comment_status( $id, 'hold' ); break; case 'spam': $changed = wp_spam_comment( $id ); break; case 'unspam': $changed = wp_unspam_comment( $id ); break; case 'trash': $changed = wp_trash_comment( $id ); break; case 'untrash': $changed = wp_untrash_comment( $id ); break; default: $changed = false; break; } return $changed; } } includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php 0000644 00000001031 15132754523 0025326 0 ustar 00 <?php /** * REST API Customer Downloads controller * * Handles requests to the /customers/<customer_id>/downloads endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Customer_Downloads_V2_Controller */ class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php 0000644 00000014164 15132754523 0024561 0 ustar 00 <?php /** * REST API Data currencies controller. * * Handles requests to the /data/currencies endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Data Currencies controller class. * * @package WooCommerce\RestApi */ class WC_REST_Data_Currencies_Controller extends WC_REST_Data_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'data/currencies'; /** * Register routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/current', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_current_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<currency>[\w-]{3})', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'location' => array( 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), 'type' => 'string', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get currency information. * * @param string $code Currency code. * @param WP_REST_Request $request Request data. * @return array|mixed Response data, ready for insertion into collection data. */ public function get_currency( $code, $request ) { $currencies = get_woocommerce_currencies(); $data = array(); if ( ! array_key_exists( $code, $currencies ) ) { return false; } $currency = array( 'code' => $code, 'name' => $currencies[ $code ], 'symbol' => get_woocommerce_currency_symbol( $code ), ); return $currency; } /** * Return the list of currencies. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $currencies = get_woocommerce_currencies(); foreach ( array_keys( $currencies ) as $code ) { $currency = $this->get_currency( $code, $request ); $response = $this->prepare_item_for_response( $currency, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Return information for a specific currency. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $data = $this->get_currency( strtoupper( $request['currency'] ), $request ); if ( empty( $data ) ) { return new WP_Error( 'woocommerce_rest_data_invalid_currency', __( 'There are no currencies matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); } return $this->prepare_item_for_response( $data, $request ); } /** * Return information for the current site currency. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_current_item( $request ) { $currency = get_option( 'woocommerce_currency' ); return $this->prepare_item_for_response( $this->get_currency( $currency, $request ), $request ); } /** * Prepare the data object for response. * * @param object $item Data object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, 'view' ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item ) ); /** * Filter currency returned from the API. * * @param WP_REST_Response $response The response object. * @param array $item Currency data. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_data_currency', $response, $item, $request ); } /** * Prepare links for the request. * * @param object $item Data object. * @return array Links for the given currency. */ protected function prepare_links( $item ) { $code = strtoupper( $item['code'] ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $code ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the currency schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'data_currencies', 'type' => 'object', 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of currency.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'symbol' => array( 'type' => 'string', 'description' => __( 'Currency symbol.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php 0000644 00000075154 15132754523 0025353 0 ustar 00 <?php /** * REST API variations controller * * Handles requests to the /products/<product_id>/variations endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API variations controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Variations_V2_Controller */ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Prepare a single variation output for response. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $data = array( 'id' => $object->get_id(), 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), 'description' => wc_format_content( $object->get_description() ), 'permalink' => $object->get_permalink(), 'sku' => $object->get_sku(), 'price' => $object->get_price(), 'regular_price' => $object->get_regular_price(), 'sale_price' => $object->get_sale_price(), 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), 'on_sale' => $object->is_on_sale(), 'status' => $object->get_status(), 'purchasable' => $object->is_purchasable(), 'virtual' => $object->is_virtual(), 'downloadable' => $object->is_downloadable(), 'downloads' => $this->get_downloads( $object ), 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, 'tax_status' => $object->get_tax_status(), 'tax_class' => $object->get_tax_class(), 'manage_stock' => $object->managing_stock(), 'stock_quantity' => $object->get_stock_quantity(), 'stock_status' => $object->get_stock_status(), 'backorders' => $object->get_backorders(), 'backorders_allowed' => $object->backorders_allowed(), 'backordered' => $object->is_on_backorder(), 'low_stock_amount' => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(), 'weight' => $object->get_weight(), 'dimensions' => array( 'length' => $object->get_length(), 'width' => $object->get_width(), 'height' => $object->get_height(), ), 'shipping_class' => $object->get_shipping_class(), 'shipping_class_id' => $object->get_shipping_class_id(), 'image' => $this->get_image( $object ), 'attributes' => $this->get_attributes( $object ), 'menu_order' => $object->get_menu_order(), 'meta_data' => $object->get_meta_data(), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare a single variation for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { if ( isset( $request['id'] ) ) { $variation = wc_get_product( absint( $request['id'] ) ); } else { $variation = new WC_Product_Variation(); } $variation->set_parent_id( absint( $request['product_id'] ) ); // Status. if ( isset( $request['status'] ) ) { $variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); } // SKU. if ( isset( $request['sku'] ) ) { $variation->set_sku( wc_clean( $request['sku'] ) ); } // Thumbnail. if ( isset( $request['image'] ) ) { if ( is_array( $request['image'] ) ) { $variation = $this->set_variation_image( $variation, $request['image'] ); } else { $variation->set_image_id( '' ); } } // Virtual variation. if ( isset( $request['virtual'] ) ) { $variation->set_virtual( $request['virtual'] ); } // Downloadable variation. if ( isset( $request['downloadable'] ) ) { $variation->set_downloadable( $request['downloadable'] ); } // Downloads. if ( $variation->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $variation->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $variation->set_download_expiry( $request['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $request ); // Stock handling. if ( isset( $request['manage_stock'] ) ) { $variation->set_manage_stock( $request['manage_stock'] ); } if ( isset( $request['stock_status'] ) ) { $variation->set_stock_status( $request['stock_status'] ); } if ( isset( $request['backorders'] ) ) { $variation->set_backorders( $request['backorders'] ); } if ( $variation->get_manage_stock() ) { if ( isset( $request['stock_quantity'] ) ) { $variation->set_stock_quantity( $request['stock_quantity'] ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $variation->set_stock_quantity( $stock_quantity ); } // isset() returns false for value null, thus we need to check whether the value has been sent by the request. if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { if ( null === $request['low_stock_amount'] ) { $variation->set_low_stock_amount( '' ); } else { $variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); } } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); $variation->set_low_stock_amount( '' ); } // Regular Price. if ( isset( $request['regular_price'] ) ) { $variation->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $variation->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_from_gmt'] ) ) { $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); } if ( isset( $request['date_on_sale_to'] ) ) { $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); } if ( isset( $request['date_on_sale_to_gmt'] ) ) { $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); } // Tax class. if ( isset( $request['tax_class'] ) ) { $variation->set_tax_class( $request['tax_class'] ); } // Description. if ( isset( $request['description'] ) ) { $variation->set_description( wp_kses_post( $request['description'] ) ); } // Update taxonomies. if ( isset( $request['attributes'] ) ) { $attributes = array(); $parent = wc_get_product( $variation->get_parent_id() ); if ( ! $parent ) { return new WP_Error( // Translators: %d parent ID. "woocommerce_rest_{$this->post_type}_invalid_parent", __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), array( 'status' => 404 ) ); } $parent_attributes = $parent->get_attributes(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { continue; } $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $attribute_value = $term->slug; } else { $attribute_value = sanitize_title( $attribute_value ); } } $attributes[ $attribute_key ] = $attribute_value; } $variation->set_attributes( $attributes ); } // Menu order. if ( $request['menu_order'] ) { $variation->set_menu_order( $request['menu_order'] ); } // Meta data. if ( is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $variation Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); } /** * Get the image for a product variation. * * @param WC_Product_Variation $variation Variation data. * @return array */ protected function get_image( $variation ) { if ( ! $variation->get_image_id() ) { return; } $attachment_id = $variation->get_image_id(); $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { return; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { return; } if ( ! isset( $image ) ) { return array( 'id' => (int) $attachment_id, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), 'src' => current( $attachment ), 'name' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), ); } } /** * Set variation image. * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product_Variation $variation Variation instance. * @param array $image Image data. * @return WC_Product_Variation */ protected function set_variation_image( $variation, $image ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id ) { if ( isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); } } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() ); } else { $variation->set_image_id( '' ); return $variation; } } if ( ! wp_attachment_is_image( $attachment_id ) ) { /* translators: %s: attachment ID */ throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } $variation->set_image_id( $attachment_id ); // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'], ) ); } return $variation; } /** * Get the Variation's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Variation description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Variation URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current variation price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Variation regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Variation sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from_gmt' => array( 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to_gmt' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'on_sale' => array( 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'status' => array( 'description' => __( 'Variation status.', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_keys( get_post_statuses() ), 'context' => array( 'view', 'edit' ), ), 'purchasable' => array( 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the variation is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at variation level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'stock_status' => array( 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), 'type' => 'string', 'default' => 'instock', 'enum' => array_keys( wc_get_product_stock_status_options() ), 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'low_stock_amount' => array( 'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ), 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Variation dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'image' => array( 'description' => __( 'Variation image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); // Set post_status. $args['post_status'] = $request['status']; // Filter by sku. if ( ! empty( $request['sku'] ) ) { $skus = explode( ',', $request['sku'] ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $request['sku']; } $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_sku', 'value' => $skus, 'compare' => 'IN', ) ); } // Filter by tax class. if ( ! empty( $request['tax_class'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_tax_class', 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', ) ); } // Price filter. if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. } // Filter product based on stock_status. if ( ! empty( $request['stock_status'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_stock_status', 'value' => $request['stock_status'], ) ); } // Filter by on sale products. if ( is_bool( $request['on_sale'] ) ) { $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = array( 'product', 'product_variation' ); } else { $args['post_type'] = $this->post_type; } $args['post_parent'] = $request['product_id']; return $args; } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); unset( $params['in_stock'], $params['type'], $params['featured'], $params['category'], $params['tag'], $params['shipping_class'], $params['attribute'], $params['attribute_term'] ); $params['stock_status'] = array( 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_stock_status_options() ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version3/class-wc-rest-controller.php 0000644 00000040450 15132754523 0021507 0 ustar 00 <?php /** * REST Controller * * This class extend `WP_REST_Controller` in order to include /batch endpoint * for almost all endpoints in WooCommerce REST API. * * It's required to follow "Controller Classes" guide before extending this class: * <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/> * * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS. * If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`. * * @class WC_REST_Controller * @package WooCommerce\RestApi * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/ */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Abstract Rest Controller Class * * @package WooCommerce\RestApi * @extends WP_REST_Controller * @version 2.6.0 */ abstract class WC_REST_Controller extends WP_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = ''; /** * Used to cache computed return fields. * * @var null|array */ private $_fields = null; /** * Used to verify if cached fields are for correct request object. * * @var null|WP_REST_Request */ private $_request = null; /** * Add the schema from additional fields to an schema array. * * The type of object is inferred from the passed schema. * * @param array $schema Schema array. * * @return array */ protected function add_additional_fields_schema( $schema ) { if ( empty( $schema['title'] ) ) { return $schema; } /** * Can't use $this->get_object_type otherwise we cause an inf loop. */ $object_type = $schema['title']; $additional_fields = $this->get_additional_fields( $object_type ); foreach ( $additional_fields as $field_name => $field_options ) { if ( ! $field_options['schema'] ) { continue; } $schema['properties'][ $field_name ] = $field_options['schema']; } $schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] ); return $schema; } /** * Compatibility functions for WP 5.5, since custom types are not supported anymore. * See @link https://core.trac.wordpress.org/changeset/48306 * * @param string $method Optional. HTTP method of the request. * * @return array Endpoint arguments. */ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { $endpoint_args = parent::get_endpoint_args_for_item_schema( $method ); if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) { return $endpoint_args; } $endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args ); return $endpoint_args; } /** * Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore. * * See @link https://core.trac.wordpress.org/changeset/48306 * * We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema. * * @param array $endpoint_args Schema with datatypes to convert. * @return mixed Schema with converted datatype. */ protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) { if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) { return $endpoint_args; } foreach ( $endpoint_args as $field_id => $params ) { if ( ! isset( $params['type'] ) ) { continue; } /** * Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'. */ if ( 'date-time' === $params['type'] ) { $params['type'] = array( 'null', 'string' ); } /** * WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods. */ if ( 'mixed' === $params['type'] ) { $params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' ); } if ( isset( $params['properties'] ) ) { $params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] ); } if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) { $params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] ); } $endpoint_args[ $field_id ] = $params; } return $endpoint_args; } /** * Get normalized rest base. * * @return string */ protected function get_normalized_rest_base() { return preg_replace( '/\(.*\)\//i', '', $this->rest_base ); } /** * Check batch limit. * * @param array $items Request items. * @return bool|WP_Error */ protected function check_batch_limit( $items ) { $limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() ); $total = 0; if ( ! empty( $items['create'] ) ) { $total += count( $items['create'] ); } if ( ! empty( $items['update'] ) ) { $total += count( $items['update'] ); } if ( ! empty( $items['delete'] ) ) { $total += count( $items['delete'] ); } if ( $total > $limit ) { /* translators: %s: items limit */ return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) ); } return true; } /** * Bulk create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return array Of WP_Error or WP_REST_Response. */ public function batch_items( $request ) { /** * REST Server * * @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; // Get the request params. $items = array_filter( $request->get_params() ); $query = $request->get_query_params(); $response = array(); // Check batch limit. $limit = $this->check_batch_limit( $items ); if ( is_wp_error( $limit ) ) { return $limit; } if ( ! empty( $items['create'] ) ) { foreach ( $items['create'] as $item ) { $_item = new WP_REST_Request( 'POST' ); // Default parameters. $defaults = array(); $schema = $this->get_public_item_schema(); foreach ( $schema['properties'] as $arg => $options ) { if ( isset( $options['default'] ) ) { $defaults[ $arg ] = $options['default']; } } $_item->set_default_params( $defaults ); // Set request parameters. $_item->set_body_params( $item ); // Set query (GET) parameters. $_item->set_query_params( $query ); $_response = $this->create_item( $_item ); if ( is_wp_error( $_response ) ) { $response['create'][] = array( 'id' => 0, 'error' => array( 'code' => $_response->get_error_code(), 'message' => $_response->get_error_message(), 'data' => $_response->get_error_data(), ), ); } else { $response['create'][] = $wp_rest_server->response_to_data( $_response, '' ); } } } if ( ! empty( $items['update'] ) ) { foreach ( $items['update'] as $item ) { $_item = new WP_REST_Request( 'PUT' ); $_item->set_body_params( $item ); $_response = $this->update_item( $_item ); if ( is_wp_error( $_response ) ) { $response['update'][] = array( 'id' => $item['id'], 'error' => array( 'code' => $_response->get_error_code(), 'message' => $_response->get_error_message(), 'data' => $_response->get_error_data(), ), ); } else { $response['update'][] = $wp_rest_server->response_to_data( $_response, '' ); } } } if ( ! empty( $items['delete'] ) ) { foreach ( $items['delete'] as $id ) { $id = (int) $id; if ( 0 === $id ) { continue; } $_item = new WP_REST_Request( 'DELETE' ); $_item->set_query_params( array( 'id' => $id, 'force' => true, ) ); $_response = $this->delete_item( $_item ); if ( is_wp_error( $_response ) ) { $response['delete'][] = array( 'id' => $id, 'error' => array( 'code' => $_response->get_error_code(), 'message' => $_response->get_error_message(), 'data' => $_response->get_error_data(), ), ); } else { $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' ); } } } return $response; } /** * Validate a text value for a text based setting. * * @since 3.0.0 * @param string $value Value. * @param array $setting Setting. * @return string */ public function validate_setting_text_field( $value, $setting ) { $value = is_null( $value ) ? '' : $value; return wp_kses_post( trim( stripslashes( $value ) ) ); } /** * Validate select based settings. * * @since 3.0.0 * @param string $value Value. * @param array $setting Setting. * @return string|WP_Error */ public function validate_setting_select_field( $value, $setting ) { if ( array_key_exists( $value, $setting['options'] ) ) { return $value; } else { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } } /** * Validate multiselect based settings. * * @since 3.0.0 * @param array $values Values. * @param array $setting Setting. * @return array|WP_Error */ public function validate_setting_multiselect_field( $values, $setting ) { if ( empty( $values ) ) { return array(); } if ( ! is_array( $values ) ) { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } $final_values = array(); foreach ( $values as $value ) { if ( array_key_exists( $value, $setting['options'] ) ) { $final_values[] = $value; } } return $final_values; } /** * Validate image_width based settings. * * @since 3.0.0 * @param array $values Values. * @param array $setting Setting. * @return string|WP_Error */ public function validate_setting_image_width_field( $values, $setting ) { if ( ! is_array( $values ) ) { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } $current = $setting['value']; if ( isset( $values['width'] ) ) { $current['width'] = intval( $values['width'] ); } if ( isset( $values['height'] ) ) { $current['height'] = intval( $values['height'] ); } if ( isset( $values['crop'] ) ) { $current['crop'] = (bool) $values['crop']; } return $current; } /** * Validate radio based settings. * * @since 3.0.0 * @param string $value Value. * @param array $setting Setting. * @return string|WP_Error */ public function validate_setting_radio_field( $value, $setting ) { return $this->validate_setting_select_field( $value, $setting ); } /** * Validate checkbox based settings. * * @since 3.0.0 * @param string $value Value. * @param array $setting Setting. * @return string|WP_Error */ public function validate_setting_checkbox_field( $value, $setting ) { if ( in_array( $value, array( 'yes', 'no' ) ) ) { return $value; } elseif ( empty( $value ) ) { $value = isset( $setting['default'] ) ? $setting['default'] : 'no'; return $value; } else { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } } /** * Validate textarea based settings. * * @since 3.0.0 * @param string $value Value. * @param array $setting Setting. * @return string */ public function validate_setting_textarea_field( $value, $setting ) { $value = is_null( $value ) ? '' : $value; return wp_kses( trim( stripslashes( $value ) ), array_merge( array( 'iframe' => array( 'src' => true, 'style' => true, 'id' => true, 'class' => true, ), ), wp_kses_allowed_html( 'post' ) ) ); } /** * Add meta query. * * @since 3.0.0 * @param array $args Query args. * @param array $meta_query Meta query. * @return array */ protected function add_meta_query( $args, $meta_query ) { if ( empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); } $args['meta_query'][] = $meta_query; return $args['meta_query']; } /** * Get the batch schema, conforming to JSON Schema. * * @return array */ public function get_public_batch_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'batch', 'type' => 'object', 'properties' => array( 'create' => array( 'description' => __( 'List of created resources.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', ), ), 'update' => array( 'description' => __( 'List of updated resources.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', ), ), 'delete' => array( 'description' => __( 'List of delete resources.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'integer', ), ), ), ); return $schema; } /** * Gets an array of fields to be included on the response. * * Included fields are based on item schema and `_fields=` request argument. * Updated from WordPress 5.3, included into this class to support old versions. * * @since 3.5.0 * @param WP_REST_Request $request Full details about the request. * @return array Fields to be included in the response. */ public function get_fields_for_response( $request ) { // From xdebug profiling, this method could take upto 25% of request time in index calls. // Cache it and make sure _fields was cached on current request object! // TODO: Submit this caching behavior in core. if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) { return $this->_fields; } $this->_request = $request; $schema = $this->get_item_schema(); $properties = isset( $schema['properties'] ) ? $schema['properties'] : array(); $additional_fields = $this->get_additional_fields(); foreach ( $additional_fields as $field_name => $field_options ) { // For back-compat, include any field with an empty schema // because it won't be present in $this->get_item_schema(). if ( is_null( $field_options['schema'] ) ) { $properties[ $field_name ] = $field_options; } } // Exclude fields that specify a different context than the request context. $context = $request['context']; if ( $context ) { foreach ( $properties as $name => $options ) { if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) { unset( $properties[ $name ] ); } } } $fields = array_keys( $properties ); if ( ! isset( $request['_fields'] ) ) { $this->_fields = $fields; return $fields; } $requested_fields = wp_parse_list( $request['_fields'] ); if ( 0 === count( $requested_fields ) ) { $this->_fields = $fields; return $fields; } // Trim off outside whitespace from the comma delimited list. $requested_fields = array_map( 'trim', $requested_fields ); // Always persist 'id', because it can be needed for add_additional_fields_to_object(). if ( in_array( 'id', $fields, true ) ) { $requested_fields[] = 'id'; } // Return the list of all requested fields which appear in the schema. $this->_fields = array_reduce( $requested_fields, function( $response_fields, $field ) use ( $fields ) { if ( in_array( $field, $fields, true ) ) { $response_fields[] = $field; return $response_fields; } // Check for nested fields if $field is not a direct match. $nested_fields = explode( '.', $field ); // A nested field is included so long as its top-level property // is present in the schema. if ( in_array( $nested_fields[0], $fields, true ) ) { $response_fields[] = $field; } return $response_fields; }, array() ); return $this->_fields; } } includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php 0000644 00000003152 15132754523 0023201 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_V2_Controller */ class WC_REST_Reports_Controller extends WC_REST_Reports_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { $reports = parent::get_reports(); $reports[] = array( 'slug' => 'orders/totals', 'description' => __( 'Orders totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'products/totals', 'description' => __( 'Products totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'customers/totals', 'description' => __( 'Customers totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'coupons/totals', 'description' => __( 'Coupons totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'reviews/totals', 'description' => __( 'Reviews totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'categories/totals', 'description' => __( 'Categories totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'tags/totals', 'description' => __( 'Tags totals.', 'woocommerce' ), ); $reports[] = array( 'slug' => 'attributes/totals', 'description' => __( 'Attributes totals.', 'woocommerce' ), ); return $reports; } } includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php 0000644 00000001172 15132754523 0023324 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Webhooks controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Webhooks_V2_Controller */ class WC_REST_Webhooks_Controller extends WC_REST_Webhooks_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get the default REST API version. * * @since 3.0.0 * @return string */ protected function get_default_api_version() { return 'wp_api_v3'; } } includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php 0000644 00000000721 15132754523 0023170 0 ustar 00 <?php /** * REST API Coupons controller * * Handles requests to the /coupons endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Coupons controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Coupons_V2_Controller */ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php 0000644 00000024746 15132754523 0023543 0 ustar 00 <?php /** * REST API Customers controller * * Handles requests to the /customers endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Customers_V2_Controller */ class WC_REST_Customers_Controller extends WC_REST_Customers_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Get formatted item data. * * @param WC_Data $object WC_Data instance. * * @since 3.0.0 * @return array */ protected function get_formatted_item_data( $object ) { $data = $object->get_data(); $format_date = array( 'date_created', 'date_modified' ); // Format date values. foreach ( $format_date as $key ) { // Date created is stored UTC, date modified is stored WP local time. $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); } return array( 'id' => $object->get_id(), 'date_created' => $data['date_created'], 'date_created_gmt' => $data['date_created_gmt'], 'date_modified' => $data['date_modified'], 'date_modified_gmt' => $data['date_modified_gmt'], 'email' => $data['email'], 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'role' => $data['role'], 'username' => $data['username'], 'billing' => $data['billing'], 'shipping' => $data['shipping'], 'is_paying_customer' => $data['is_paying_customer'], 'avatar_url' => $object->get_avatar_url(), 'meta_data' => $data['meta_data'], ); } /** * Get the Customer's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'email' => array( 'description' => __( 'The email address for the customer.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'first_name' => array( 'description' => __( 'Customer first name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Customer last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'role' => array( 'description' => __( 'Customer role.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'username' => array( 'description' => __( 'Customer login name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_user', ), ), 'password' => array( 'description' => __( 'Customer password.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'billing' => array( 'description' => __( 'List of billing address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'List of shipping address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'is_paying_customer' => array( 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), 'type' => 'bool', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'avatar_url' => array( 'description' => __( 'Avatar URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php 0000644 00000001023 15132754523 0025342 0 ustar 00 <?php /** * REST API Product Attributes controller * * Handles requests to the products/attributes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Attributes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Attributes_V2_Controller */ class WC_REST_Product_Attributes_Controller extends WC_REST_Product_Attributes_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php 0000644 00000007451 15132754523 0026352 0 ustar 00 <?php /** * REST API Reports Customers Totals controller * * Handles requests to the /reports/customers/count endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports Customers Totals controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_Controller */ class WC_REST_Report_Customers_Totals_Controller extends WC_REST_Reports_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'reports/customers/totals'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { $users_count = count_users(); $total_customers = 0; foreach ( $users_count['avail_roles'] as $role => $total ) { if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) { continue; } $total_customers += (int) $total; } $customers_query = new WP_User_Query( array( 'role__not_in' => array( 'administrator', 'shop_manager' ), 'number' => 0, 'fields' => 'ID', 'count_total' => true, 'meta_query' => array( // WPCS: slow query ok. array( 'key' => 'paying_customer', 'value' => 1, 'compare' => '=', ), ), ) ); $total_paying = (int) $customers_query->get_total(); $data = array( array( 'slug' => 'paying', 'name' => __( 'Paying customer', 'woocommerce' ), 'total' => $total_paying, ), array( 'slug' => 'non_paying', 'name' => __( 'Non-paying customer', 'woocommerce' ), 'total' => $total_customers - $total_paying, ), ); return $data; } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'name' => $report->name, 'total' => $report->total, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_customers_count', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_customer_total', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Customer type name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Amount of customers.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php 0000644 00000001101 15132754523 0026304 0 ustar 00 <?php /** * REST API Product Attribute Terms controller * * Handles requests to the products/attributes/<attribute_id>/terms endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Attribute Terms controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Attribute_Terms_V2_Controller */ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php 0000644 00000006172 15132754523 0023350 0 ustar 00 <?php /** * REST API Settings controller * * Handles requests to the /settings endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Settings controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Settings_V2_Controller */ class WC_REST_Settings_Controller extends WC_REST_Settings_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Register routes. */ public function register_routes() { parent::register_routes(); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Makes sure the current user has access to WRITE the settings APIs. * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|bool */ public function update_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Update a setting. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $options_controller = new WC_REST_Setting_Options_Controller(); $response = $options_controller->update_item( $request ); return $response; } /** * Get the groups schema, conforming to JSON Schema. * * @since 3.0.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'setting_group', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'parent_id' => array( 'description' => __( 'ID of parent grouping.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sub_groups' => array( 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php 0000644 00000025527 15132754523 0024610 0 ustar 00 <?php /** * REST API Data continents controller. * * Handles requests to the /data/continents endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Data continents controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Data_Continents_Controller extends WC_REST_Data_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'data/continents'; /** * Register routes. * * @since 3.5.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<location>[\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'continent' => array( 'description' => __( '2 character continent code.', 'woocommerce' ), 'type' => 'string', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Return the list of countries and states for a given continent. * * @since 3.5.0 * @param string $continent_code Continent code. * @param WP_REST_Request $request Request data. * @return array|mixed Response data, ready for insertion into collection data. */ public function get_continent( $continent_code, $request ) { $continents = WC()->countries->get_continents(); $countries = WC()->countries->get_countries(); $states = WC()->countries->get_states(); $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; $data = array(); if ( ! array_key_exists( $continent_code, $continents ) ) { return false; } $continent_list = $continents[ $continent_code ]; $continent = array( 'code' => $continent_code, 'name' => $continent_list['name'], ); $local_countries = array(); foreach ( $continent_list['countries'] as $country_code ) { if ( isset( $countries[ $country_code ] ) ) { $country = array( 'code' => $country_code, 'name' => $countries[ $country_code ], ); // If we have detailed locale information include that in the response. if ( array_key_exists( $country_code, $locale_info ) ) { // Defensive programming against unexpected changes in locale-info.php. $country_data = wp_parse_args( $locale_info[ $country_code ], array( 'currency_code' => 'USD', 'currency_pos' => 'left', 'decimal_sep' => '.', 'dimension_unit' => 'in', 'num_decimals' => 2, 'thousand_sep' => ',', 'weight_unit' => 'lbs', ) ); $country = array_merge( $country, $country_data ); } $local_states = array(); if ( isset( $states[ $country_code ] ) ) { foreach ( $states[ $country_code ] as $state_code => $state_name ) { $local_states[] = array( 'code' => $state_code, 'name' => $state_name, ); } } $country['states'] = $local_states; // Allow only desired keys (e.g. filter out tax rates). $allowed = array( 'code', 'currency_code', 'currency_pos', 'decimal_sep', 'dimension_unit', 'name', 'num_decimals', 'states', 'thousand_sep', 'weight_unit', ); $country = array_intersect_key( $country, array_flip( $allowed ) ); $local_countries[] = $country; } } $continent['countries'] = $local_countries; return $continent; } /** * Return the list of states for all continents. * * @since 3.5.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $continents = WC()->countries->get_continents(); $data = array(); foreach ( array_keys( $continents ) as $continent_code ) { $continent = $this->get_continent( $continent_code, $request ); $response = $this->prepare_item_for_response( $continent, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Return the list of locations for a given continent. * * @since 3.5.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $data = $this->get_continent( strtoupper( $request['location'] ), $request ); if ( empty( $data ) ) { return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); } return $this->prepare_item_for_response( $data, $request ); } /** * Prepare the data object for response. * * @since 3.5.0 * @param object $item Data object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, 'view' ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item ) ); /** * Filter the location list returned from the API. * * Allows modification of the loction data right before it is returned. * * @param WP_REST_Response $response The response object. * @param array $item The original list of continent(s), countries, and states. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); } /** * Prepare links for the request. * * @param object $item Data object. * @return array Links for the given continent. */ protected function prepare_links( $item ) { $continent_code = strtolower( $item['code'] ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the location schema, conforming to JSON Schema. * * @since 3.5.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'data_continents', 'type' => 'object', 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( '2 character continent code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of continent.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'countries' => array( 'type' => 'array', 'description' => __( 'List of countries on this continent.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'currency_code' => array( 'type' => 'string', 'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'currency_pos' => array( 'type' => 'string', 'description' => __( 'Currency symbol position for this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'decimal_sep' => array( 'type' => 'string', 'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'dimension_unit' => array( 'type' => 'string', 'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'num_decimals' => array( 'type' => 'integer', 'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'states' => array( 'type' => 'array', 'description' => __( 'List of states in this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( 'State code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of state.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), 'thousand_sep' => array( 'type' => 'string', 'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'weight_unit' => array( 'type' => 'string', 'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php 0000644 00000007053 15132754523 0022633 0 ustar 00 <?php /** * REST API Taxes controller * * Handles requests to the /taxes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Taxes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Taxes_V2_Controller */ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Add tax rate locales to the response array. * * @param array $data Response data. * @param stdClass $tax Tax object. * * @return array */ protected function add_tax_rate_locales( $data, $tax ) { global $wpdb; $data = parent::add_tax_rate_locales( $data, $tax ); $data['postcodes'] = array(); $data['cities'] = array(); // Get locales from a tax rate. $locales = $wpdb->get_results( $wpdb->prepare( " SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d ", $tax->tax_rate_id ) ); if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { foreach ( $locales as $locale ) { if ( 'postcode' === $locale->location_type ) { $data['postcodes'][] = $locale->location_code; } elseif ( 'city' === $locale->location_type ) { $data['cities'][] = $locale->location_code; } } } return $data; } /** * Get the taxes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = parent::get_item_schema(); $schema['properties']['postcodes'] = array( 'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ); $schema['properties']['cities'] = array( 'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ); $schema['properties']['postcode']['description'] = __( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' ); $schema['properties']['city']['description'] = __( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' ); return $schema; } /** * Create a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response The response, or an error. */ public function create_item( $request ) { $this->adjust_cities_and_postcodes( $request ); return parent::create_item( $request ); } /** * Update a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response The response, or an error. */ public function update_item( $request ) { $this->adjust_cities_and_postcodes( $request ); return parent::update_item( $request ); } /** * Convert array "cities" and "postcodes" parameters * into semicolon-separated strings "city" and "postcode". * * @param WP_REST_Request $request The request to adjust. */ private function adjust_cities_and_postcodes( &$request ) { if ( isset( $request['cities'] ) ) { $request['city'] = join( ';', $request['cities'] ); } if ( isset( $request['postcodes'] ) ) { $request['postcode'] = join( ';', $request['postcodes'] ); } } } includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php 0000644 00000006160 15132754523 0025620 0 ustar 00 <?php /** * REST API Reports Orders Totals controller * * Handles requests to the /reports/orders/count endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports Orders Totals controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_Controller */ class WC_REST_Report_Orders_Totals_Controller extends WC_REST_Reports_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'reports/orders/totals'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { $totals = wp_count_posts( 'shop_order' ); $data = array(); foreach ( wc_get_order_statuses() as $slug => $name ) { if ( ! isset( $totals->$slug ) ) { continue; } $data[] = array( 'slug' => str_replace( 'wc-', '', $slug ), 'name' => $name, 'total' => (int) $totals->$slug, ); } return $data; } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'name' => $report->name, 'total' => $report->total, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_orders_count', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_order_total', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Order status name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Amount of orders.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php 0000644 00000021612 15132754523 0025307 0 ustar 00 <?php /** * REST API Product Categories controller * * Handles requests to the products/categories endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Categories controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Categories_V2_Controller */ class WC_REST_Product_Categories_Controller extends WC_REST_Product_Categories_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Prepare a single product category output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request Request instance. * @return WP_REST_Response */ public function prepare_item_for_response( $item, $request ) { // Get category display type. $display_type = get_term_meta( $item->term_id, 'display_type', true ); // Get category order. $menu_order = get_term_meta( $item->term_id, 'order', true ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'parent' => (int) $item->parent, 'description' => $item->description, 'display' => $display_type ? $display_type : 'default', 'image' => null, 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); // Get category image. $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); if ( $image_id ) { $attachment = get_post( $image_id ); $data['image'] = array( 'id' => (int) $image_id, 'date_created' => wc_rest_prepare_date_response( $attachment->post_date ), 'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ), 'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), 'src' => wp_get_attachment_url( $image_id ), 'name' => get_the_title( $attachment ), 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Category schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'parent' => array( 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'display' => array( 'description' => __( 'Category archive display type.', 'woocommerce' ), 'type' => 'string', 'default' => 'default', 'enum' => array( 'default', 'products', 'subcategories', 'both' ), 'context' => array( 'view', 'edit' ), ), 'image' => array( 'description' => __( 'Image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Update term meta fields. * * @param WP_Term $term Term object. * @param WP_REST_Request $request Request instance. * @return bool|WP_Error * * @since 3.5.5 */ protected function update_term_meta_fields( $term, $request ) { $id = (int) $term->term_id; if ( isset( $request['display'] ) ) { update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); } if ( isset( $request['menu_order'] ) ) { update_term_meta( $id, 'order', $request['menu_order'] ); } if ( isset( $request['image'] ) ) { if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); if ( is_wp_error( $upload ) ) { return $upload; } $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); } else { $image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0; } // Check if image_id is a valid image attachment before updating the term meta. if ( $image_id && wp_attachment_is_image( $image_id ) ) { update_term_meta( $id, 'thumbnail_id', $image_id ); // Set the image alt. if ( ! empty( $request['image']['alt'] ) ) { update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); } // Set the image title. if ( ! empty( $request['image']['name'] ) ) { wp_update_post( array( 'ID' => $image_id, 'post_title' => wc_clean( $request['image']['name'] ), ) ); } } else { delete_term_meta( $id, 'thumbnail_id' ); } } return true; } } includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php 0000644 00000006470 15132754523 0026012 0 ustar 00 <?php /** * REST API Reports Reviews Totals controller * * Handles requests to the /reports/reviews/count endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports Reviews Totals controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_Controller */ class WC_REST_Report_Reviews_Totals_Controller extends WC_REST_Reports_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'reports/reviews/totals'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { $data = array(); $query_data = array( 'count' => true, 'post_type' => 'product', 'meta_key' => 'rating', // WPCS: slow query ok. 'meta_value' => '', // WPCS: slow query ok. ); for ( $i = 1; $i <= 5; $i++ ) { $query_data['meta_value'] = $i; $data[] = array( 'slug' => 'rated_' . $i . '_out_of_5', /* translators: %s: average rating */ 'name' => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ), 'total' => (int) get_comments( $query_data ), ); } return $data; } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'name' => $report->name, 'total' => $report->total, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_reviews_count', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_review_total', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Review type name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Amount of reviews.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php 0000644 00000022015 15132754523 0023000 0 ustar 00 <?php /** * REST API Orders controller * * Handles requests to the /orders endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Orders_V2_Controller */ class WC_REST_Orders_Controller extends WC_REST_Orders_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Calculate coupons. * * @throws WC_REST_Exception When fails to set any item. * @param WP_REST_Request $request Request object. * @param WC_Order $order Order data. * @return bool */ protected function calculate_coupons( $request, $order ) { if ( ! isset( $request['coupon_lines'] ) ) { return false; } // Validate input and at the same time store the processed coupon codes to apply. $coupon_codes = array(); $discounts = new WC_Discounts( $order ); $current_order_coupons = array_values( $order->get_coupons() ); $current_order_coupon_codes = array_map( function( $coupon ) { return $coupon->get_code(); }, $current_order_coupons ); foreach ( $request['coupon_lines'] as $item ) { if ( ! empty( $item['id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 ); } if ( empty( $item['code'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } $coupon_code = wc_format_coupon_code( wc_clean( $item['code'] ) ); $coupon = new WC_Coupon( $coupon_code ); // Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons. if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) { $check_result = $discounts->is_coupon_valid( $coupon ); if ( is_wp_error( $check_result ) ) { throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 ); } } $coupon_codes[] = $coupon_code; } // Remove all coupons first to ensure calculation is correct. foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) { $order->remove_coupon( $existing_coupon->get_code() ); } // Apply the coupons. foreach ( $coupon_codes as $new_coupon ) { $results = $order->apply_coupon( $new_coupon ); if ( is_wp_error( $results ) ) { throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 ); } } return true; } /** * Prepare a single order for create or update. * * @throws WC_REST_Exception When fails to set any item. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $order = new WC_Order( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'coupon_lines': case 'status': // Change should be done later so transitions have new data. break; case 'billing': case 'shipping': $this->update_address( $order, $value, $key ); break; case 'line_items': case 'shipping_lines': case 'fee_lines': if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( is_array( $item ) ) { if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { $order->remove_item( $item['id'] ); } else { $this->set_item( $order, $key, $item ); } } } } break; case 'meta_data': if ( is_array( $value ) ) { foreach ( $value as $meta ) { $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; default: if ( is_callable( array( $order, "set_{$key}" ) ) ) { $order->{"set_{$key}"}( $value ); } break; } } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $order Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); } /** * Save an object data. * * @since 3.0.0 * @throws WC_REST_Exception But all errors are validated before returning any data. * @param WP_REST_Request $request Full details about the request. * @param bool $creating If is creating a new object. * @return WC_Data|WP_Error */ protected function save_object( $request, $creating = false ) { try { $object = $this->prepare_object_for_database( $request, $creating ); if ( is_wp_error( $object ) ) { return $object; } // Make sure gateways are loaded so hooks from gateways fire on save/create. WC()->payment_gateways(); if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { // Make sure customer exists. if ( false === get_user_by( 'id', $request['customer_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } // Make sure customer is part of blog. if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); } } if ( $creating ) { $object->set_created_via( 'rest-api' ); $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $object->calculate_totals(); } else { // If items have changed, recalculate order totals. if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { $object->calculate_totals( true ); } } // Set coupons. $this->calculate_coupons( $request, $object ); // Set status. if ( ! empty( $request['status'] ) ) { $object->set_status( $request['status'] ); } $object->save(); // Actions for after the order is saved. if ( true === $request['set_paid'] ) { if ( $creating || $object->needs_payment() ) { $object->payment_complete( $request['transaction_id'] ); } } return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { // This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query. $statuses = $request['status']; unset( $request['status'] ); $args = parent::prepare_objects_query( $request ); $args['post_status'] = array(); foreach ( $statuses as $status ) { if ( in_array( $status, $this->get_order_statuses(), true ) ) { $args['post_status'][] = 'wc-' . $status; } elseif ( 'any' === $status ) { // Set status to "any" and short-circuit out. $args['post_status'] = 'any'; break; } else { $args['post_status'][] = $status; } } // Put the statuses back for further processing (next/prev links, etc). $request['status'] = $statuses; return $args; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = parent::get_item_schema(); $schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true; return $schema; } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), ), 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php 0000644 00000056664 15132754523 0022460 0 ustar 00 <?php /** * Abstract Rest CRUD Controller Class * * @class WC_REST_CRUD_Controller * @package WooCommerce\RestApi * @version 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_REST_CRUD_Controller class. * * @extends WC_REST_Posts_Controller */ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * If object is hierarchical. * * @var bool */ protected $hierarchical = false; /** * Get object. * * @param int $id Object ID. * @return object WC_Data object or WP_Error object. */ protected function get_object( $id ) { // translators: %s: Class method name. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); } /** * Check if a given request has access to read an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete an item. * * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get object permalink. * * @param object $object Object. * @return string */ protected function get_permalink( $object ) { return ''; } /** * Prepares the object for the REST response. * * @since 3.0.0 * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. */ protected function prepare_object_for_response( $object, $request ) { // translators: %s: Class method name. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); } /** * Prepares one object for create or update operation. * * @since 3.0.0 * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. */ protected function prepare_object_for_database( $request, $creating = false ) { // translators: %s: Class method name. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); } /** * Get a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = $this->prepare_object_for_response( $object, $request ); $response = rest_ensure_response( $data ); if ( $this->public ) { $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); } return $response; } /** * Save an object data. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @param bool $creating If is creating a new object. * @return WC_Data|WP_Error */ protected function save_object( $request, $creating = false ) { try { $object = $this->prepare_object_for_database( $request, $creating ); if ( is_wp_error( $object ) ) { return $object; } $object->save(); return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $object = $this->save_object( $request, true ); if ( is_wp_error( $object ) ) { return $object; } try { $this->update_additional_fields_for_object( $object, $request ); /** * Fires after a single object is created or updated via the REST API. * * @param WC_Data $object Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating object, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); } catch ( WC_Data_Exception $e ) { $object->delete(); return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { $object->delete(); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_object_for_response( $object, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) ); return $response; } /** * Update a single post. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); } $object = $this->save_object( $request, false ); if ( is_wp_error( $object ) ) { return $object; } try { $this->update_additional_fields_for_object( $object, $request ); /** * Fires after a single object is created or updated via the REST API. * * @param WC_Data $object Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating object, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_object_for_response( $object, $request ); return rest_ensure_response( $response ); } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { $args = array(); $args['offset'] = $request['offset']; $args['order'] = $request['order']; $args['orderby'] = $request['orderby']; $args['paged'] = $request['page']; $args['post__in'] = $request['include']; $args['post__not_in'] = $request['exclude']; $args['posts_per_page'] = $request['per_page']; $args['name'] = $request['slug']; $args['post_parent__in'] = $request['parent']; $args['post_parent__not_in'] = $request['parent_exclude']; $args['s'] = $request['search']; $args['fields'] = $this->get_fields_for_response( $request ); if ( 'date' === $args['orderby'] ) { $args['orderby'] = 'date ID'; } $date_query = array(); $use_gmt = $request['dates_are_gmt']; if ( isset( $request['before'] ) ) { $date_query[] = array( 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', 'before' => $request['before'], ); } if ( isset( $request['after'] ) ) { $date_query[] = array( 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', 'after' => $request['after'], ); } if ( isset( $request['modified_before'] ) ) { $date_query[] = array( 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', 'before' => $request['modified_before'], ); } if ( isset( $request['modified_after'] ) ) { $date_query[] = array( 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', 'after' => $request['modified_after'], ); } if ( ! empty( $date_query ) ) { $date_query['relation'] = 'AND'; $args['date_query'] = $date_query; } // Force the post_type argument, since it's not a user input variable. $args['post_type'] = $this->post_type; /** * Filter the query arguments for a request. * * Enables adding extra arguments or setting defaults for a post * collection request. * * @param array $args Key value array of query var to query value. * @param WP_REST_Request $request The request used. */ $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); return $this->prepare_items_query( $args, $request ); } /** * Get objects. * * @since 3.0.0 * @param array $query_args Query args. * @return array */ protected function get_objects( $query_args ) { $query = new WP_Query(); $result = $query->query( $query_args ); $total_posts = $query->found_posts; if ( $total_posts < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $query_args['paged'] ); $count_query = new WP_Query(); $count_query->query( $query_args ); $total_posts = $count_query->found_posts; } return array( 'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ), 'total' => (int) $total_posts, 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), ); } /** * Get a collection of posts. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $query_args = $this->prepare_objects_query( $request ); if ( is_wp_error( current( $query_args ) ) ) { return current( $query_args ); } $query_results = $this->get_objects( $query_args ); $objects = array(); foreach ( $query_results['objects'] as $object ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { continue; } $data = $this->prepare_object_for_response( $object, $request ); $objects[] = $this->prepare_response_for_collection( $data ); } $page = (int) $query_args['paged']; $max_pages = $query_results['pages']; $response = rest_ensure_response( $objects ); $response->header( 'X-WP-Total', $query_results['total'] ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = $this->rest_base; $attrib_prefix = '(?P<'; if ( strpos( $base, $attrib_prefix ) !== false ) { $attrib_names = array(); preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE ); foreach ( $attrib_names as $attrib_name_match ) { $beginning_offset = strlen( $attrib_prefix ); $attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] ); $attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset ); if ( isset( $request[ $attrib_name ] ) ) { $base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base ); } } } $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $force = (bool) $request['force']; $object = $this->get_object( (int) $request['id'] ); $result = false; if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); /** * Filter whether an object is trashable. * * Return false to disable trash support for the object. * * @param boolean $supports_trash Whether the object type support trashing. * @param WC_Data $object The object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_object_for_response( $object, $request ); // If we're forcing, then delete permanently. if ( $force ) { $object->delete( true ); $result = 0 === $object->get_id(); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); } // Otherwise, only trash if we haven't already. if ( is_callable( array( $object, 'get_status' ) ) ) { if ( 'trash' === $object->get_status() ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); } $object->delete(); $result = 'trash' === $object->get_status(); } } if ( ! $result ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } /** * Fires after a single object is deleted or trashed via the REST API. * * @param WC_Data $object The deleted or trashed object. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); return $response; } /** * Get fields for an object if getter is defined. * * @param object $object Object we are fetching response for. * @param string $context Context of the request. Can be `view` or `edit`. * @param array $fields List of fields to fetch. * @return array Data fetched from getters. */ public function fetch_fields_using_getters( $object, $context, $fields ) { $data = array(); foreach ( $fields as $field ) { if ( method_exists( $this, "api_get_$field" ) ) { $data[ $field ] = $this->{"api_get_$field"}( $object, $context ); } } return $data; } /** * Prepare links for the request. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return array Links for the given post. */ protected function prepare_links( $object, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; $params['page'] = array( 'description' => __( 'Current page of the collection.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['search'] = array( 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['after'] = array( 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['before'] = array( 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['modified_after'] = array( 'description' => __( 'Limit response to resources modified after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['modified_before'] = array( 'description' => __( 'Limit response to resources modified before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['dates_are_gmt'] = array( 'description' => __( 'Whether to consider GMT post dates when limiting response by published or modified date.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'validate_callback' => 'rest_validate_request_arg', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'id', 'include', 'title', 'slug', 'modified', ), 'validate_callback' => 'rest_validate_request_arg', ); if ( $this->hierarchical ) { $params['parent'] = array( 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), ); $params['parent_exclude'] = array( 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), ); } /** * Filter collection parameters for the posts controller. * * The dynamic part of the filter `$this->post_type` refers to the post * type slug for the controller. * * This filter registers the collection parameter, but does not map the * collection parameter to an internal WP_Query parameter. Use the * `rest_{$this->post_type}_query` filter to set WP_Query parameters. * * @param array $query_params JSON Schema-formatted collection parameters. * @param WP_Post_Type $post_type Post type object. */ return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type ); } } includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php 0000644 00000001001 15132754523 0024754 0 ustar 00 <?php /** * REST API WC Shipping Methods controller * * Handles requests to the /shipping_methods endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Shipping methods controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Methods_V2_Controller */ class WC_REST_Shipping_Methods_Controller extends WC_REST_Shipping_Methods_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php 0000644 00000006715 15132754523 0026016 0 ustar 00 <?php /** * REST API Reports Coupons Totals controller * * Handles requests to the /reports/coupons/count endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports Coupons Totals controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_Controller */ class WC_REST_Report_Coupons_Totals_Controller extends WC_REST_Reports_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'reports/coupons/totals'; /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { global $wpdb; $data = get_transient( 'rest_api_coupons_type_count' ); if ( false !== $data ) { return $data; } $types = wc_get_coupon_types(); $data = array(); foreach ( $types as $slug => $name ) { $results = $wpdb->get_results( $wpdb->prepare( " SELECT count(meta_id) AS total FROM $wpdb->postmeta WHERE meta_key = 'discount_type' AND meta_value = %s ", $slug ) ); $total = isset( $results[0] ) ? (int) $results[0]->total : 0; $data[] = array( 'slug' => $slug, 'name' => $name, 'total' => $total, ); } set_transient( 'rest_api_coupons_type_count', $data, YEAR_IN_SECONDS ); return $data; } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'name' => $report->name, 'total' => $report->total, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_coupons_count', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_coupon_total', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Coupon type name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Amount of coupons.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php 0000644 00000001020 15132754523 0025476 0 ustar 00 <?php /** * REST API WC System Status Tools Controller * * Handles requests to the /system_status/tools/* endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * System status tools controller. * * @package WooCommerce\RestApi * @extends WC_REST_System_Status_Tools_V2_Controller */ class WC_REST_System_Status_Tools_Controller extends WC_REST_System_Status_Tools_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php 0000644 00000015157 15132754523 0024435 0 ustar 00 <?php /** * REST API Data countries controller. * * Handles requests to the /data/countries endpoint. * * @package WooCommerce\RestApi * @since 3.5.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Data countries controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Data_Countries_Controller extends WC_REST_Data_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Route base. * * @var string */ protected $rest_base = 'data/countries'; /** * Register routes. * * @since 3.5.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<location>[\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'location' => array( 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), 'type' => 'string', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get a list of countries and states. * * @param string $country_code Country code. * @param WP_REST_Request $request Request data. * @return array|mixed Response data, ready for insertion into collection data. */ public function get_country( $country_code, $request ) { $countries = WC()->countries->get_countries(); $states = WC()->countries->get_states(); $data = array(); if ( ! array_key_exists( $country_code, $countries ) ) { return false; } $country = array( 'code' => $country_code, 'name' => $countries[ $country_code ], ); $local_states = array(); if ( isset( $states[ $country_code ] ) ) { foreach ( $states[ $country_code ] as $state_code => $state_name ) { $local_states[] = array( 'code' => $state_code, 'name' => $state_name, ); } } $country['states'] = $local_states; return $country; } /** * Return the list of states for all countries. * * @since 3.5.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $countries = WC()->countries->get_countries(); $data = array(); foreach ( array_keys( $countries ) as $country_code ) { $country = $this->get_country( $country_code, $request ); $response = $this->prepare_item_for_response( $country, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Return the list of states for a given country. * * @since 3.5.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $data = $this->get_country( strtoupper( $request['location'] ), $request ); if ( empty( $data ) ) { return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); } return $this->prepare_item_for_response( $data, $request ); } /** * Prepare the data object for response. * * @since 3.5.0 * @param object $item Data object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, 'view' ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item ) ); /** * Filter the states list for a country returned from the API. * * Allows modification of the loction data right before it is returned. * * @param WP_REST_Response $response The response object. * @param array $data The original country's states list. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); } /** * Prepare links for the request. * * @param object $item Data object. * @return array Links for the given country. */ protected function prepare_links( $item ) { $country_code = strtolower( $item['code'] ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the location schema, conforming to JSON Schema. * * @since 3.5.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'data_countries', 'type' => 'object', 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'states' => array( 'type' => 'array', 'description' => __( 'List of states in this country.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'code' => array( 'type' => 'string', 'description' => __( 'State code.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Full name of state.', 'woocommerce' ), 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php 0000644 00000001010 15132754523 0025254 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/top_sellers endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Report Top Sellers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Report_Top_Sellers_V2_Controller */ class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Top_Sellers_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; } includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php 0000644 00000001072 15132754523 0026760 0 ustar 00 <?php /** * REST API Product Shipping Classes controller * * Handles requests to the products/shipping_classes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Shipping Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Shipping_Classes_V1_Controller */ class WC_REST_Product_Shipping_Classes_V2_Controller extends WC_REST_Product_Shipping_Classes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php 0000644 00000012222 15132754523 0025012 0 ustar 00 <?php /** * REST API Network Orders controller * * Handles requests to the /orders/network endpoint * * @package WooCommerce\RestApi * @since 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Network Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Orders_V2_Controller */ class WC_REST_Network_Orders_V2_Controller extends WC_REST_Orders_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Register the routes for network orders. */ public function register_routes() { if ( is_multisite() ) { register_rest_route( $this->namespace, '/' . $this->rest_base . '/network', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'network_orders' ), 'permission_callback' => array( $this, 'network_orders_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } } /** * Retrieves the item's schema for display / public consumption purposes. * * @return array Public item schema data. */ public function get_public_item_schema() { $schema = parent::get_public_item_schema(); $schema['properties']['blog'] = array( 'description' => __( 'Blog id of the record on the multisite.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ); $schema['properties']['edit_url'] = array( 'description' => __( 'URL to edit the order', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ); $schema['properties']['customer'][] = array( 'description' => __( 'Name of the customer for the order', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ); $schema['properties']['status_name'][] = array( 'description' => __( 'Order Status', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ); $schema['properties']['formatted_total'][] = array( 'description' => __( 'Order total formatted for locale', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ); return $schema; } /** * Does a permissions check for the proper requested blog * * @param WP_REST_Request $request Full details about the request. * * @return bool $permission */ public function network_orders_permissions_check( $request ) { $blog_id = $request->get_param( 'blog_id' ); $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); switch_to_blog( $blog_id ); $permission = $this->get_items_permissions_check( $request ); restore_current_blog(); return $permission; } /** * Get a collection of orders from the requested blog id * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response */ public function network_orders( $request ) { $blog_id = $request->get_param( 'blog_id' ); $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); $active_plugins = get_blog_option( $blog_id, 'active_plugins', array() ); $network_active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); $plugins = array_merge( $active_plugins, $network_active_plugins ); $wc_active = false; foreach ( $plugins as $plugin ) { if ( substr_compare( $plugin, '/woocommerce.php', strlen( $plugin ) - strlen( '/woocommerce.php' ), strlen( '/woocommerce.php' ) ) === 0 ) { $wc_active = true; } } // If WooCommerce not active for site, return an empty response. if ( ! $wc_active ) { $response = rest_ensure_response( array() ); return $response; } switch_to_blog( $blog_id ); add_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); $items = $this->get_items( $request ); remove_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); foreach ( $items->data as &$current_order ) { $order = wc_get_order( $current_order['id'] ); $current_order['blog'] = get_blog_details( get_current_blog_id() ); $current_order['edit_url'] = get_admin_url( $blog_id, 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' ); /* translators: 1: first name 2: last name */ $current_order['customer'] = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) ); $current_order['status_name'] = wc_get_order_status_name( $order->get_status() ); $current_order['formatted_total'] = $order->get_formatted_order_total(); } restore_current_blog(); return $items; } /** * Filters the post statuses to on hold and processing for the network order query. * * @param array $args Query args. * * @return array */ public function network_orders_filter_args( $args ) { $args['post_status'] = array( 'wc-on-hold', 'wc-processing', ); return $args; } } includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php 0000644 00000014204 15132754523 0023667 0 ustar 00 <?php /** * REST API Settings controller * * Handles requests to the /settings endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Settings controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Settings_V2_Controller extends WC_REST_Controller { /** * WP REST API namespace/version. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'settings'; /** * Register routes. * * @since 3.0.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get all settings groups items. * * @since 3.0.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $groups = apply_filters( 'woocommerce_settings_groups', array() ); if ( empty( $groups ) ) { return new WP_Error( 'rest_setting_groups_empty', __( 'No setting groups have been registered.', 'woocommerce' ), array( 'status' => 500 ) ); } $defaults = $this->group_defaults(); $filtered_groups = array(); foreach ( $groups as $group ) { $sub_groups = array(); foreach ( $groups as $_group ) { if ( ! empty( $_group['parent_id'] ) && $group['id'] === $_group['parent_id'] ) { $sub_groups[] = $_group['id']; } } $group['sub_groups'] = $sub_groups; $group = wp_parse_args( $group, $defaults ); if ( ! is_null( $group['id'] ) && ! is_null( $group['label'] ) ) { $group_obj = $this->filter_group( $group ); $group_data = $this->prepare_item_for_response( $group_obj, $request ); $group_data = $this->prepare_response_for_collection( $group_data ); $filtered_groups[] = $group_data; } } $response = rest_ensure_response( $filtered_groups ); return $response; } /** * Prepare links for the request. * * @param string $group_id Group ID. * @return array Links for the given group. */ protected function prepare_links( $group_id ) { $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'options' => array( 'href' => rest_url( trailingslashit( $base ) . $group_id ), ), ); return $links; } /** * Prepare a report sales object for serialization. * * @since 3.0.0 * @param array $item Group object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { $context = empty( $request['context'] ) ? 'view' : $request['context']; $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item['id'] ) ); return $response; } /** * Filters out bad values from the groups array/filter so we * only return known values via the API. * * @since 3.0.0 * @param array $group Group. * @return array */ public function filter_group( $group ) { return array_intersect_key( $group, array_flip( array_filter( array_keys( $group ), array( $this, 'allowed_group_keys' ) ) ) ); } /** * Callback for allowed keys for each group response. * * @since 3.0.0 * @param string $key Key to check. * @return boolean */ public function allowed_group_keys( $key ) { return in_array( $key, array( 'id', 'label', 'description', 'parent_id', 'sub_groups' ) ); } /** * Returns default settings for groups. null means the field is required. * * @since 3.0.0 * @return array */ protected function group_defaults() { return array( 'id' => null, 'label' => null, 'description' => '', 'parent_id' => '', 'sub_groups' => array(), ); } /** * Makes sure the current user has access to READ the settings APIs. * * @since 3.0.0 * @param WP_REST_Request $request Full data about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get the groups schema, conforming to JSON Schema. * * @since 3.0.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'setting_group', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'parent_id' => array( 'description' => __( 'ID of parent grouping.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'sub_groups' => array( 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php 0000644 00000014216 15132754523 0023653 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Webhooks controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Webhooks_V1_Controller */ class WC_REST_Webhooks_V2_Controller extends WC_REST_Webhooks_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Prepare a single webhook output for response. * * @param int $id Webhook ID. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response */ public function prepare_item_for_response( $id, $request ) { $webhook = wc_get_webhook( $id ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $data = array( 'id' => $webhook->get_id(), 'name' => $webhook->get_name(), 'status' => $webhook->get_status(), 'topic' => $webhook->get_topic(), 'resource' => $webhook->get_resource(), 'event' => $webhook->get_event(), 'hooks' => $webhook->get_hooks(), 'delivery_url' => $webhook->get_delivery_url(), 'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created(), false ), 'date_created_gmt' => wc_rest_prepare_date_response( $webhook->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified(), false ), 'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $webhook->get_id(), $request ) ); /** * Filter webhook object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Webhook $webhook Webhook object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); } /** * Get the default REST API version. * * @since 3.0.0 * @return string */ protected function get_default_api_version() { return 'wp_api_v2'; } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Webhook status.', 'woocommerce' ), 'type' => 'string', 'default' => 'active', 'enum' => array_keys( wc_get_webhook_statuses() ), 'context' => array( 'view', 'edit' ), ), 'topic' => array( 'description' => __( 'Webhook topic.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'resource' => array( 'description' => __( 'Webhook resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'event' => array( 'description' => __( 'Webhook event.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'hooks' => array( 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'delivery_url' => array( 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'secret' => array( 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'date_created' => array( 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the webhook was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the webhook was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php 0000644 00000030627 15132754523 0024062 0 ustar 00 <?php /** * REST API Customers controller * * Handles requests to the /customers endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Customers_V1_Controller */ class WC_REST_Customers_V2_Controller extends WC_REST_Customers_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Get formatted item data. * * @since 3.0.0 * @param WC_Data $object WC_Data instance. * @return array */ protected function get_formatted_item_data( $object ) { $data = $object->get_data(); $format_date = array( 'date_created', 'date_modified' ); // Format date values. foreach ( $format_date as $key ) { $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); } return array( 'id' => $object->get_id(), 'date_created' => $data['date_created'], 'date_created_gmt' => $data['date_created_gmt'], 'date_modified' => $data['date_modified'], 'date_modified_gmt' => $data['date_modified_gmt'], 'email' => $data['email'], 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'role' => $data['role'], 'username' => $data['username'], 'billing' => $data['billing'], 'shipping' => $data['shipping'], 'is_paying_customer' => $data['is_paying_customer'], 'orders_count' => $object->get_order_count(), 'total_spent' => $object->get_total_spent(), 'avatar_url' => $object->get_avatar_url(), 'meta_data' => $data['meta_data'], ); } /** * Prepare a single customer output for response. * * @param WP_User $user_data User object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $user_data, $request ) { $customer = new WC_Customer( $user_data->ID ); $data = $this->get_formatted_item_data( $customer ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $user_data ) ); /** * Filter customer data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_User $user_data User object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); } /** * Update customer meta fields. * * @param WC_Customer $customer Customer data. * @param WP_REST_Request $request Request data. */ protected function update_customer_meta_fields( $customer, $request ) { parent::update_customer_meta_fields( $customer, $request ); // Meta data. if ( isset( $request['meta_data'] ) ) { if ( is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $customer->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } } } /** * Get the Customer's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'email' => array( 'description' => __( 'The email address for the customer.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'first_name' => array( 'description' => __( 'Customer first name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Customer last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'role' => array( 'description' => __( 'Customer role.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'username' => array( 'description' => __( 'Customer login name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_user', ), ), 'password' => array( 'description' => __( 'Customer password.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'billing' => array( 'description' => __( 'List of billing address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'List of shipping address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'is_paying_customer' => array( 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), 'type' => 'bool', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'orders_count' => array( 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_spent' => array( 'description' => __( 'Total amount spent.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'avatar_url' => array( 'description' => __( 'Avatar URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php 0000644 00000053503 15132754523 0026037 0 ustar 00 <?php /** * REST API WC System Status Tools Controller * * Handles requests to the /system_status/tools/* endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * System status tools controller. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'system_status/tools'; /** * Register the routes for /system_status/tools/*. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to view system status tools. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to view a specific system status tool. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to execute a specific system status tool. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * A list of available tools for use in the system status section. * 'button' becomes 'action' in the API. * * @return array */ public function get_tools() { $tools = array( 'clear_transients' => array( 'name' => __( 'WooCommerce transients', 'woocommerce' ), 'button' => __( 'Clear transients', 'woocommerce' ), 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), ), 'clear_expired_transients' => array( 'name' => __( 'Expired transients', 'woocommerce' ), 'button' => __( 'Clear transients', 'woocommerce' ), 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), ), 'delete_orphaned_variations' => array( 'name' => __( 'Orphaned variations', 'woocommerce' ), 'button' => __( 'Delete orphaned variations', 'woocommerce' ), 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), ), 'clear_expired_download_permissions' => array( 'name' => __( 'Used-up download permissions', 'woocommerce' ), 'button' => __( 'Clean up download permissions', 'woocommerce' ), 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ), ), 'regenerate_product_lookup_tables' => array( 'name' => __( 'Product lookup tables', 'woocommerce' ), 'button' => __( 'Regenerate', 'woocommerce' ), 'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ), ), 'recount_terms' => array( 'name' => __( 'Term counts', 'woocommerce' ), 'button' => __( 'Recount terms', 'woocommerce' ), 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), ), 'reset_roles' => array( 'name' => __( 'Capabilities', 'woocommerce' ), 'button' => __( 'Reset capabilities', 'woocommerce' ), 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), ), 'clear_sessions' => array( 'name' => __( 'Clear customer sessions', 'woocommerce' ), 'button' => __( 'Clear', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' ) ), ), 'clear_template_cache' => array( 'name' => __( 'Clear template cache', 'woocommerce' ), 'button' => __( 'Clear', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This tool will empty the template cache.', 'woocommerce' ) ), ), 'install_pages' => array( 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ), 'button' => __( 'Create pages', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) ), ), 'delete_taxes' => array( 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ), 'button' => __( 'Delete tax rates', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' ) ), ), 'regenerate_thumbnails' => array( 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ), 'button' => __( 'Regenerate', 'woocommerce' ), 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ), ), 'db_update_routine' => array( 'name' => __( 'Update database', 'woocommerce' ), 'button' => __( 'Update database', 'woocommerce' ), 'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s', __( 'Note:', 'woocommerce' ), __( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' ) ), ), ); if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) { $tools['verify_db_tables'] = array( 'name' => __( 'Verify base database tables', 'woocommerce' ), 'button' => __( 'Verify database', 'woocommerce' ), 'desc' => sprintf( __( 'Verify if all base database tables are present.', 'woocommerce' ) ), ); } // Jetpack does the image resizing heavy lifting so you don't have to. if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) { unset( $tools['regenerate_thumbnails'] ); } if ( ! function_exists( 'wc_clear_template_cache' ) ) { unset( $tools['clear_template_cache'] ); } return apply_filters( 'woocommerce_debug_tools', $tools ); } /** * Get a list of system status tools. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $tools = array(); foreach ( $this->get_tools() as $id => $tool ) { $tools[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( array( 'id' => $id, 'name' => $tool['name'], 'action' => $tool['button'], 'description' => $tool['desc'], ), $request ) ); } $response = rest_ensure_response( $tools ); return $response; } /** * Return a single tool. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $tools = $this->get_tools(); if ( empty( $tools[ $request['id'] ] ) ) { return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $tool = $tools[ $request['id'] ]; return rest_ensure_response( $this->prepare_item_for_response( array( 'id' => $request['id'], 'name' => $tool['name'], 'action' => $tool['button'], 'description' => $tool['desc'], ), $request ) ); } /** * Update (execute) a tool. * * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $tools = $this->get_tools(); if ( empty( $tools[ $request['id'] ] ) ) { return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $tool = $tools[ $request['id'] ]; $tool = array( 'id' => $request['id'], 'name' => $tool['name'], 'action' => $tool['button'], 'description' => $tool['desc'], ); $execute_return = $this->execute_tool( $request['id'] ); $tool = array_merge( $tool, $execute_return ); /** * Fires after a WooCommerce REST system status tool has been executed. * * @param array $tool Details about the tool that has been executed. * @param WP_REST_Request $request The current WP_REST_Request object. */ do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tool, $request ); return rest_ensure_response( $response ); } /** * Prepare a tool item for serialization. * * @param array $item Object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { $context = empty( $request['context'] ) ? 'view' : $request['context']; $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item['id'] ) ); return $response; } /** * Get the system status tools schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'system_status_tool', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'name' => array( 'description' => __( 'Tool name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'action' => array( 'description' => __( 'What running the tool will do.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'description' => array( 'description' => __( 'Tool description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'success' => array( 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'edit' ), ), 'message' => array( 'description' => __( 'Tool return message.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Prepare links for the request. * * @param string $id ID. * @return array */ protected function prepare_links( $id ) { $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'item' => array( 'href' => rest_url( trailingslashit( $base ) . $id ), 'embeddable' => true, ), ); return $links; } /** * Get any query params needed. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } /** * Actually executes a tool. * * @param string $tool Tool. * @return array */ public function execute_tool( $tool ) { global $wpdb; $ran = true; switch ( $tool ) { case 'clear_transients': wc_delete_product_transients(); wc_delete_shop_order_transients(); delete_transient( 'wc_count_comments' ); delete_transient( 'as_comment_count' ); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( $attribute_taxonomies ) { foreach ( $attribute_taxonomies as $attribute ) { delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name ); } } WC_Cache_Helper::get_transient_version( 'shipping', true ); $message = __( 'Product transients cleared', 'woocommerce' ); break; case 'clear_expired_transients': /* translators: %d: amount of expired transients */ $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() ); break; case 'delete_orphaned_variations': // Delete orphans. $result = absint( $wpdb->query( "DELETE products FROM {$wpdb->posts} products LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" ) ); /* translators: %d: amount of orphaned variations */ $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); break; case 'clear_expired_download_permissions': // Delete expired download permissions and ones with 0 downloads remaining. $result = absint( $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )", gmdate( 'Y-m-d', current_time( 'timestamp' ) ) ) ) ); /* translators: %d: amount of permissions */ $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result ); break; case 'regenerate_product_lookup_tables': if ( ! wc_update_product_lookup_tables_is_running() ) { wc_update_product_lookup_tables(); } $message = __( 'Lookup tables are regenerating', 'woocommerce' ); break; case 'reset_roles': // Remove then re-add caps and roles. WC_Install::remove_roles(); WC_Install::create_roles(); $message = __( 'Roles successfully reset', 'woocommerce' ); break; case 'recount_terms': wc_recount_all_terms(); $message = __( 'Terms successfully recounted', 'woocommerce' ); break; case 'clear_sessions': $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok. wp_cache_flush(); /* translators: %d: amount of sessions */ $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) ); break; case 'install_pages': WC_Install::create_pages(); $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); break; case 'delete_taxes': $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) { WC_Cache_Helper::invalidate_cache_group( 'taxes' ); } else { WC_Cache_Helper::incr_cache_prefix( 'taxes' ); } $message = __( 'Tax rates successfully deleted', 'woocommerce' ); break; case 'regenerate_thumbnails': WC_Regenerate_Images::queue_image_regeneration(); $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' ); break; case 'db_update_routine': $blog_id = get_current_blog_id(); // Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck(). // This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running. do_action( 'wp_' . $blog_id . '_wc_updater_cron' ); $message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' ); break; case 'clear_template_cache': if ( function_exists( 'wc_clear_template_cache' ) ) { wc_clear_template_cache(); $message = __( 'Template cache cleared.', 'woocommerce' ); } else { $message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' ); $ran = false; } break; case 'verify_db_tables': if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) { $message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' ); $ran = false; break; } // Try to manually create table again. $missing_tables = WC_Install::verify_base_tables( true, true ); if ( 0 === count( $missing_tables ) ) { $message = __( 'Database verified successfully.', 'woocommerce' ); } else { $message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' ); $message .= implode( ', ', $missing_tables ); $ran = false; } break; default: $tools = $this->get_tools(); if ( isset( $tools[ $tool ]['callback'] ) ) { $callback = $tools[ $tool ]['callback']; try { $return = call_user_func( $callback ); } catch ( Exception $exception ) { $return = $exception; } if ( is_a( $return, Exception::class ) ) { $callback_string = $this->get_printable_callback_name( $callback, $tool ); $ran = false; /* translators: %1$s: callback string, %2$s: error message */ $message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() ); $logger = wc_get_logger(); $logger->error( sprintf( 'Error running debug tool %s: %s', $tool, $return->getMessage() ), array( 'source' => 'run-debug-tool', 'tool' => $tool, 'callback' => $callback, 'error' => $return, ) ); } elseif ( is_string( $return ) ) { $message = $return; } elseif ( false === $return ) { $callback_string = $this->get_printable_callback_name( $callback, $tool ); $ran = false; /* translators: %s: callback string */ $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); } else { $message = __( 'Tool ran.', 'woocommerce' ); } } else { $ran = false; $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); } break; } return array( 'success' => $ran, 'message' => $message, ); } /** * Get a printable name for a callback. * * @param mixed $callback The callback to get a name for. * @param string $default The default name, to be returned when the callback is an inline function. * @return string A printable name for the callback. */ private function get_printable_callback_name( $callback, $default ) { if ( is_array( $callback ) ) { return get_class( $callback[0] ) . '::' . $callback[1]; } if ( is_string( $callback ) ) { return $callback; } return $default; } } includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php 0000644 00000171610 15132754523 0023332 0 ustar 00 <?php /** * REST API Orders controller * * Handles requests to the /orders endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'orders'; /** * Post type. * * @var string */ protected $post_type = 'shop_order'; /** * If object is hierarchical. * * @var bool */ protected $hierarchical = true; /** * Stores the request. * * @var array */ protected $request = array(); /** * Register the routes for orders. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get object. Return false if object is not of required type. * * @since 3.0.0 * @param int $id Object ID. * @return WC_Data|bool */ protected function get_object( $id ) { $order = wc_get_order( $id ); // In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path. if ( ! $order || 'shop_order_refund' === $order->get_type() ) { return false; } return $order; } /** * Expands an order item to get its data. * * @param WC_Order_item $item Order item data. * @return array */ protected function get_order_item_data( $item ) { $data = $item->get_data(); $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); // Format decimal values. foreach ( $format_decimal as $key ) { if ( isset( $data[ $key ] ) ) { $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); } } // Add SKU and PRICE to products. if ( is_callable( array( $item, 'get_product' ) ) ) { $data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null; $data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0; } // Add parent_name if the product is a variation. if ( is_callable( array( $item, 'get_product' ) ) ) { $product = $item->get_product(); if ( is_callable( array( $product, 'get_parent_data' ) ) ) { $data['parent_name'] = $product->get_title(); } else { $data['parent_name'] = null; } } // Format taxes. if ( ! empty( $data['taxes']['total'] ) ) { $taxes = array(); foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { $taxes[] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', ); } $data['taxes'] = $taxes; } elseif ( isset( $data['taxes'] ) ) { $data['taxes'] = array(); } // Remove names for coupons, taxes and shipping. if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { unset( $data['name'] ); } // Remove props we don't want to expose. unset( $data['order_id'] ); unset( $data['type'] ); // Expand meta_data to include user-friendly values. $formatted_meta_data = $item->get_formatted_meta_data( null, true ); $data['meta_data'] = array_map( array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), $data['meta_data'], array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data ) ); return $data; } /** * Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding * {@link WC_Meta_Data}. Returns the merged array. * * @param WC_Meta_Data $meta_item An object from {@link WC_Order_Item::get_meta_data()}. * @param array $formatted_meta_data An object result from {@link WC_Order_Item::get_formatted_meta_data}. * The keys are the IDs of {@link WC_Meta_Data}. * * @return array */ private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) { $result = array( 'id' => $meta_item->id, 'key' => $meta_item->key, 'value' => $meta_item->value, 'display_key' => $meta_item->key, // Default to original key, in case a formatted key is not available. 'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available. ); if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) { $formatted_meta_item = $formatted_meta_data[ $meta_item->id ]; $result['display_key'] = wc_clean( $formatted_meta_item->display_key ); $result['display_value'] = wc_clean( $formatted_meta_item->display_value ); } return $result; } /** * Get formatted item data. * * @since 3.0.0 * @param WC_Order $order WC_Data instance. * * @return array */ protected function get_formatted_item_data( $order ) { $extra_fields = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds' ); $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); // These fields are dependent on other fields. $dependent_fields = array( 'date_created_gmt' => 'date_created', 'date_modified_gmt' => 'date_modified', 'date_completed_gmt' => 'date_completed', 'date_paid_gmt' => 'date_paid', ); $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); // Only fetch fields that we need. $fields = $this->get_fields_for_response( $this->request ); foreach ( $dependent_fields as $field_key => $dependency ) { if ( in_array( $field_key, $fields ) && ! in_array( $dependency, $fields ) ) { $fields[] = $dependency; } } $extra_fields = array_intersect( $extra_fields, $fields ); $format_decimal = array_intersect( $format_decimal, $fields ); $format_date = array_intersect( $format_date, $fields ); $format_line_items = array_intersect( $format_line_items, $fields ); $data = $order->get_base_data(); // Add extra data as necessary. foreach ( $extra_fields as $field ) { switch ( $field ) { case 'meta_data': $data['meta_data'] = $order->get_meta_data(); break; case 'line_items': $data['line_items'] = $order->get_items( 'line_item' ); break; case 'tax_lines': $data['tax_lines'] = $order->get_items( 'tax' ); break; case 'shipping_lines': $data['shipping_lines'] = $order->get_items( 'shipping' ); break; case 'fee_lines': $data['fee_lines'] = $order->get_items( 'fee' ); break; case 'coupon_lines': $data['coupon_lines'] = $order->get_items( 'coupon' ); break; case 'refunds': $data['refunds'] = array(); foreach ( $order->get_refunds() as $refund ) { $data['refunds'][] = array( 'id' => $refund->get_id(), 'reason' => $refund->get_reason() ? $refund->get_reason() : '', 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), ); } break; } } // Format decimal values. foreach ( $format_decimal as $key ) { $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); } // Format date values. foreach ( $format_date as $key ) { $datetime = $data[ $key ]; $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); } // Format the order status. $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; // Format line items. foreach ( $format_line_items as $key ) { $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); } $allowed_fields = array( 'id', 'parent_id', 'number', 'order_key', 'created_via', 'version', 'status', 'currency', 'date_created', 'date_created_gmt', 'date_modified', 'date_modified_gmt', 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax', 'prices_include_tax', 'customer_id', 'customer_ip_address', 'customer_user_agent', 'customer_note', 'billing', 'shipping', 'payment_method', 'payment_method_title', 'transaction_id', 'date_paid', 'date_paid_gmt', 'date_completed', 'date_completed_gmt', 'cart_hash', 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds', ); $data = array_intersect_key( $data, array_flip( $allowed_fields ) ); return $data; } /** * Prepare a single order output for response. * * @since 3.0.0 * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $this->request = $request; $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); $request['context'] = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->get_formatted_item_data( $object ); $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $request['context'] ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare links for the request. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return array Links for the given post. */ protected function prepare_links( $object, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( 0 !== (int) $object->get_customer_id() ) { $links['customer'] = array( 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), ); } if ( 0 !== (int) $object->get_parent_id() ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), ); } return $links; } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { global $wpdb; $args = parent::prepare_objects_query( $request ); // Set post_status. if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) { $args['post_status'] = 'wc-' . $request['status']; } elseif ( 'any' === $request['status'] ) { $args['post_status'] = 'any'; } else { $args['post_status'] = $request['status']; } if ( isset( $request['customer'] ) ) { if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } $args['meta_query'][] = array( 'key' => '_customer_user', 'value' => $request['customer'], 'type' => 'NUMERIC', ); } // Search by product. if ( ! empty( $request['product'] ) ) { $order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) AND order_item_type = 'line_item'", $request['product'] ) ); // Force WP_Query return empty if don't found any order. $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); $args['post__in'] = $order_ids; } // Search. if ( ! empty( $args['s'] ) ) { $order_ids = wc_order_search( $args['s'] ); if ( ! empty( $order_ids ) ) { unset( $args['s'] ); $args['post__in'] = array_merge( $order_ids, array( 0 ) ); } } /** * Filter the query arguments for a request. * * Enables adding extra arguments or setting defaults for an order collection request. * * @param array $args Key value array of query var to query value. * @param WP_REST_Request $request The request used. */ $args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request ); return $args; } /** * Only return writable props from schema. * * @param array $schema Schema. * @return bool */ protected function filter_writable_props( $schema ) { return empty( $schema['readonly'] ); } /** * Prepare a single order for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $order = new WC_Order( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'status': // Status change should be done later so transitions have new data. break; case 'billing': case 'shipping': $this->update_address( $order, $value, $key ); break; case 'line_items': case 'shipping_lines': case 'fee_lines': case 'coupon_lines': if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( is_array( $item ) ) { if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { $order->remove_item( $item['id'] ); } else { $this->set_item( $order, $key, $item ); } } } } break; case 'meta_data': if ( is_array( $value ) ) { foreach ( $value as $meta ) { $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; default: if ( is_callable( array( $order, "set_{$key}" ) ) ) { $order->{"set_{$key}"}( $value ); } break; } } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $order Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); } /** * Save an object data. * * @since 3.0.0 * @throws WC_REST_Exception But all errors are validated before returning any data. * @param WP_REST_Request $request Full details about the request. * @param bool $creating If is creating a new object. * @return WC_Data|WP_Error */ protected function save_object( $request, $creating = false ) { try { $object = $this->prepare_object_for_database( $request, $creating ); if ( is_wp_error( $object ) ) { return $object; } // Make sure gateways are loaded so hooks from gateways fire on save/create. WC()->payment_gateways(); if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { // Make sure customer exists. if ( false === get_user_by( 'id', $request['customer_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } // Make sure customer is part of blog. if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); } } if ( $creating ) { $object->set_created_via( 'rest-api' ); $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $object->calculate_totals(); } else { // If items have changed, recalculate order totals. if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { $object->calculate_totals( true ); } } // Set status. if ( ! empty( $request['status'] ) ) { $object->set_status( $request['status'] ); } $object->save(); // Actions for after the order is saved. if ( true === $request['set_paid'] ) { if ( $creating || $object->needs_payment() ) { $object->payment_complete( $request['transaction_id'] ); } } return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update address. * * @param WC_Order $order Order data. * @param array $posted Posted data. * @param string $type Address type. */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { $order->{"set_{$type}_{$key}"}( $value ); } } } /** * Gets the product ID from the SKU or posted ID. * * @throws WC_REST_Exception When SKU or ID is not valid. * @param array $posted Request data. * @param string $action 'create' to add line item or 'update' to update it. * @return int */ protected function get_product_id( $posted, $action = 'create' ) { if ( ! empty( $posted['sku'] ) ) { $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { $product_id = (int) $posted['product_id']; } elseif ( ! empty( $posted['variation_id'] ) ) { $product_id = (int) $posted['variation_id']; } elseif ( 'update' === $action ) { $product_id = 0; } else { throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); } return $product_id; } /** * Maybe set an item prop if the value was posted. * * @param WC_Order_Item $item Order item. * @param string $prop Order property. * @param array $posted Request data. */ protected function maybe_set_item_prop( $item, $prop, $posted ) { if ( isset( $posted[ $prop ] ) ) { $item->{"set_$prop"}( $posted[ $prop ] ); } } /** * Maybe set item props if the values were posted. * * @param WC_Order_Item $item Order item data. * @param string[] $props Properties. * @param array $posted Request data. */ protected function maybe_set_item_props( $item, $props, $posted ) { foreach ( $props as $prop ) { $this->maybe_set_item_prop( $item, $prop, $posted ); } } /** * Maybe set item meta if posted. * * @param WC_Order_Item $item Order item data. * @param array $posted Request data. */ protected function maybe_set_item_meta_data( $item, $posted ) { if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { foreach ( $posted['meta_data'] as $meta ) { if ( isset( $meta['key'] ) ) { $value = isset( $meta['value'] ) ? $meta['value'] : null; $item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' ); } } } } /** * Create or update a line item. * * @param array $posted Line item data. * @param string $action 'create' to add line item or 'update' to update it. * @param object $item Passed when updating an item. Null during creation. * @return WC_Order_Item_Product * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_line_items( $posted, $action = 'create', $item = null ) { $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; $product = wc_get_product( $this->get_product_id( $posted, $action ) ); if ( $product && $product !== $item->get_product() ) { $item->set_product( $product ); if ( 'create' === $action ) { $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); $item->set_total( $total ); $item->set_subtotal( $total ); } } $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); $this->maybe_set_item_meta_data( $item, $posted ); return $item; } /** * Create or update an order shipping method. * * @param array $posted $shipping Item data. * @param string $action 'create' to add shipping or 'update' to update it. * @param object $item Passed when updating an item. Null during creation. * @return WC_Order_Item_Shipping * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) { $item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; if ( 'create' === $action ) { if ( empty( $posted['method_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted ); $this->maybe_set_item_meta_data( $item, $posted ); return $item; } /** * Create or update an order fee. * * @param array $posted Item data. * @param string $action 'create' to add fee or 'update' to update it. * @param object $item Passed when updating an item. Null during creation. * @return WC_Order_Item_Fee * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) { $item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; if ( 'create' === $action ) { if ( empty( $posted['name'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); $this->maybe_set_item_meta_data( $item, $posted ); return $item; } /** * Create or update an order coupon. * * @param array $posted Item data. * @param string $action 'create' to add coupon or 'update' to update it. * @param object $item Passed when updating an item. Null during creation. * @return WC_Order_Item_Coupon * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) { $item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; if ( 'create' === $action ) { if ( empty( $posted['code'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); $this->maybe_set_item_meta_data( $item, $posted ); return $item; } /** * Wrapper method to create/update order items. * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @param WC_Order $order order object. * @param string $item_type The item type. * @param array $posted item provided in the request body. * @throws WC_REST_Exception If item ID is not associated with order. */ protected function set_item( $order, $item_type, $posted ) { global $wpdb; if ( ! empty( $posted['id'] ) ) { $action = 'update'; } else { $action = 'create'; } $method = 'prepare_' . $item_type; $item = null; // Verify provided line item ID is associated with order. if ( 'update' === $action ) { $item = $order->get_item( absint( $posted['id'] ), false ); if ( ! $item ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); } } // Prepare item data. $item = $this->$method( $posted, $action, $item ); do_action( 'woocommerce_rest_set_order_item', $item, $posted ); // If creating the order, add the item to it. if ( 'create' === $action ) { $order->add_item( $item ); } else { $item->save(); } } /** * Helper method to check if the resource ID associated with the provided item is null. * Items can be deleted by setting the resource ID to null. * * @param array $item Item provided in the request body. * @return bool True if the item resource ID is null, false otherwise. */ protected function item_is_null( $item ) { $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); foreach ( $keys as $key ) { if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { return true; } } return false; } /** * Get order statuses without prefixes. * * @return array */ protected function get_order_statuses() { $order_statuses = array(); foreach ( array_keys( wc_get_order_statuses() ) as $status ) { $order_statuses[] = str_replace( 'wc-', '', $status ); } return $order_statuses; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'parent_id' => array( 'description' => __( 'Parent order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'number' => array( 'description' => __( 'Order number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'created_via' => array( 'description' => __( 'Shows where the order was created.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'version' => array( 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'status' => array( 'description' => __( 'Order status.', 'woocommerce' ), 'type' => 'string', 'default' => 'pending', 'enum' => $this->get_order_statuses(), 'context' => array( 'view', 'edit' ), ), 'currency' => array( 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), 'type' => 'string', 'default' => get_woocommerce_currency(), 'enum' => array_keys( get_woocommerce_currencies() ), 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_total' => array( 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_tax' => array( 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_total' => array( 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax' => array( 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_tax' => array( 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Grand total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Sum of all taxes.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'prices_include_tax' => array( 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_id' => array( 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), 'type' => 'integer', 'default' => 0, 'context' => array( 'view', 'edit' ), ), 'customer_ip_address' => array( 'description' => __( "Customer's IP address.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_user_agent' => array( 'description' => __( 'User agent of the customer.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_note' => array( 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'billing' => array( 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => array( 'string', 'null' ), 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'payment_method' => array( 'description' => __( 'Payment method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'payment_method_title' => array( 'description' => __( 'Payment method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'transaction_id' => array( 'description' => __( 'Unique transaction ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_paid' => array( 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_paid_gmt' => array( 'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_completed' => array( 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_completed_gmt' => array( 'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_hash' => array( 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'parent_name' => array( 'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'display_key' => array( 'description' => __( 'Meta key for UI display.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'display_value' => array( 'description' => __( 'Meta value for UI display.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'tax_lines' => array( 'description' => __( 'Tax lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_code' => array( 'description' => __( 'Tax rate code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Tax rate label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'compound' => array( 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tax_total' => array( 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax_total' => array( 'description' => __( 'Shipping tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'shipping_lines' => array( 'description' => __( 'Shipping lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_title' => array( 'description' => __( 'Shipping method name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'method_id' => array( 'description' => __( 'Shipping method ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'instance_id' => array( 'description' => __( 'Shipping instance ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'fee_lines' => array( 'description' => __( 'Fee lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Fee name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'enum' => array( 'taxable', 'none' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'coupon_lines' => array( 'description' => __( 'Coupons line data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'discount' => array( 'description' => __( 'Discount total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount_tax' => array( 'description' => __( 'Discount total tax.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'refunds' => array( 'description' => __( 'List of refunds.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Refund ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reason' => array( 'description' => __( 'Refund reason.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Refund total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'set_paid' => array( 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['customer'] = array( 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['product'] = array( 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['dp'] = array( 'default' => wc_get_price_decimals(), 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php 0000644 00000005656 15132754523 0024271 0 ustar 00 <?php /** * REST API Tax Classes controller * * Handles requests to the /taxes/classes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Tax Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Tax_Classes_V1_Controller */ class WC_REST_Tax_Classes_V2_Controller extends WC_REST_Tax_Classes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Register the routes for tax classes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)', array( 'args' => array( 'slug' => array( 'description' => __( 'Unique slug for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get one tax class. * * @param WP_REST_Request $request Request object. * @return array */ public function get_item( $request ) { if ( 'standard' === $request['slug'] ) { $tax_class = array( 'slug' => 'standard', 'name' => __( 'Standard rate', 'woocommerce' ), ); } else { $tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); } $data = array(); if ( $tax_class ) { $class = $this->prepare_item_for_response( $tax_class, $request ); $class = $this->prepare_response_for_collection( $class ); $data[] = $class; } return rest_ensure_response( $data ); } } includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php 0000644 00000232721 15132754523 0023700 0 ustar 00 <?php /** * REST API Products controller * * Handles requests to the /products endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Products controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'products'; /** * Post type. * * @var string */ protected $post_type = 'product'; /** * If object is hierarchical. * * @var bool */ protected $hierarchical = true; /** * Initialize product actions. */ public function __construct() { add_action( "woocommerce_rest_insert_{$this->post_type}_object", array( $this, 'clear_transients' ) ); } /** * Register the routes for products. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), 'type' => 'boolean', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get object. * * @param int $id Object ID. * * @since 3.0.0 * @return WC_Data */ protected function get_object( $id ) { return wc_get_product( $id ); } /** * Prepare a single product output for response. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * * @since 3.0.0 * @return WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $this->request = $request; $data = $this->get_product_data( $object, $context, $request ); // Add variations to variable products. if ( $object->is_type( 'variable' ) && $object->has_child() ) { $data['variations'] = $object->get_children(); } // Add grouped products data. if ( $object->is_type( 'grouped' ) && $object->has_child() ) { $data['grouped_products'] = $object->get_children(); } $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare objects query. * * @param WP_REST_Request $request Full details about the request. * * @since 3.0.0 * @return array */ protected function prepare_objects_query( $request ) { $args = parent::prepare_objects_query( $request ); // Set post_status. $args['post_status'] = $request['status']; // Taxonomy query to filter products by type, category, // tag, shipping class, and attribute. $tax_query = array(); // Map between taxonomy name and arg's key. $taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', 'product_shipping_class' => 'shipping_class', ); // Set tax_query for each passed arg. foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], ); } } // Filter product type by slug. if ( ! empty( $request['type'] ) ) { $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], ); } // Filter by attribute and term. if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $tax_query[] = array( 'taxonomy' => $request['attribute'], 'field' => 'term_id', 'terms' => $request['attribute_term'], ); } } if ( ! empty( $tax_query ) ) { $args['tax_query'] = $tax_query; // WPCS: slow query ok. } // Filter featured. if ( is_bool( $request['featured'] ) ) { $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', ); } // Filter by sku. if ( ! empty( $request['sku'] ) ) { $skus = explode( ',', $request['sku'] ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $request['sku']; } $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_sku', 'value' => $skus, 'compare' => 'IN', ) ); } // Filter by tax class. if ( ! empty( $request['tax_class'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_tax_class', 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', ) ); } // Price filter. if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. } // Filter product in stock or out of stock. if ( is_bool( $request['in_stock'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. $args, array( 'key' => '_stock_status', 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', ) ); } // Filter by on sale products. if ( is_bool( $request['on_sale'] ) ) { $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = array( 'product', 'product_variation' ); } else { $args['post_type'] = $this->post_type; } return $args; } /** * Get the downloads for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * * @return array */ protected function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_downloads() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // MD5 hash. 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get taxonomy terms. * * @param WC_Product $product Product instance. * @param string $taxonomy Taxonomy slug. * * @return array */ protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { $terms = array(); foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { $terms[] = array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ); } return $terms; } /** * Get the images for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * * @return array */ protected function get_images( $product ) { $images = array(); $attachment_ids = array(); // Add featured image. if ( $product->get_image_id() ) { $attachment_ids[] = $product->get_image_id(); } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), 'src' => current( $attachment ), 'name' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => (int) $position, ); } // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. 'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now. 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), 'date_modified_gmt' => wc_rest_prepare_date_response( time() ), 'src' => wc_placeholder_img_src(), 'name' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Get attribute taxonomy label. * * @param string $name Taxonomy name. * * @deprecated 3.0.0 * @return string */ protected function get_attribute_taxonomy_label( $name ) { $tax = get_taxonomy( $name ); $labels = get_taxonomy_labels( $tax ); return $labels->singular_name; } /** * Get product attribute taxonomy name. * * @param string $slug Taxonomy name. * @param WC_Product $product Product data. * * @since 3.0.0 * @return string */ protected function get_attribute_taxonomy_name( $slug, $product ) { // Format slug so it matches attributes of the product. $slug = wc_attribute_taxonomy_slug( $slug ); $attributes = $product->get_attributes(); $attribute = false; // pa_ attributes. if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; } elseif ( isset( $attributes[ $slug ] ) ) { $attribute = $attributes[ $slug ]; } if ( ! $attribute ) { return $slug; } // Taxonomy attribute name. if ( $attribute->is_taxonomy() ) { $taxonomy = $attribute->get_taxonomy_object(); return $taxonomy->attribute_label; } // Custom product attribute name. return $attribute->get_name(); } /** * Get default attributes. * * @param WC_Product $product Product instance. * * @return array */ protected function get_default_attributes( $product ) { $default = array(); if ( $product->is_type( 'variable' ) ) { foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { if ( 0 === strpos( $key, 'pa_' ) ) { $default[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $key ), 'name' => $this->get_attribute_taxonomy_name( $key, $product ), 'option' => $value, ); } else { $default[] = array( 'id' => 0, 'name' => $this->get_attribute_taxonomy_name( $key, $product ), 'option' => $value, ); } } } return $default; } /** * Get attribute options. * * @param int $product_id Product ID. * @param array $attribute Attribute data. * * @return array */ protected function get_attribute_options( $product_id, $attribute ) { if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names', ) ); } elseif ( isset( $attribute['value'] ) ) { return array_map( 'trim', explode( '|', $attribute['value'] ) ); } return array(); } /** * Get the attributes for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * * @return array */ protected function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { $_product = wc_get_product( $product->get_parent_id() ); foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { $name = str_replace( 'attribute_', '', $attribute_name ); if ( empty( $attribute ) && '0' !== $attribute ) { continue; } // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { $option_term = get_term_by( 'slug', $attribute, $name ); $attributes[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $name ), 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, ); } else { $attributes[] = array( 'id' => 0, 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), 'option' => $attribute, ); } } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } } return $attributes; } /** * Fetch price HTML. * * @param WC_Product $product Product object. * @param string $context Context of request, can be `view` or `edit`. * * @return string */ protected function api_get_price_html( $product, $context ) { return $product->get_price_html(); } /** * Fetch related IDs. * * @param WC_Product $product Product object. * @param string $context Context of request, can be `view` or `edit`. * * @return array */ protected function api_get_related_ids( $product, $context ) { return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ); } /** * Fetch meta data. * * @param WC_Product $product Product object. * @param string $context Context of request, can be `view` or `edit`. * * @return array */ protected function api_get_meta_data( $product, $context ) { return $product->get_meta_data(); } /** * Get product data. * * @param WC_Product $product Product instance. * @param string $context Request context. Options: 'view' and 'edit'. * * @return array */ protected function get_product_data( $product, $context = 'view' ) { /* * @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently. * * TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean. */ $request = func_num_args() >= 3 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) ); $fields = $this->get_fields_for_response( $request ); $base_data = array(); foreach ( $fields as $field ) { switch ( $field ) { case 'id': $base_data['id'] = $product->get_id(); break; case 'name': $base_data['name'] = $product->get_name( $context ); break; case 'slug': $base_data['slug'] = $product->get_slug( $context ); break; case 'permalink': $base_data['permalink'] = $product->get_permalink(); break; case 'date_created': $base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false ); break; case 'date_created_gmt': $base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) ); break; case 'date_modified': $base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ); break; case 'date_modified_gmt': $base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) ); break; case 'type': $base_data['type'] = $product->get_type(); break; case 'status': $base_data['status'] = $product->get_status( $context ); break; case 'featured': $base_data['featured'] = $product->is_featured(); break; case 'catalog_visibility': $base_data['catalog_visibility'] = $product->get_catalog_visibility( $context ); break; case 'description': $base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ); break; case 'short_description': $base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ); break; case 'sku': $base_data['sku'] = $product->get_sku( $context ); break; case 'price': $base_data['price'] = $product->get_price( $context ); break; case 'regular_price': $base_data['regular_price'] = $product->get_regular_price( $context ); break; case 'sale_price': $base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : ''; break; case 'date_on_sale_from': $base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ); break; case 'date_on_sale_from_gmt': $base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ); break; case 'date_on_sale_to': $base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ); break; case 'date_on_sale_to_gmt': $base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ); break; case 'on_sale': $base_data['on_sale'] = $product->is_on_sale( $context ); break; case 'purchasable': $base_data['purchasable'] = $product->is_purchasable(); break; case 'total_sales': $base_data['total_sales'] = $product->get_total_sales( $context ); break; case 'virtual': $base_data['virtual'] = $product->is_virtual(); break; case 'downloadable': $base_data['downloadable'] = $product->is_downloadable(); break; case 'downloads': $base_data['downloads'] = $this->get_downloads( $product ); break; case 'download_limit': $base_data['download_limit'] = $product->get_download_limit( $context ); break; case 'download_expiry': $base_data['download_expiry'] = $product->get_download_expiry( $context ); break; case 'external_url': $base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : ''; break; case 'button_text': $base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : ''; break; case 'tax_status': $base_data['tax_status'] = $product->get_tax_status( $context ); break; case 'tax_class': $base_data['tax_class'] = $product->get_tax_class( $context ); break; case 'manage_stock': $base_data['manage_stock'] = $product->managing_stock(); break; case 'stock_quantity': $base_data['stock_quantity'] = $product->get_stock_quantity( $context ); break; case 'in_stock': $base_data['in_stock'] = $product->is_in_stock(); break; case 'backorders': $base_data['backorders'] = $product->get_backorders( $context ); break; case 'backorders_allowed': $base_data['backorders_allowed'] = $product->backorders_allowed(); break; case 'backordered': $base_data['backordered'] = $product->is_on_backorder(); break; case 'low_stock_amount': $base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount(); break; case 'sold_individually': $base_data['sold_individually'] = $product->is_sold_individually(); break; case 'weight': $base_data['weight'] = $product->get_weight( $context ); break; case 'dimensions': $base_data['dimensions'] = array( 'length' => $product->get_length( $context ), 'width' => $product->get_width( $context ), 'height' => $product->get_height( $context ), ); break; case 'shipping_required': $base_data['shipping_required'] = $product->needs_shipping(); break; case 'shipping_taxable': $base_data['shipping_taxable'] = $product->is_shipping_taxable(); break; case 'shipping_class': $base_data['shipping_class'] = $product->get_shipping_class(); break; case 'shipping_class_id': $base_data['shipping_class_id'] = $product->get_shipping_class_id( $context ); break; case 'reviews_allowed': $base_data['reviews_allowed'] = $product->get_reviews_allowed( $context ); break; case 'average_rating': $base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ); break; case 'rating_count': $base_data['rating_count'] = $product->get_rating_count(); break; case 'upsell_ids': $base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) ); break; case 'cross_sell_ids': $base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) ); break; case 'parent_id': $base_data['parent_id'] = $product->get_parent_id( $context ); break; case 'purchase_note': $base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ); break; case 'categories': $base_data['categories'] = $this->get_taxonomy_terms( $product ); break; case 'tags': $base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' ); break; case 'images': $base_data['images'] = $this->get_images( $product ); break; case 'attributes': $base_data['attributes'] = $this->get_attributes( $product ); break; case 'default_attributes': $base_data['default_attributes'] = $this->get_default_attributes( $product ); break; case 'variations': $base_data['variations'] = array(); break; case 'grouped_products': $base_data['grouped_products'] = array(); break; case 'menu_order': $base_data['menu_order'] = $product->get_menu_order( $context ); break; } } $data = array_merge( $base_data, $this->fetch_fields_using_getters( $product, $context, $fields ) ); return $data; } /** * Prepare links for the request. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * * @return array Links for the given post. */ protected function prepare_links( $object, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. ), ); if ( $object->get_parent_id() ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. ); } return $links; } /** * Prepare a single product for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; // Type is the most important part here because we need to be using the correct class and methods. if ( isset( $request['type'] ) ) { $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); if ( ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } $product = new $classname( $id ); } elseif ( isset( $request['id'] ) ) { $product = wc_get_product( $id ); } else { $product = new WC_Product_Simple(); } if ( 'variation' === $product->get_type() ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404, ) ); } // Post title. if ( isset( $request['name'] ) ) { $product->set_name( wp_filter_post_kses( $request['name'] ) ); } // Post content. if ( isset( $request['description'] ) ) { $product->set_description( wp_filter_post_kses( $request['description'] ) ); } // Post excerpt. if ( isset( $request['short_description'] ) ) { $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); } // Post status. if ( isset( $request['status'] ) ) { $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); } // Post slug. if ( isset( $request['slug'] ) ) { $product->set_slug( $request['slug'] ); } // Menu order. if ( isset( $request['menu_order'] ) ) { $product->set_menu_order( $request['menu_order'] ); } // Comment status. if ( isset( $request['reviews_allowed'] ) ) { $product->set_reviews_allowed( $request['reviews_allowed'] ); } // Virtual. if ( isset( $request['virtual'] ) ) { $product->set_virtual( $request['virtual'] ); } // Tax status. if ( isset( $request['tax_status'] ) ) { $product->set_tax_status( $request['tax_status'] ); } // Tax Class. if ( isset( $request['tax_class'] ) ) { $product->set_tax_class( $request['tax_class'] ); } // Catalog Visibility. if ( isset( $request['catalog_visibility'] ) ) { $product->set_catalog_visibility( $request['catalog_visibility'] ); } // Purchase Note. if ( isset( $request['purchase_note'] ) ) { $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); } // Featured Product. if ( isset( $request['featured'] ) ) { $product->set_featured( $request['featured'] ); } // Shipping data. $product = $this->save_product_shipping_data( $product, $request ); // SKU. if ( isset( $request['sku'] ) ) { $product->set_sku( wc_clean( $request['sku'] ) ); } // Attributes. if ( isset( $request['attributes'] ) ) { $attributes = array(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = wc_clean( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( $attribute_id ) { if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names. $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Custom attribute - Add attribute to array and set the values. if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; } else { $values = explode( WC_DELIMITER, $attribute['options'] ); } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $request['regular_price'] ) ) { $product->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $product->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_from_gmt'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); } if ( isset( $request['date_on_sale_to'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to'] ); } if ( isset( $request['date_on_sale_to_gmt'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); } } // Product parent ID. if ( isset( $request['parent_id'] ) ) { $product->set_parent_id( $request['parent_id'] ); } // Sold individually. if ( isset( $request['sold_individually'] ) ) { $product->set_sold_individually( $request['sold_individually'] ); } // Stock status. if ( isset( $request['in_stock'] ) ) { $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; } else { $stock_status = $product->get_stock_status(); } // Stock data. if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $product->set_manage_stock( $request['manage_stock'] ); } // Backorders. if ( isset( $request['backorders'] ) ) { $product->set_backorders( $request['backorders'] ); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( $product->get_manage_stock() ) { // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity. if ( isset( $request['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells. if ( isset( $request['upsell_ids'] ) ) { $upsells = array(); $ids = $request['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } } $product->set_upsell_ids( $upsells ); } // Cross sells. if ( isset( $request['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $request['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } } $product->set_cross_sell_ids( $crosssells ); } // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); } // Downloadable. if ( isset( $request['downloadable'] ) ) { $product->set_downloadable( $request['downloadable'] ); } // Downloadable options. if ( $product->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $product->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $product->set_download_expiry( $request['download_expiry'] ); } } // Product url and button text for external products. if ( $product->is_type( 'external' ) ) { if ( isset( $request['external_url'] ) ) { $product->set_product_url( $request['external_url'] ); } if ( isset( $request['button_text'] ) ) { $product->set_button_text( $request['button_text'] ); } } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $request ); } // Set children for a grouped product. if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { $product->set_children( $request['grouped_products'] ); } // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $product = $this->set_product_images( $product, $request['images'] ); } // Allow set meta_data. if ( is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $product Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); } /** * Set product images. * * @param WC_Product $product Product instance. * @param array $images Images data. * * @throws WC_REST_Exception REST API exceptions. * @return WC_Product */ protected function set_product_images( $product, $images ) { $images = is_array( $images ) ? array_filter( $images ) : array(); if ( ! empty( $images ) ) { $gallery_positions = array(); foreach ( $images as $index => $image ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } else { continue; } } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); } if ( ! wp_attachment_is_image( $attachment_id ) ) { /* translators: %s: attachment id */ throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'], ) ); } // Set the image source if present, for future reference. if ( ! empty( $image['src'] ) ) { update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); } } // Sort images and get IDs in correct order. asort( $gallery_positions ); // Get gallery in correct order. $gallery = array_keys( $gallery_positions ); // Featured image is in position 0. $image_id = array_shift( $gallery ); // Set images. $product->set_image_id( $image_id ); $product->set_gallery_image_ids( $gallery ); } else { $product->set_image_id( '' ); $product->set_gallery_image_ids( array() ); } return $product; } /** * Save product shipping data. * * @param WC_Product $product Product instance. * @param array $data Shipping data. * * @return WC_Product */ protected function save_product_shipping_data( $product, $data ) { // Virtual. if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { $product->set_weight( '' ); $product->set_height( '' ); $product->set_length( '' ); $product->set_width( '' ); } else { if ( isset( $data['weight'] ) ) { $product->set_weight( $data['weight'] ); } // Height. if ( isset( $data['dimensions']['height'] ) ) { $product->set_height( $data['dimensions']['height'] ); } // Width. if ( isset( $data['dimensions']['width'] ) ) { $product->set_width( $data['dimensions']['width'] ); } // Length. if ( isset( $data['dimensions']['length'] ) ) { $product->set_length( $data['dimensions']['length'] ); } } // Shipping class. if ( isset( $data['shipping_class'] ) ) { $data_store = $product->get_data_store(); $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } return $product; } /** * Save downloadable files. * * @param WC_Product $product Product instance. * @param array $downloads Downloads data. * @param int $deprecated Deprecated since 3.0. * * @return WC_Product */ protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { if ( $deprecated ) { wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); } $files = array(); foreach ( $downloads as $key => $file ) { if ( empty( $file['file'] ) ) { continue; } $download = new WC_Product_Download(); $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); $files[] = $download; } $product->set_downloads( $files ); return $product; } /** * Save taxonomy terms. * * @param WC_Product $product Product instance. * @param array $terms Terms data. * @param string $taxonomy Taxonomy name. * * @return WC_Product */ protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { $term_ids = wp_list_pluck( $terms, 'id' ); if ( 'cat' === $taxonomy ) { $product->set_category_ids( $term_ids ); } elseif ( 'tag' === $taxonomy ) { $product->set_tag_ids( $term_ids ); } return $product; } /** * Save default attributes. * * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * * @since 3.0.0 * @return WC_Product */ protected function save_default_attributes( $product, $request ) { if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { $attributes = $product->get_attributes(); $default_attributes = array(); foreach ( $request['default_attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( isset( $attributes[ $attribute_name ] ) ) { $_attribute = $attributes[ $attribute_name ]; if ( $_attribute['is_variation'] ) { $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( ! empty( $_attribute['is_taxonomy'] ) ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $value = $term->slug; } else { $value = sanitize_title( $value ); } } if ( $value ) { $default_attributes[ $attribute_name ] = $value; } } } } $product->set_default_attributes( $default_attributes ); } return $product; } /** * Clear caches here so in sync with any new variations/children. * * @param WC_Data $object Object data. */ public function clear_transients( $object ) { wc_delete_product_transients( $object->get_id() ); wp_cache_delete( 'product-' . $object->get_id(), 'products' ); } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = (bool) $request['force']; $object = $this->get_object( (int) $request['id'] ); $result = false; if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404, ) ); } if ( 'variation' === $object->get_type() ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404, ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); /** * Filter whether an object is trashable. * * Return false to disable trash support for the object. * * @param boolean $supports_trash Whether the object type support trashing. * @param WC_Data $object The object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", /* translators: %s: post type */ sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code(), ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_object_for_response( $object, $request ); // If we're forcing, then delete permanently. if ( $force ) { if ( $object->is_type( 'variable' ) ) { foreach ( $object->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->delete( true ); } } } else { // For other product types, if the product has children, remove the relationship. foreach ( $object->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->set_parent_id( 0 ); $child->save(); } } } $object->delete( true ); $result = 0 === $object->get_id(); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', /* translators: %s: post type */ sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501, ) ); } // Otherwise, only trash if we haven't already. if ( is_callable( array( $object, 'get_status' ) ) ) { if ( 'trash' === $object->get_status() ) { return new WP_Error( 'woocommerce_rest_already_trashed', /* translators: %s: post type */ sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410, ) ); } $object->delete(); $result = 'trash' === $object->get_status(); } } if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', /* translators: %s: post type */ sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500, ) ); } // Delete parent product transients. if ( 0 !== $object->get_parent_id() ) { wc_delete_product_transients( $object->get_parent_id() ); } /** * Fires after a single object is deleted or trashed via the REST API. * * @param WC_Data $object The deleted or trashed object. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); return $response; } /** * Get the Product's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'slug' => array( 'description' => __( 'Product slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'default' => 'simple', 'enum' => array_keys( wc_get_product_types() ), 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Product status (post status).', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), 'context' => array( 'view', 'edit' ), ), 'featured' => array( 'description' => __( 'Featured product.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'catalog_visibility' => array( 'description' => __( 'Catalog visibility.', 'woocommerce' ), 'type' => 'string', 'default' => 'visible', 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Product description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'short_description' => array( 'description' => __( 'Product short description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Product regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Product sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from_gmt' => array( 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to_gmt' => array( 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'price_html' => array( 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'on_sale' => array( 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_sales' => array( 'description' => __( 'Amount of sales.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the product is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the product is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'external_url' => array( 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'button_text' => array( 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sold_individually' => array( 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Product dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_taxable' => array( 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reviews_allowed' => array( 'description' => __( 'Allow reviews.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'average_rating' => array( 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rating_count' => array( 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'related_ids' => array( 'description' => __( 'List of related products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'upsell_ids' => array( 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'cross_sell_ids' => array( 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'parent_id' => array( 'description' => __( 'Product parent ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'purchase_note' => array( 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'categories' => array( 'description' => __( 'List of categories.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Category ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Category slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'tags' => array( 'description' => __( 'List of tags.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tag ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Tag slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'images' => array( 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Attribute position.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'visible' => array( 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'variation' => array( 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'options' => array( 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'string', ), ), ), ), ), 'default_attributes' => array( 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'variations' => array( 'description' => __( 'List of variations IDs.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'integer', ), 'readonly' => true, ), 'grouped_products' => array( 'description' => __( 'List of grouped products ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) ); $params['slug'] = array( 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['type'] = array( 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_types() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['sku'] = array( 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['featured'] = array( 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'wc_string_to_bool', 'validate_callback' => 'rest_validate_request_arg', ); $params['category'] = array( 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag'] = array( 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['shipping_class'] = array( 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute'] = array( 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute_term'] = array( 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); if ( wc_tax_enabled() ) { $params['tax_class'] = array( 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); } $params['in_stock'] = array( 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'wc_string_to_bool', 'validate_callback' => 'rest_validate_request_arg', ); $params['on_sale'] = array( 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'wc_string_to_bool', 'validate_callback' => 'rest_validate_request_arg', ); $params['min_price'] = array( 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['max_price'] = array( 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php 0000644 00000021046 15132754523 0025006 0 ustar 00 <?php /** * REST API Shipping Zones controller * * Handles requests to the /shipping/zones endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zones class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zones_Controller_Base */ class WC_REST_Shipping_Zones_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base { /** * Register the routes for Shipping Zones. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Shipping zone name.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_items_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get a single Shipping Zone. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { $zone = $this->get_zone( $request->get_param( 'id' ) ); if ( is_wp_error( $zone ) ) { return $zone; } $data = $zone->get_data(); $data = $this->prepare_item_for_response( $data, $request ); $data = $this->prepare_response_for_collection( $data ); return rest_ensure_response( $data ); } /** * Get all Shipping Zones. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response */ public function get_items( $request ) { $rest_of_the_world = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 ); $zones = WC_Shipping_Zones::get_zones(); array_unshift( $zones, $rest_of_the_world->get_data() ); $data = array(); foreach ( $zones as $zone_obj ) { $zone = $this->prepare_item_for_response( $zone_obj, $request ); $zone = $this->prepare_response_for_collection( $zone ); $data[] = $zone; } return rest_ensure_response( $data ); } /** * Create a single Shipping Zone. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { $zone = new WC_Shipping_Zone( null ); if ( ! is_null( $request->get_param( 'name' ) ) ) { $zone->set_zone_name( $request->get_param( 'name' ) ); } if ( ! is_null( $request->get_param( 'order' ) ) ) { $zone->set_zone_order( $request->get_param( 'order' ) ); } $zone->save(); if ( $zone->get_id() !== 0 ) { $request->set_param( 'id', $zone->get_id() ); $response = $this->get_item( $request ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $zone->get_id() ) ) ); return $response; } else { return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( "Resource cannot be created. Check to make sure 'order' and 'name' are present.", 'woocommerce' ), array( 'status' => 500 ) ); } } /** * Update a single Shipping Zone. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { $zone = $this->get_zone( $request->get_param( 'id' ) ); if ( is_wp_error( $zone ) ) { return $zone; } if ( 0 === $zone->get_id() ) { return new WP_Error( 'woocommerce_rest_shipping_zone_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); } $zone_changed = false; if ( ! is_null( $request->get_param( 'name' ) ) ) { $zone->set_zone_name( $request->get_param( 'name' ) ); $zone_changed = true; } if ( ! is_null( $request->get_param( 'order' ) ) ) { $zone->set_zone_order( $request->get_param( 'order' ) ); $zone_changed = true; } if ( $zone_changed ) { $zone->save(); } return $this->get_item( $request ); } /** * Delete a single Shipping Zone. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function delete_item( $request ) { $zone = $this->get_zone( $request->get_param( 'id' ) ); if ( is_wp_error( $zone ) ) { return $zone; } $force = $request['force']; $response = $this->get_item( $request ); if ( $force ) { $zone->delete(); } else { return new WP_Error( 'rest_trash_not_supported', __( 'Shipping zones do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } return $response; } /** * Prepare the Shipping Zone for the REST response. * * @param array $item Shipping Zone. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item['id'], 'name' => $item['zone_name'], 'order' => (int) $item['zone_order'], ); $context = empty( $request['context'] ) ? 'view' : $request['context']; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $data['id'] ) ); return $response; } /** * Prepare links for the request. * * @param int $zone_id Given Shipping Zone ID. * @return array Links for the given Shipping Zone. */ protected function prepare_links( $zone_id ) { $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $zone_id ), ), 'collection' => array( 'href' => rest_url( $base ), ), 'describedby' => array( 'href' => rest_url( trailingslashit( $base ) . $zone_id . '/locations' ), ), ); return $links; } /** * Get the Shipping Zones schema, conforming to JSON Schema * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'shipping_zone', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Shipping zone name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'order' => array( 'description' => __( 'Shipping zone order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php 0000644 00000012764 15132754523 0024301 0 ustar 00 <?php /** * REST API Order Notes controller * * Handles requests to the /orders/<order_id>/notes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Order Notes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Order_Notes_V1_Controller */ class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Get order notes from an order. * * @param WP_REST_Request $request Request data. * * @return array|WP_Error */ public function get_items( $request ) { $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $args = array( 'post_id' => $order->get_id(), 'approve' => 'approve', 'type' => 'order_note', ); // Allow filter by order note type. if ( 'customer' === $request['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'value' => 1, 'compare' => '=', ), ); } elseif ( 'internal' === $request['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'compare' => 'NOT EXISTS', ), ); } remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $data = array(); foreach ( $notes as $note ) { $order_note = $this->prepare_item_for_response( $note, $request ); $order_note = $this->prepare_response_for_collection( $order_note ); $data[] = $order_note; } return rest_ensure_response( $data ); } /** * Prepare a single order note output for response. * * @param WP_Comment $note Order note object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => (int) $note->comment_ID, 'date_created' => wc_rest_prepare_date_response( $note->comment_date ), 'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $note ) ); /** * Filter order note object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $note Order note object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); } /** * Get the Order Notes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'order_note', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'note' => array( 'description' => __( 'Order note content.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_note' => array( 'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['type'] = array( 'default' => 'any', 'description' => __( 'Limit result to customers or internal notes.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'any', 'customer', 'internal' ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php 0000644 00000014613 15132754523 0025175 0 ustar 00 <?php /** * REST API Product Reviews Controller * * Handles requests to /products/<product_id>/reviews. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Reviews Controller Class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Reviews_V1_Controller */ class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews'; /** * Register the routes for product reviews. */ public function register_routes() { parent::register_routes(); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check if a given request has access to batch manage product reviews. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Prepare a single product review output for response. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $review, $request ) { $data = array( 'id' => (int) $review->comment_ID, 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), 'review' => $review->comment_content, 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), 'name' => $review->comment_author, 'email' => $review->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $review, $request ) ); /** * Filter product reviews object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); } /** * Bulk create, update and delete items. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array Of WP_Error or WP_REST_Response. */ public function batch_items( $request ) { $items = array_filter( $request->get_params() ); $params = $request->get_url_params(); $product_id = $params['product_id']; $body_params = array(); foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { if ( ! empty( $items[ $batch_type ] ) ) { $injected_items = array(); foreach ( $items[ $batch_type ] as $item ) { $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; } $body_params[ $batch_type ] = $injected_items; } } $request = new WP_REST_Request( $request->get_method() ); $request->set_body_params( $body_params ); return parent::batch_items( $request ); } /** * Get the Product Review's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_review', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'review' => array( 'description' => __( 'The content of the review.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_created_gmt' => array( 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'rating' => array( 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Reviewer name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Reviewer email.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'verified' => array( 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php 0000644 00000001013 15132754523 0025603 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/top_sellers endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Report Top Sellers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Report_Top_Sellers_V1_Controller */ class WC_REST_Report_Top_Sellers_V2_Controller extends WC_REST_Report_Top_Sellers_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php 0000644 00000015432 15132754523 0025315 0 ustar 00 <?php /** * REST API WC Shipping Methods controller * * Handles requests to the /shipping_methods endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Shipping methods controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Shipping_Methods_V2_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'shipping_methods'; /** * Register the route for /shipping_methods and /shipping_methods/<method> */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to view shipping methods. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a shipping method. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get shipping methods. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $wc_shipping = WC_Shipping::instance(); $response = array(); foreach ( $wc_shipping->get_shipping_methods() as $id => $shipping_method ) { $method = $this->prepare_item_for_response( $shipping_method, $request ); $method = $this->prepare_response_for_collection( $method ); $response[] = $method; } return rest_ensure_response( $response ); } /** * Get a single Shipping Method. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { $wc_shipping = WC_Shipping::instance(); $methods = $wc_shipping->get_shipping_methods(); if ( empty( $methods[ $request['id'] ] ) ) { return new WP_Error( 'woocommerce_rest_shipping_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } $method = $methods[ $request['id'] ]; $response = $this->prepare_item_for_response( $method, $request ); return rest_ensure_response( $response ); } /** * Prepare a shipping method for response. * * @param WC_Shipping_Method $method Shipping method object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $method, $request ) { $data = array( 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $method, $request ) ); /** * Filter shipping methods object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Shipping_Method $method Shipping method object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_shipping_method', $response, $method, $request ); } /** * Prepare links for the request. * * @param WC_Shipping_Method $method Shipping method object. * @param WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $method, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $method->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the shipping method schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'shipping_method', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'title' => array( 'description' => __( 'Shipping method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Shipping method description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get any query params needed. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php 0000644 00000042367 15132754523 0025210 0 ustar 00 <?php /** * REST API Setting Options controller * * Handles requests to the /settings/$group/$setting endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Setting Options controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { /** * WP REST API namespace/version. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'settings/(?P<group_id>[\w-]+)'; /** * Register routes. * * @since 3.0.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'group' => array( 'description' => __( 'Settings group ID.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 'args' => array( 'group' => array( 'description' => __( 'Settings group ID.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array( 'args' => array( 'group' => array( 'description' => __( 'Settings group ID.', 'woocommerce' ), 'type' => 'string', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Return a single setting. * * @since 3.0.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $setting = $this->get_setting( $request['group_id'], $request['id'] ); if ( is_wp_error( $setting ) ) { return $setting; } $response = $this->prepare_item_for_response( $setting, $request ); return rest_ensure_response( $response ); } /** * Return all settings in a group. * * @since 3.0.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $settings = $this->get_group_settings( $request['group_id'] ); if ( is_wp_error( $settings ) ) { return $settings; } $data = array(); foreach ( $settings as $setting_obj ) { $setting = $this->prepare_item_for_response( $setting_obj, $request ); $setting = $this->prepare_response_for_collection( $setting ); if ( $this->is_setting_type_valid( $setting['type'] ) ) { $data[] = $setting; } } return rest_ensure_response( $data ); } /** * Get all settings in a group. * * @since 3.0.0 * @param string $group_id Group ID. * @return array|WP_Error */ public function get_group_settings( $group_id ) { if ( empty( $group_id ) ) { return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); } $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); if ( empty( $settings ) ) { return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); } $filtered_settings = array(); foreach ( $settings as $setting ) { $option_key = $setting['option_key']; $setting = $this->filter_setting( $setting ); $default = isset( $setting['default'] ) ? $setting['default'] : ''; // Get the option value. if ( is_array( $option_key ) ) { $option = get_option( $option_key[0] ); $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default; } else { $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default ); $setting['value'] = $admin_setting_value; } if ( 'multi_select_countries' === $setting['type'] ) { $setting['options'] = WC()->countries->get_countries(); $setting['type'] = 'multiselect'; } elseif ( 'single_select_country' === $setting['type'] ) { $setting['type'] = 'select'; $setting['options'] = $this->get_countries_and_states(); } $filtered_settings[] = $setting; } return $filtered_settings; } /** * Returns a list of countries and states for use in the base location setting. * * @since 3.0.7 * @return array Array of states and countries. */ private function get_countries_and_states() { $countries = WC()->countries->get_countries(); if ( ! $countries ) { return array(); } $output = array(); foreach ( $countries as $key => $value ) { $states = WC()->countries->get_states( $key ); if ( $states ) { foreach ( $states as $state_key => $state_value ) { $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; } } else { $output[ $key ] = $value; } } return $output; } /** * Get setting data. * * @since 3.0.0 * @param string $group_id Group ID. * @param string $setting_id Setting ID. * @return stdClass|WP_Error */ public function get_setting( $group_id, $setting_id ) { if ( empty( $setting_id ) ) { return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); } $settings = $this->get_group_settings( $group_id ); if ( is_wp_error( $settings ) ) { return $settings; } $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id ); if ( empty( $array_key ) ) { return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); } $setting = $settings[ $array_key[0] ]; if ( ! $this->is_setting_type_valid( $setting['type'] ) ) { return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); } return $setting; } /** * Bulk create, update and delete items. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array Of WP_Error or WP_REST_Response. */ public function batch_items( $request ) { // Get the request params. $items = array_filter( $request->get_params() ); /* * Since our batch settings update is group-specific and matches based on the route, * we inject the URL parameters (containing group) into the batch items */ if ( ! empty( $items['update'] ) ) { $to_update = array(); foreach ( $items['update'] as $item ) { $to_update[] = array_merge( $request->get_url_params(), $item ); } $request = new WP_REST_Request( $request->get_method() ); $request->set_body_params( array( 'update' => $to_update ) ); } return parent::batch_items( $request ); } /** * Update a single setting in a group. * * @since 3.0.0 * @param WP_REST_Request $request Request data. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $setting = $this->get_setting( $request['group_id'], $request['id'] ); if ( is_wp_error( $setting ) ) { return $setting; } if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) { $value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting ); } else { $value = $this->validate_setting_text_field( $request['value'], $setting ); } if ( is_wp_error( $value ) ) { return $value; } if ( is_array( $setting['option_key'] ) ) { $setting['value'] = $value; $option_key = $setting['option_key']; $prev = get_option( $option_key[0] ); $prev[ $option_key[1] ] = $request['value']; update_option( $option_key[0], $prev ); } else { $update_data = array(); $update_data[ $setting['option_key'] ] = $value; $setting['value'] = $value; WC_Admin_Settings::save_fields( array( $setting ), $update_data ); } $response = $this->prepare_item_for_response( $setting, $request ); return rest_ensure_response( $response ); } /** * Prepare a single setting object for response. * * @since 3.0.0 * @param object $item Setting object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, $request ) { unset( $item['option_key'] ); $data = $this->filter_setting( $item ); $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) ); return $response; } /** * Prepare links for the request. * * @since 3.0.0 * @param string $setting_id Setting ID. * @param string $group_id Group ID. * @return array Links for the given setting. */ protected function prepare_links( $setting_id, $group_id ) { $base = str_replace( '(?P<group_id>[\w-]+)', $group_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), ); return $links; } /** * Makes sure the current user has access to READ the settings APIs. * * @since 3.0.0 * @param WP_REST_Request $request Full data about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Makes sure the current user has access to WRITE the settings APIs. * * @since 3.0.0 * @param WP_REST_Request $request Full data about the request. * @return WP_Error|boolean */ public function update_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Filters out bad values from the settings array/filter so we * only return known values via the API. * * @since 3.0.0 * @param array $setting Settings. * @return array */ public function filter_setting( $setting ) { $setting = array_intersect_key( $setting, array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) ) ); if ( empty( $setting['options'] ) ) { unset( $setting['options'] ); } if ( 'image_width' === $setting['type'] ) { $setting = $this->cast_image_width( $setting ); } return $setting; } /** * For image_width, Crop can return "0" instead of false -- so we want * to make sure we return these consistently the same we accept them. * * @todo remove in 4.0 * @since 3.0.0 * @param array $setting Settings. * @return array */ public function cast_image_width( $setting ) { foreach ( array( 'default', 'value' ) as $key ) { if ( isset( $setting[ $key ] ) ) { $setting[ $key ]['width'] = intval( $setting[ $key ]['width'] ); $setting[ $key ]['height'] = intval( $setting[ $key ]['height'] ); $setting[ $key ]['crop'] = (bool) $setting[ $key ]['crop']; } } return $setting; } /** * Callback for allowed keys for each setting response. * * @since 3.0.0 * @param string $key Key to check. * @return boolean */ public function allowed_setting_keys( $key ) { return in_array( $key, array( 'id', 'label', 'description', 'default', 'tip', 'placeholder', 'type', 'options', 'value', 'option_key', ) ); } /** * Boolean for if a setting type is a valid supported setting type. * * @since 3.0.0 * @param string $type Type. * @return bool */ public function is_setting_type_valid( $type ) { return in_array( $type, array( 'text', // Validates with validate_setting_text_field. 'email', // Validates with validate_setting_text_field. 'number', // Validates with validate_setting_text_field. 'color', // Validates with validate_setting_text_field. 'password', // Validates with validate_setting_text_field. 'textarea', // Validates with validate_setting_textarea_field. 'select', // Validates with validate_setting_select_field. 'multiselect', // Validates with validate_setting_multiselect_field. 'radio', // Validates with validate_setting_radio_field (-> validate_setting_select_field). 'checkbox', // Validates with validate_setting_checkbox_field. 'image_width', // Validates with validate_setting_image_width_field. 'thumbnail_cropping', // Validates with validate_setting_text_field. ) ); } /** * Get the settings schema, conforming to JSON Schema. * * @since 3.0.0 * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'setting', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Setting value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'default' => array( 'description' => __( 'Default value for the setting.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tip' => array( 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'placeholder' => array( 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Type of setting.', 'woocommerce' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), 'context' => array( 'view', 'edit' ), 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox', 'thumbnail_cropping' ), 'readonly' => true, ), 'options' => array( 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php 0000644 00000012247 15132754523 0026617 0 ustar 00 <?php /** * REST API Shipping Zone Locations controller * * Handles requests to the /shipping/zones/<id>/locations endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zone Locations class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zones_Controller_Base */ class WC_REST_Shipping_Zone_Locations_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base { /** * Register the routes for Shipping Zone Locations. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/locations', array( 'args' => array( 'id' => array( 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_items' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get all Shipping Zone Locations. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { $zone = $this->get_zone( (int) $request['id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $locations = $zone->get_zone_locations(); $data = array(); foreach ( $locations as $location_obj ) { $location = $this->prepare_item_for_response( $location_obj, $request ); $location = $this->prepare_response_for_collection( $location ); $data[] = $location; } return rest_ensure_response( $data ); } /** * Update all Shipping Zone Locations. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function update_items( $request ) { $zone = $this->get_zone( (int) $request['id'] ); if ( is_wp_error( $zone ) ) { return $zone; } if ( 0 === $zone->get_id() ) { return new WP_Error( 'woocommerce_rest_shipping_zone_locations_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); } $raw_locations = $request->get_json_params(); $locations = array(); foreach ( (array) $raw_locations as $raw_location ) { if ( empty( $raw_location['code'] ) ) { continue; } $type = ! empty( $raw_location['type'] ) ? sanitize_text_field( $raw_location['type'] ) : 'country'; if ( ! in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true ) ) { continue; } $locations[] = array( 'code' => sanitize_text_field( $raw_location['code'] ), 'type' => sanitize_text_field( $type ), ); } $zone->set_locations( $locations ); $zone->save(); return $this->get_items( $request ); } /** * Prepare the Shipping Zone Location for the REST response. * * @param array $item Shipping Zone Location. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $context = empty( $request['context'] ) ? 'view' : $request['context']; $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( (int) $request['id'] ) ); return $response; } /** * Prepare links for the request. * * @param int $zone_id Given Shipping Zone ID. * @return array Links for the given Shipping Zone Location. */ protected function prepare_links( $zone_id ) { $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; $links = array( 'collection' => array( 'href' => rest_url( $base . '/locations' ), ), 'describes' => array( 'href' => rest_url( $base ), ), ); return $links; } /** * Get the Shipping Zone Locations schema, conforming to JSON Schema * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'shipping_zone_location', 'type' => 'object', 'properties' => array( 'code' => array( 'description' => __( 'Shipping zone location code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'type' => array( 'description' => __( 'Shipping zone location type.', 'woocommerce' ), 'type' => 'string', 'default' => 'country', 'enum' => array( 'postcode', 'state', 'country', 'continent', ), 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php 0000644 00000001104 15132754523 0026633 0 ustar 00 <?php /** * REST API Product Attribute Terms controller * * Handles requests to the products/attributes/<attribute_id>/terms endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Attribute Terms controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Attribute_Terms_V1_Controller */ class WC_REST_Product_Attribute_Terms_V2_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php 0000644 00000036731 15132754523 0025337 0 ustar 00 <?php /** * REST API WC Payment gateways controller * * Handles requests to the /payment_gateways endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Paymenga gateways controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Payment_Gateways_V2_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'payment_gateways'; /** * Register the route for /payment_gateways and /payment_gateways/<id> */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to view payment gateways. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a payment gateway. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check whether a given request has permission to edit payment gateways. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get payment gateways. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $payment_gateways = WC()->payment_gateways->payment_gateways(); $response = array(); foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { $payment_gateway->id = $payment_gateway_id; $gateway = $this->prepare_item_for_response( $payment_gateway, $request ); $gateway = $this->prepare_response_for_collection( $gateway ); $response[] = $gateway; } return rest_ensure_response( $response ); } /** * Get a single payment gateway. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { $gateway = $this->get_gateway( $request ); if ( is_null( $gateway ) ) { return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } $gateway = $this->prepare_item_for_response( $gateway, $request ); return rest_ensure_response( $gateway ); } /** * Update A Single Payment Method. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function update_item( $request ) { $gateway = $this->get_gateway( $request ); if ( is_null( $gateway ) ) { return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } // Get settings. $gateway->init_form_fields(); $settings = $gateway->settings; // Update settings. if ( isset( $request['settings'] ) ) { $errors_found = false; foreach ( $gateway->form_fields as $key => $field ) { if ( isset( $request['settings'][ $key ] ) ) { if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); } else { $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); } if ( is_wp_error( $value ) ) { $errors_found = true; break; } $settings[ $key ] = $value; } } if ( $errors_found ) { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } } // Update if this method is enabled or not. if ( isset( $request['enabled'] ) ) { $settings['enabled'] = wc_bool_to_string( $request['enabled'] ); $gateway->enabled = $settings['enabled']; } // Update title. if ( isset( $request['title'] ) ) { $settings['title'] = $request['title']; $gateway->title = $settings['title']; } // Update description. if ( isset( $request['description'] ) ) { $settings['description'] = $request['description']; $gateway->description = $settings['description']; } // Update options. $gateway->settings = $settings; update_option( $gateway->get_option_key(), apply_filters( 'woocommerce_gateway_' . $gateway->id . '_settings_values', $settings, $gateway ) ); // Update order. if ( isset( $request['order'] ) ) { $order = (array) get_option( 'woocommerce_gateway_order' ); $order[ $gateway->id ] = $request['order']; update_option( 'woocommerce_gateway_order', $order ); $gateway->order = absint( $request['order'] ); } $gateway = $this->prepare_item_for_response( $gateway, $request ); return rest_ensure_response( $gateway ); } /** * Get a gateway based on the current request object. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|null */ public function get_gateway( $request ) { $gateway = null; $payment_gateways = WC()->payment_gateways->payment_gateways(); foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { if ( $request['id'] !== $payment_gateway_id ) { continue; } $payment_gateway->id = $payment_gateway_id; $gateway = $payment_gateway; } return $gateway; } /** * Prepare a payment gateway for response. * * @param WC_Payment_Gateway $gateway Payment gateway object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $gateway, $request ) { $order = (array) get_option( 'woocommerce_gateway_order' ); $item = array( 'id' => $gateway->id, 'title' => $gateway->title, 'description' => $gateway->description, 'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '', 'enabled' => ( 'yes' === $gateway->enabled ), 'method_title' => $gateway->get_method_title(), 'method_description' => $gateway->get_method_description(), 'settings' => $this->get_settings( $gateway ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $item, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $gateway, $request ) ); /** * Filter payment gateway objects returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Payment_Gateway $gateway Payment gateway object. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request ); } /** * Return settings associated with this payment gateway. * * @param WC_Payment_Gateway $gateway Gateway data. * * @return array */ public function get_settings( $gateway ) { $settings = array(); $gateway->init_form_fields(); foreach ( $gateway->form_fields as $id => $field ) { // Make sure we at least have a title and type. if ( empty( $field['title'] ) || empty( $field['type'] ) ) { continue; } // Ignore 'title' settings/fields -- they are UI only. if ( 'title' === $field['type'] ) { continue; } // Ignore 'enabled' and 'description' which get included elsewhere. if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { continue; } $data = array( 'id' => $id, 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], 'description' => empty( $field['description'] ) ? '' : $field['description'], 'type' => $field['type'], 'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ], 'default' => empty( $field['default'] ) ? '' : $field['default'], 'tip' => empty( $field['description'] ) ? '' : $field['description'], 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], ); if ( ! empty( $field['options'] ) ) { $data['options'] = $field['options']; } $settings[ $id ] = $data; } return $settings; } /** * Prepare links for the request. * * @param WC_Payment_Gateway $gateway Payment gateway object. * @param WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $gateway, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $gateway->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the payment gateway schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'payment_gateway', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Payment gateway ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'title' => array( 'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'order' => array( 'description' => __( 'Payment gateway sort order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'absint', ), ), 'enabled' => array( 'description' => __( 'Payment gateway enabled status.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), ), 'method_title' => array( 'description' => __( 'Payment gateway method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_description' => array( 'description' => __( 'Payment gateway method description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'settings' => array( 'description' => __( 'Payment gateway settings.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Type of setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Setting value.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'default' => array( 'description' => __( 'Default value for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tip' => array( 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'placeholder' => array( 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get any query params needed. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php 0000644 00000001026 15132754523 0025671 0 ustar 00 <?php /** * REST API Product Attributes controller * * Handles requests to the products/attributes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Attributes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Attributes_V1_Controller */ class WC_REST_Product_Attributes_V2_Controller extends WC_REST_Product_Attributes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php 0000644 00000045375 15132754523 0024623 0 ustar 00 <?php /** * REST API Order Refunds controller * * Handles requests to the /orders/<order_id>/refunds endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Order Refunds controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Orders_V2_Controller */ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds'; /** * Post type. * * @var string */ protected $post_type = 'shop_order_refund'; /** * Stores the request. * * @var array */ protected $request = array(); /** * Order refunds actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' ); } /** * Register the routes for order refunds. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => true, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get object. * * @since 3.0.0 * @param int $id Object ID. * @return WC_Data */ protected function get_object( $id ) { return wc_get_order( $id ); } /** * Get formatted item data. * * @since 3.0.0 * @param WC_Data $object WC_Data instance. * @return array */ protected function get_formatted_item_data( $object ) { $data = $object->get_data(); $format_decimal = array( 'amount' ); $format_date = array( 'date_created' ); $format_line_items = array( 'line_items', 'shipping_lines', 'tax_lines', 'fee_lines' ); // Format decimal values. foreach ( $format_decimal as $key ) { $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); } // Format date values. foreach ( $format_date as $key ) { $datetime = $data[ $key ]; $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); } // Format line items. foreach ( $format_line_items as $key ) { $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); } return array( 'id' => $object->get_id(), 'date_created' => $data['date_created'], 'date_created_gmt' => $data['date_created_gmt'], 'amount' => $data['amount'], 'reason' => $data['reason'], 'refunded_by' => $data['refunded_by'], 'refunded_payment' => $data['refunded_payment'], 'meta_data' => $data['meta_data'], 'line_items' => $data['line_items'], 'shipping_lines' => $data['shipping_lines'], 'tax_lines' => $data['tax_lines'], 'fee_lines' => $data['fee_lines'], ); } /** * Prepare a single order output for response. * * @since 3.0.0 * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * * @return WP_Error|WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $this->request = $request; $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order ) { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } if ( ! $object || $object->get_parent_id() !== $order->get_id() ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); } $data = $this->get_formatted_item_data( $object ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare links for the request. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return array Links for the given post. */ protected function prepare_links( $object, $request ) { $base = str_replace( '(?P<order_id>[\d]+)', $object->get_parent_id(), $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), ), ); return $links; } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { $args = parent::prepare_objects_query( $request ); $args['post_status'] = array_keys( wc_get_order_statuses() ); $args['post_parent__in'] = array( absint( $request['order_id'] ) ); return $args; } /** * Prepares one object for create or update operation. * * @since 3.0.0 * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. */ protected function prepare_object_for_database( $request, $creating = false ) { $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order ) { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } if ( 0 > $request['amount'] ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); } // Create the refund. $refund = wc_create_refund( array( 'order_id' => $order->get_id(), 'amount' => $request['amount'], 'reason' => empty( $request['reason'] ) ? null : $request['reason'], 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, 'restock_items' => true, ) ); if ( is_wp_error( $refund ) ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); } if ( ! $refund ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } $refund->save_meta_data(); } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $coupon Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); } /** * Save an object data. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @param bool $creating If is creating a new object. * @return WC_Data|WP_Error */ protected function save_object( $request, $creating = false ) { try { $object = $this->prepare_object_for_database( $request, $creating ); if ( is_wp_error( $object ) ) { return $object; } return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the refund schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the order refund was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'amount' => array( 'description' => __( 'Refund amount.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'reason' => array( 'description' => __( 'Reason for refund.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'refunded_by' => array( 'description' => __( 'User ID of user who created the refund.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'refunded_payment' => array( 'description' => __( 'If the payment was refunded via the API.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'api_refund' => array( 'description' => __( 'When true, the payment gateway API is used to generate the refund.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'edit' ), 'default' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); unset( $params['status'], $params['customer'], $params['product'] ); return $params; } } includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php 0000644 00000000710 15132754523 0023150 0 ustar 00 <?php /** * REST API Taxes controller * * Handles requests to the /taxes endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Taxes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Taxes_V1_Controller */ class WC_REST_Taxes_V2_Controller extends WC_REST_Taxes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php 0000644 00000045157 15132754523 0023530 0 ustar 00 <?php /** * REST API Coupons controller * * Handles requests to the /coupons endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Coupons controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Coupons_V2_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'coupons'; /** * Post type. * * @var string */ protected $post_type = 'shop_coupon'; /** * Register the routes for coupons. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'required' => true, 'type' => 'string', ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get object. * * @since 3.0.0 * @param int $id Object ID. * @return WC_Data */ protected function get_object( $id ) { return new WC_Coupon( $id ); } /** * Get formatted item data. * * @since 3.0.0 * @param WC_Data $object WC_Data instance. * @return array */ protected function get_formatted_item_data( $object ) { $data = $object->get_data(); $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); $format_date = array( 'date_created', 'date_modified', 'date_expires' ); $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); // Format decimal values. foreach ( $format_decimal as $key ) { $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); } // Format date values. foreach ( $format_date as $key ) { $datetime = $data[ $key ]; $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); } // Format null values. foreach ( $format_null as $key ) { $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; } return array( 'id' => $object->get_id(), 'code' => $data['code'], 'amount' => $data['amount'], 'date_created' => $data['date_created'], 'date_created_gmt' => $data['date_created_gmt'], 'date_modified' => $data['date_modified'], 'date_modified_gmt' => $data['date_modified_gmt'], 'discount_type' => $data['discount_type'], 'description' => $data['description'], 'date_expires' => $data['date_expires'], 'date_expires_gmt' => $data['date_expires_gmt'], 'usage_count' => $data['usage_count'], 'individual_use' => $data['individual_use'], 'product_ids' => $data['product_ids'], 'excluded_product_ids' => $data['excluded_product_ids'], 'usage_limit' => $data['usage_limit'], 'usage_limit_per_user' => $data['usage_limit_per_user'], 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], 'free_shipping' => $data['free_shipping'], 'product_categories' => $data['product_categories'], 'excluded_product_categories' => $data['excluded_product_categories'], 'exclude_sale_items' => $data['exclude_sale_items'], 'minimum_amount' => $data['minimum_amount'], 'maximum_amount' => $data['maximum_amount'], 'email_restrictions' => $data['email_restrictions'], 'used_by' => $data['used_by'], 'meta_data' => $data['meta_data'], ); } /** * Prepare a single coupon output for response. * * @since 3.0.0 * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $data = $this->get_formatted_item_data( $object ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { $args = parent::prepare_objects_query( $request ); if ( ! empty( $request['code'] ) ) { $id = wc_get_coupon_id_by_code( $request['code'] ); $args['post__in'] = array( $id ); } // Get only ids. $args['fields'] = 'ids'; return $args; } /** * Only return writable props from schema. * * @param array $schema Schema. * @return bool */ protected function filter_writable_props( $schema ) { return empty( $schema['readonly'] ); } /** * Prepare a single coupon for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $coupon = new WC_Coupon( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Validate required POST fields. if ( $creating && empty( $request['code'] ) ) { return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); } // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'code': $coupon_code = wc_format_coupon_code( $value ); $id = $coupon->get_id() ? $coupon->get_id() : 0; $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); } $coupon->set_code( $coupon_code ); break; case 'meta_data': if ( is_array( $value ) ) { foreach ( $value as $meta ) { $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; case 'description': $coupon->set_description( wp_filter_post_kses( $value ) ); break; default: if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { $coupon->{"set_{$key}"}( $value ); } break; } } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $coupon Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); } /** * Get the Coupon's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'amount' => array( 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_type' => array( 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), 'type' => 'string', 'default' => 'fixed_cart', 'enum' => array_keys( wc_get_coupon_types() ), 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Coupon description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_expires' => array( 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_expires_gmt' => array( 'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'usage_count' => array( 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'individual_use' => array( 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_ids' => array( 'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'excluded_product_ids' => array( 'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'usage_limit' => array( 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'usage_limit_per_user' => array( 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'limit_usage_to_x_items' => array( 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'free_shipping' => array( 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_categories' => array( 'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'excluded_product_categories' => array( 'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'exclude_sale_items' => array( 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'minimum_amount' => array( 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'maximum_amount' => array( 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email_restrictions' => array( 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ), 'used_by' => array( 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['code'] = array( 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php 0000644 00000042503 15132754523 0026265 0 ustar 00 <?php /** * REST API Shipping Zone Methods controller * * Handles requests to the /shipping/zones/<id>/methods endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Shipping Zone Methods class. * * @package WooCommerce\RestApi * @extends WC_REST_Shipping_Zones_Controller_Base */ class WC_REST_Shipping_Zone_Methods_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base { /** * Register the routes for Shipping Zone Methods. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods', array( 'args' => array( 'zone_id' => array( 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'method_id' => array( 'required' => true, 'readonly' => false, 'description' => __( 'Shipping method ID.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods/(?P<instance_id>[\d]+)', array( 'args' => array( 'zone_id' => array( 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), 'type' => 'integer', ), 'instance_id' => array( 'description' => __( 'Unique ID for the instance.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_items_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get a single Shipping Zone Method. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { $zone = $this->get_zone( $request['zone_id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $instance_id = (int) $request['instance_id']; $methods = $zone->get_shipping_methods(); $method = false; foreach ( $methods as $method_obj ) { if ( $instance_id === $method_obj->instance_id ) { $method = $method_obj; break; } } if ( false === $method ) { return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = $this->prepare_item_for_response( $method, $request ); return rest_ensure_response( $data ); } /** * Get all Shipping Zone Methods. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { $zone = $this->get_zone( $request['zone_id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $methods = $zone->get_shipping_methods(); $data = array(); foreach ( $methods as $method_obj ) { $method = $this->prepare_item_for_response( $method_obj, $request ); $data[] = $method; } return rest_ensure_response( $data ); } /** * Create a new shipping zone method instance. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { $method_id = $request['method_id']; $zone = $this->get_zone( $request['zone_id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $instance_id = $zone->add_shipping_method( $method_id ); $methods = $zone->get_shipping_methods(); $method = false; foreach ( $methods as $method_obj ) { if ( $instance_id === $method_obj->instance_id ) { $method = $method_obj; break; } } if ( false === $method ) { return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( 'Resource cannot be created.', 'woocommerce' ), array( 'status' => 500 ) ); } $method = $this->update_fields( $instance_id, $method, $request ); if ( is_wp_error( $method ) ) { return $method; } $data = $this->prepare_item_for_response( $method, $request ); return rest_ensure_response( $data ); } /** * Delete a shipping method instance. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item( $request ) { $zone = $this->get_zone( $request['zone_id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $instance_id = (int) $request['instance_id']; $force = $request['force']; $methods = $zone->get_shipping_methods(); $method = false; foreach ( $methods as $method_obj ) { if ( $instance_id === $method_obj->instance_id ) { $method = $method_obj; break; } } if ( false === $method ) { return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } $method = $this->update_fields( $instance_id, $method, $request ); if ( is_wp_error( $method ) ) { return $method; } $request->set_param( 'context', 'view' ); $response = $this->prepare_item_for_response( $method, $request ); // Actually delete. if ( $force ) { $zone->delete_shipping_method( $instance_id ); } else { return new WP_Error( 'rest_trash_not_supported', __( 'Shipping methods do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } /** * Fires after a product review is deleted via the REST API. * * @param object $method * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'rest_delete_product_review', $method, $response, $request ); return $response; } /** * Update A Single Shipping Zone Method. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function update_item( $request ) { $zone = $this->get_zone( $request['zone_id'] ); if ( is_wp_error( $zone ) ) { return $zone; } $instance_id = (int) $request['instance_id']; $methods = $zone->get_shipping_methods(); $method = false; foreach ( $methods as $method_obj ) { if ( $instance_id === $method_obj->instance_id ) { $method = $method_obj; break; } } if ( false === $method ) { return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } $method = $this->update_fields( $instance_id, $method, $request ); if ( is_wp_error( $method ) ) { return $method; } $data = $this->prepare_item_for_response( $method, $request ); return rest_ensure_response( $data ); } /** * Updates settings, order, and enabled status on create. * * @param int $instance_id Instance ID. * @param WC_Shipping_Method $method Shipping method data. * @param WP_REST_Request $request Request data. * * @return WC_Shipping_Method */ public function update_fields( $instance_id, $method, $request ) { global $wpdb; // Update settings if present. if ( isset( $request['settings'] ) ) { $method->init_instance_settings(); $instance_settings = $method->instance_settings; $errors_found = false; foreach ( $method->get_instance_form_fields() as $key => $field ) { if ( isset( $request['settings'][ $key ] ) ) { if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); } else { $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); } if ( is_wp_error( $value ) ) { $errors_found = true; break; } $instance_settings[ $key ] = $value; } } if ( $errors_found ) { return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); } update_option( $method->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $method->id . '_instance_settings_values', $instance_settings, $method ) ); } // Update order. if ( isset( $request['order'] ) ) { $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $request['order'] ) ), array( 'instance_id' => absint( $instance_id ) ) ); $method->method_order = absint( $request['order'] ); } // Update if this method is enabled or not. if ( isset( $request['enabled'] ) ) { if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $request['enabled'] ), array( 'instance_id' => absint( $instance_id ) ) ) ) { do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $request['zone_id'], $request['enabled'] ); $method->enabled = ( true === $request['enabled'] ? 'yes' : 'no' ); } } return $method; } /** * Prepare the Shipping Zone Method for the REST response. * * @param array $item Shipping Zone Method. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $method = array( 'id' => $item->instance_id, 'instance_id' => $item->instance_id, 'title' => $item->instance_settings['title'], 'order' => $item->method_order, 'enabled' => ( 'yes' === $item->enabled ), 'method_id' => $item->id, 'method_title' => $item->method_title, 'method_description' => $item->method_description, 'settings' => $this->get_settings( $item ), ); $context = empty( $request['context'] ) ? 'view' : $request['context']; $data = $this->add_additional_fields_to_object( $method, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $request['zone_id'], $item->instance_id ) ); $response = $this->prepare_response_for_collection( $response ); return $response; } /** * Return settings associated with this shipping zone method instance. * * @param WC_Shipping_Method $item Shipping method data. * * @return array */ public function get_settings( $item ) { $item->init_instance_settings(); $settings = array(); foreach ( $item->get_instance_form_fields() as $id => $field ) { $data = array( 'id' => $id, 'label' => $field['title'], 'description' => empty( $field['description'] ) ? '' : $field['description'], 'type' => $field['type'], 'value' => $item->instance_settings[ $id ], 'default' => empty( $field['default'] ) ? '' : $field['default'], 'tip' => empty( $field['description'] ) ? '' : $field['description'], 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], ); if ( ! empty( $field['options'] ) ) { $data['options'] = $field['options']; } $settings[ $id ] = $data; } return $settings; } /** * Prepare links for the request. * * @param int $zone_id Given Shipping Zone ID. * @param int $instance_id Given Shipping Zone Method Instance ID. * @return array Links for the given Shipping Zone Method. */ protected function prepare_links( $zone_id, $instance_id ) { $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; $links = array( 'self' => array( 'href' => rest_url( $base . '/methods/' . $instance_id ), ), 'collection' => array( 'href' => rest_url( $base . '/methods' ), ), 'describes' => array( 'href' => rest_url( $base ), ), ); return $links; } /** * Get the Shipping Zone Methods schema, conforming to JSON Schema * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'shipping_zone_method', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'instance_id' => array( 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'title' => array( 'description' => __( 'Shipping method customer facing title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'order' => array( 'description' => __( 'Shipping method sort order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'enabled' => array( 'description' => __( 'Shipping method enabled status.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), ), 'method_id' => array( 'description' => __( 'Shipping method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_title' => array( 'description' => __( 'Shipping method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_description' => array( 'description' => __( 'Shipping method description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'settings' => array( 'description' => __( 'Shipping method settings.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Type of setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Setting value.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'default' => array( 'description' => __( 'Default value for the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tip' => array( 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'placeholder' => array( 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php 0000644 00000012675 15132754523 0025672 0 ustar 00 <?php /** * REST API Customer Downloads controller * * Handles requests to the /customers/<customer_id>/downloads endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Customer_Downloads_V1_Controller */ class WC_REST_Customer_Downloads_V2_Controller extends WC_REST_Customer_Downloads_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Prepare a single download output for response. * * @param stdClass $download Download object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $download, $request ) { $data = array( 'download_id' => $download->download_id, 'download_url' => $download->download_url, 'product_id' => $download->product_id, 'product_name' => $download->product_name, 'download_name' => $download->download_name, 'order_id' => $download->order_id, 'order_key' => $download->order_key, 'downloads_remaining' => '' === $download->downloads_remaining ? 'unlimited' : $download->downloads_remaining, 'access_expires' => $download->access_expires ? wc_rest_prepare_date_response( $download->access_expires ) : 'never', 'access_expires_gmt' => $download->access_expires ? wc_rest_prepare_date_response( get_gmt_from_date( $download->access_expires ) ) : 'never', 'file' => $download->file, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $download, $request ) ); /** * Filter customer download data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $download Download object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); } /** * Get the Customer Download's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer_download', 'type' => 'object', 'properties' => array( 'download_id' => array( 'description' => __( 'Download ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'download_url' => array( 'description' => __( 'Download file URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Downloadable product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'product_name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'download_name' => array( 'description' => __( 'Downloadable file name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'order_id' => array( 'description' => __( 'Order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'downloads_remaining' => array( 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'access_expires' => array( 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'access_expires_gmt' => array( 'description' => __( 'The date when download access expires, as GMT.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File details.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php 0000644 00000000755 15132754523 0024455 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/sales endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Report Sales controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Report_Sales_V1_Controller */ class WC_REST_Report_Sales_V2_Controller extends WC_REST_Report_Sales_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php 0000644 00000000762 15132754523 0024447 0 ustar 00 <?php /** * REST API Product Tags controller * * Handles requests to the products/tags endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Tags controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Tags_V1_Controller */ class WC_REST_Product_Tags_V2_Controller extends WC_REST_Product_Tags_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php 0000644 00000105277 15132754523 0025677 0 ustar 00 <?php /** * REST API variations controller * * Handles requests to the /products/<product_id>/variations endpoints. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * REST API variations controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Products_V2_Controller */ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'products/(?P<product_id>[\d]+)/variations'; /** * Post type. * * @var string */ protected $post_type = 'product_variation'; /** * Register the routes for products. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get object. * * @since 3.0.0 * @param int $id Object ID. * @return WC_Data */ protected function get_object( $id ) { return wc_get_product( $id ); } /** * Check if a given request has access to update an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } // Check if variation belongs to the correct parent product. if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Prepare a single variation output for response. * * @since 3.0.0 * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_object_for_response( $object, $request ) { $data = array( 'id' => $object->get_id(), 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), 'description' => wc_format_content( $object->get_description() ), 'permalink' => $object->get_permalink(), 'sku' => $object->get_sku(), 'price' => $object->get_price(), 'regular_price' => $object->get_regular_price(), 'sale_price' => $object->get_sale_price(), 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), 'on_sale' => $object->is_on_sale(), 'visible' => $object->is_visible(), 'purchasable' => $object->is_purchasable(), 'virtual' => $object->is_virtual(), 'downloadable' => $object->is_downloadable(), 'downloads' => $this->get_downloads( $object ), 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, 'tax_status' => $object->get_tax_status(), 'tax_class' => $object->get_tax_class(), 'manage_stock' => $object->managing_stock(), 'stock_quantity' => $object->get_stock_quantity(), 'in_stock' => $object->is_in_stock(), 'backorders' => $object->get_backorders(), 'backorders_allowed' => $object->backorders_allowed(), 'backordered' => $object->is_on_backorder(), 'weight' => $object->get_weight(), 'dimensions' => array( 'length' => $object->get_length(), 'width' => $object->get_width(), 'height' => $object->get_height(), ), 'shipping_class' => $object->get_shipping_class(), 'shipping_class_id' => $object->get_shipping_class_id(), 'image' => current( $this->get_images( $object ) ), 'attributes' => $this->get_attributes( $object ), 'menu_order' => $object->get_menu_order(), 'meta_data' => $object->get_meta_data(), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, * refers to object type being prepared for the response. * * @param WP_REST_Response $response The response object. * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare objects query. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_objects_query( $request ) { $args = parent::prepare_objects_query( $request ); $args['post_parent'] = $request['product_id']; return $args; } /** * Prepare a single variation for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. * @return WP_Error|WC_Data */ protected function prepare_object_for_database( $request, $creating = false ) { if ( isset( $request['id'] ) ) { $variation = wc_get_product( absint( $request['id'] ) ); } else { $variation = new WC_Product_Variation(); } // Update parent ID just once. if ( 0 === $variation->get_parent_id() ) { $variation->set_parent_id( absint( $request['product_id'] ) ); } // Status. if ( isset( $request['visible'] ) ) { $variation->set_status( false === $request['visible'] ? 'private' : 'publish' ); } // SKU. if ( isset( $request['sku'] ) ) { $variation->set_sku( wc_clean( $request['sku'] ) ); } // Thumbnail. if ( isset( $request['image'] ) ) { if ( is_array( $request['image'] ) && ! empty( $request['image'] ) ) { $image = $request['image']; if ( is_array( $image ) ) { $image['position'] = 0; } $variation = $this->set_product_images( $variation, array( $image ) ); } else { $variation->set_image_id( '' ); } } // Virtual variation. if ( isset( $request['virtual'] ) ) { $variation->set_virtual( $request['virtual'] ); } // Downloadable variation. if ( isset( $request['downloadable'] ) ) { $variation->set_downloadable( $request['downloadable'] ); } // Downloads. if ( $variation->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $variation->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $variation->set_download_expiry( $request['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $request ); // Stock handling. if ( isset( $request['manage_stock'] ) ) { if ( 'parent' === $request['manage_stock'] ) { $variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does. } else { $variation->set_manage_stock( wc_string_to_bool( $request['manage_stock'] ) ); } } if ( isset( $request['in_stock'] ) ) { $variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' ); } if ( isset( $request['backorders'] ) ) { $variation->set_backorders( $request['backorders'] ); } if ( $variation->get_manage_stock() ) { if ( isset( $request['stock_quantity'] ) ) { $variation->set_stock_quantity( $request['stock_quantity'] ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $variation->set_stock_quantity( $stock_quantity ); } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); } // Regular Price. if ( isset( $request['regular_price'] ) ) { $variation->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $variation->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_from_gmt'] ) ) { $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); } if ( isset( $request['date_on_sale_to'] ) ) { $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); } if ( isset( $request['date_on_sale_to_gmt'] ) ) { $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); } // Tax class. if ( isset( $request['tax_class'] ) ) { $variation->set_tax_class( $request['tax_class'] ); } // Description. if ( isset( $request['description'] ) ) { $variation->set_description( wp_kses_post( $request['description'] ) ); } // Update taxonomies. if ( isset( $request['attributes'] ) ) { $attributes = array(); $parent = wc_get_product( $variation->get_parent_id() ); $parent_attributes = $parent->get_attributes(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $raw_attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $raw_attribute_name ) { continue; } $attribute_name = sanitize_title( $raw_attribute_name ); if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { continue; } $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine if ( $term && ! is_wp_error( $term ) ) { $attribute_value = $term->slug; } else { $attribute_value = sanitize_title( $attribute_value ); } } $attributes[ $attribute_key ] = $attribute_value; } $variation->set_attributes( $attributes ); } // Menu order. if ( $request['menu_order'] ) { $variation->set_menu_order( $request['menu_order'] ); } // Meta data. if ( is_array( $request['meta_data'] ) ) { foreach ( $request['meta_data'] as $meta ) { $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } /** * Filters an object before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, * refers to the object type slug. * * @param WC_Data $variation Object object. * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); } /** * Clear caches here so in sync with any new variations. * * @param WC_Data $object Object data. */ public function clear_transients( $object ) { wc_delete_product_transients( $object->get_parent_id() ); wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' ); } /** * Delete a variation. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error|WP_REST_Response */ public function delete_item( $request ) { $force = (bool) $request['force']; $object = $this->get_object( (int) $request['id'] ); $result = false; if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404, ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); /** * Filter whether an object is trashable. * * Return false to disable trash support for the object. * * @param boolean $supports_trash Whether the object type support trashing. * @param WC_Data $object The object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { return new WP_Error( /* translators: %s: post type */ "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code(), ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_object_for_response( $object, $request ); // If we're forcing, then delete permanently. if ( $force ) { $object->delete( true ); $result = 0 === $object->get_id(); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { return new WP_Error( /* translators: %s: post type */ 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501, ) ); } // Otherwise, only trash if we haven't already. if ( is_callable( array( $object, 'get_status' ) ) ) { if ( 'trash' === $object->get_status() ) { return new WP_Error( /* translators: %s: post type */ 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410, ) ); } $object->delete(); $result = 'trash' === $object->get_status(); } } if ( ! $result ) { return new WP_Error( /* translators: %s: post type */ 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500, ) ); } // Delete parent product transients. if ( 0 !== $object->get_parent_id() ) { wc_delete_product_transients( $object->get_parent_id() ); } /** * Fires after a single object is deleted or trashed via the REST API. * * @param WC_Data $object The deleted or trashed object. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); return $response; } /** * Bulk create, update and delete items. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return array Of WP_Error or WP_REST_Response. */ public function batch_items( $request ) { $items = array_filter( $request->get_params() ); $params = $request->get_url_params(); $query = $request->get_query_params(); $product_id = $params['product_id']; $body_params = array(); foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { if ( ! empty( $items[ $batch_type ] ) ) { $injected_items = array(); foreach ( $items[ $batch_type ] as $item ) { $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id, ), $item ) : $item; } $body_params[ $batch_type ] = $injected_items; } } $request = new WP_REST_Request( $request->get_method() ); $request->set_body_params( $body_params ); $request->set_query_params( $query ); return parent::batch_items( $request ); } /** * Prepare links for the request. * * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. * @return array Links for the given post. */ protected function prepare_links( $object, $request ) { $product_id = (int) $request['product_id']; $base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), ), ); return $links; } /** * Get the Variation's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Variation description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Variation URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current variation price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Variation regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Variation sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from_gmt' => array( 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to_gmt' => array( 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'on_sale' => array( 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'visible' => array( 'description' => __( "Define if the variation is visible on the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'purchasable' => array( 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the variation is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at variation level.', 'woocommerce' ), 'type' => 'mixed', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Variation dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'image' => array( 'description' => __( 'Variation image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'meta_data' => array( 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php 0000644 00000127520 15132754523 0024702 0 ustar 00 <?php /** * REST API WC System Status controller * * Handles requests to the /system_status endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * System status controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Route base. * * @var string */ protected $rest_base = 'system_status'; /** * Register the route for /system_status */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to view system status. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get a system status info, by section. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $fields = $this->get_fields_for_response( $request ); $mappings = $this->get_item_mappings_per_fields( $fields ); $response = $this->prepare_item_for_response( $mappings, $request ); return rest_ensure_response( $response ); } /** * Get the system status schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'system_status', 'type' => 'object', 'properties' => array( 'environment' => array( 'description' => __( 'Environment.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'home_url' => array( 'description' => __( 'Home URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'site_url' => array( 'description' => __( 'Site URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'version' => array( 'description' => __( 'WooCommerce version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'log_directory' => array( 'description' => __( 'Log directory.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'log_directory_writable' => array( 'description' => __( 'Is log directory writable?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'wp_version' => array( 'description' => __( 'WordPress version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'wp_multisite' => array( 'description' => __( 'Is WordPress multisite?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'wp_memory_limit' => array( 'description' => __( 'WordPress memory limit.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'wp_debug_mode' => array( 'description' => __( 'Is WordPress debug mode active?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'wp_cron' => array( 'description' => __( 'Are WordPress cron jobs enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'language' => array( 'description' => __( 'WordPress language.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'server_info' => array( 'description' => __( 'Server info.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'php_version' => array( 'description' => __( 'PHP version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'php_post_max_size' => array( 'description' => __( 'PHP post max size.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'php_max_execution_time' => array( 'description' => __( 'PHP max execution time.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'php_max_input_vars' => array( 'description' => __( 'PHP max input vars.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'curl_version' => array( 'description' => __( 'cURL version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'suhosin_installed' => array( 'description' => __( 'Is SUHOSIN installed?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'max_upload_size' => array( 'description' => __( 'Max upload size.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'mysql_version' => array( 'description' => __( 'MySQL version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'mysql_version_string' => array( 'description' => __( 'MySQL version string.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'default_timezone' => array( 'description' => __( 'Default timezone.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'fsockopen_or_curl_enabled' => array( 'description' => __( 'Is fsockopen/cURL enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'soapclient_enabled' => array( 'description' => __( 'Is SoapClient class enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'domdocument_enabled' => array( 'description' => __( 'Is DomDocument class enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'gzip_enabled' => array( 'description' => __( 'Is GZip enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'mbstring_enabled' => array( 'description' => __( 'Is mbstring enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'remote_post_successful' => array( 'description' => __( 'Remote POST successful?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'remote_post_response' => array( 'description' => __( 'Remote POST response.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'remote_get_successful' => array( 'description' => __( 'Remote GET successful?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'remote_get_response' => array( 'description' => __( 'Remote GET response.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ), 'database' => array( 'description' => __( 'Database.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'wc_database_version' => array( 'description' => __( 'WC database version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'database_prefix' => array( 'description' => __( 'Database prefix.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'maxmind_geoip_database' => array( 'description' => __( 'MaxMind GeoIP database.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'database_tables' => array( 'description' => __( 'Database tables.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), ), ), 'active_plugins' => array( 'description' => __( 'Active plugins.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'inactive_plugins' => array( 'description' => __( 'Inactive plugins.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'dropins_mu_plugins' => array( 'description' => __( 'Dropins & MU plugins.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'theme' => array( 'description' => __( 'Theme.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'name' => array( 'description' => __( 'Theme name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'version' => array( 'description' => __( 'Theme version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'version_latest' => array( 'description' => __( 'Latest version of theme.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'author_url' => array( 'description' => __( 'Theme author URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'is_child_theme' => array( 'description' => __( 'Is this theme a child theme?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'has_woocommerce_support' => array( 'description' => __( 'Does the theme declare WooCommerce support?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'has_woocommerce_file' => array( 'description' => __( 'Does the theme have a woocommerce.php file?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'has_outdated_templates' => array( 'description' => __( 'Does this theme have outdated templates?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'overrides' => array( 'description' => __( 'Template overrides.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'parent_name' => array( 'description' => __( 'Parent theme name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'parent_version' => array( 'description' => __( 'Parent theme version.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'parent_author_url' => array( 'description' => __( 'Parent theme author URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), ), ), 'settings' => array( 'description' => __( 'Settings.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'api_enabled' => array( 'description' => __( 'REST API enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'force_ssl' => array( 'description' => __( 'SSL forced?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'currency' => array( 'description' => __( 'Currency.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'currency_symbol' => array( 'description' => __( 'Currency symbol.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'currency_position' => array( 'description' => __( 'Currency position.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'thousand_separator' => array( 'description' => __( 'Thousand separator.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'decimal_separator' => array( 'description' => __( 'Decimal separator.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'number_of_decimals' => array( 'description' => __( 'Number of decimals.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'geolocation_enabled' => array( 'description' => __( 'Geolocation enabled?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'taxonomies' => array( 'description' => __( 'Taxonomy terms for product/order statuses.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'product_visibility_terms' => array( 'description' => __( 'Terms in the product visibility taxonomy.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), ), ), 'security' => array( 'description' => __( 'Security.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'secure_connection' => array( 'description' => __( 'Is the connection to your store secure?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), 'hide_errors' => array( 'description' => __( 'Hide errors from visitors?', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), ), ), 'pages' => array( 'description' => __( 'WooCommerce pages.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'post_type_counts' => array( 'description' => __( 'Total post count.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Return an array of sections and the data associated with each. * * @deprecated 3.9.0 * @return array */ public function get_item_mappings() { return array( 'environment' => $this->get_environment_info(), 'database' => $this->get_database_info(), 'active_plugins' => $this->get_active_plugins(), 'inactive_plugins' => $this->get_inactive_plugins(), 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), 'theme' => $this->get_theme_info(), 'settings' => $this->get_settings(), 'security' => $this->get_security_info(), 'pages' => $this->get_pages(), 'post_type_counts' => $this->get_post_type_counts(), ); } /** * Return an array of sections and the data associated with each. * * @since 3.9.0 * @param array $fields List of fields to be included on the response. * @return array */ public function get_item_mappings_per_fields( $fields ) { return array( 'environment' => $this->get_environment_info_per_fields( $fields ), 'database' => $this->get_database_info(), 'active_plugins' => $this->get_active_plugins(), 'inactive_plugins' => $this->get_inactive_plugins(), 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), 'theme' => $this->get_theme_info(), 'settings' => $this->get_settings(), 'security' => $this->get_security_info(), 'pages' => $this->get_pages(), 'post_type_counts' => $this->get_post_type_counts(), ); } /** * Get array of environment information. Includes thing like software * versions, and various server settings. * * @deprecated 3.9.0 * @return array */ public function get_environment_info() { return $this->get_environment_info_per_fields( array( 'environment' ) ); } /** * Check if field item exists. * * @since 3.9.0 * @param string $section Fields section. * @param array $items List of items to check for. * @param array $fields List of fields to be included on the response. * @return bool */ private function check_if_field_item_exists( $section, $items, $fields ) { if ( ! in_array( $section, $fields, true ) ) { return false; } $exclude = array(); foreach ( $fields as $field ) { $values = explode( '.', $field ); if ( $section !== $values[0] || empty( $values[1] ) ) { continue; } $exclude[] = $values[1]; } return 0 <= count( array_intersect( $items, $exclude ) ); } /** * Get array of environment information. Includes thing like software * versions, and various server settings. * * @param array $fields List of fields to be included on the response. * @return array */ public function get_environment_info_per_fields( $fields ) { global $wpdb; $enable_remote_post = $this->check_if_field_item_exists( 'environment', array( 'remote_post_successful', 'remote_post_response' ), $fields ); $enable_remote_get = $this->check_if_field_item_exists( 'environment', array( 'remote_get_successful', 'remote_get_response' ), $fields ); // Figure out cURL version, if installed. $curl_version = ''; if ( function_exists( 'curl_version' ) ) { $curl_version = curl_version(); $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version']; } elseif ( extension_loaded( 'curl' ) ) { $curl_version = __( 'cURL installed but unable to retrieve version.', 'woocommerce' ); } // WP memory limit. $wp_memory_limit = wc_let_to_num( WP_MEMORY_LIMIT ); if ( function_exists( 'memory_get_usage' ) ) { $wp_memory_limit = max( $wp_memory_limit, wc_let_to_num( @ini_get( 'memory_limit' ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } // Test POST requests. $post_response_successful = null; $post_response_code = null; if ( $enable_remote_post ) { $post_response_code = get_transient( 'woocommerce_test_remote_post' ); if ( false === $post_response_code || is_wp_error( $post_response_code ) ) { $response = wp_safe_remote_post( 'https://www.paypal.com/cgi-bin/webscr', array( 'timeout' => 10, 'user-agent' => 'WooCommerce/' . WC()->version, 'httpversion' => '1.1', 'body' => array( 'cmd' => '_notify-validate', ), ) ); if ( ! is_wp_error( $response ) ) { $post_response_code = $response['response']['code']; } set_transient( 'woocommerce_test_remote_post', $post_response_code, HOUR_IN_SECONDS ); } $post_response_successful = ! is_wp_error( $post_response_code ) && $post_response_code >= 200 && $post_response_code < 300; } // Test GET requests. $get_response_successful = null; $get_response_code = null; if ( $enable_remote_get ) { $get_response_code = get_transient( 'woocommerce_test_remote_get' ); if ( false === $get_response_code || is_wp_error( $get_response_code ) ) { $response = wp_safe_remote_get( 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ) ); if ( ! is_wp_error( $response ) ) { $get_response_code = $response['response']['code']; } set_transient( 'woocommerce_test_remote_get', $get_response_code, HOUR_IN_SECONDS ); } $get_response_successful = ! is_wp_error( $get_response_code ) && $get_response_code >= 200 && $get_response_code < 300; } $database_version = wc_get_server_database_version(); // Return all environment info. Described by JSON Schema. return array( 'home_url' => get_option( 'home' ), 'site_url' => get_option( 'siteurl' ), 'version' => WC()->version, 'log_directory' => WC_LOG_DIR, 'log_directory_writable' => (bool) @fopen( WC_LOG_DIR . 'test-log.log', 'a' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen 'wp_version' => get_bloginfo( 'version' ), 'wp_multisite' => is_multisite(), 'wp_memory_limit' => $wp_memory_limit, 'wp_debug_mode' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ), 'wp_cron' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ), 'language' => get_locale(), 'external_object_cache' => wp_using_ext_object_cache(), 'server_info' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wc_clean( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', 'php_version' => phpversion(), 'php_post_max_size' => wc_let_to_num( ini_get( 'post_max_size' ) ), 'php_max_execution_time' => (int) ini_get( 'max_execution_time' ), 'php_max_input_vars' => (int) ini_get( 'max_input_vars' ), 'curl_version' => $curl_version, 'suhosin_installed' => extension_loaded( 'suhosin' ), 'max_upload_size' => wp_max_upload_size(), 'mysql_version' => $database_version['number'], 'mysql_version_string' => $database_version['string'], 'default_timezone' => date_default_timezone_get(), 'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ), 'soapclient_enabled' => class_exists( 'SoapClient' ), 'domdocument_enabled' => class_exists( 'DOMDocument' ), 'gzip_enabled' => is_callable( 'gzopen' ), 'mbstring_enabled' => extension_loaded( 'mbstring' ), 'remote_post_successful' => $post_response_successful, 'remote_post_response' => is_wp_error( $post_response_code ) ? $post_response_code->get_error_message() : $post_response_code, 'remote_get_successful' => $get_response_successful, 'remote_get_response' => is_wp_error( $get_response_code ) ? $get_response_code->get_error_message() : $get_response_code, ); } /** * Add prefix to table. * * @param string $table Table name. * @return stromg */ protected function add_db_table_prefix( $table ) { global $wpdb; return $wpdb->prefix . $table; } /** * Get array of database information. Version, prefix, and table existence. * * @return array */ public function get_database_info() { global $wpdb; $tables = array(); $database_size = array(); // It is not possible to get the database name from some classes that replace wpdb (e.g., HyperDB) // and that is why this if condition is needed. if ( defined( 'DB_NAME' ) ) { $database_table_information = $wpdb->get_results( $wpdb->prepare( "SELECT table_name AS 'name', engine AS 'engine', round( ( data_length / 1024 / 1024 ), 2 ) 'data', round( ( index_length / 1024 / 1024 ), 2 ) 'index' FROM information_schema.TABLES WHERE table_schema = %s ORDER BY name ASC;", DB_NAME ) ); // WC Core tables to check existence of. $core_tables = apply_filters( 'woocommerce_database_tables', array( 'woocommerce_sessions', 'woocommerce_api_keys', 'woocommerce_attribute_taxonomies', 'woocommerce_downloadable_product_permissions', 'woocommerce_order_items', 'woocommerce_order_itemmeta', 'woocommerce_tax_rates', 'woocommerce_tax_rate_locations', 'woocommerce_shipping_zones', 'woocommerce_shipping_zone_locations', 'woocommerce_shipping_zone_methods', 'woocommerce_payment_tokens', 'woocommerce_payment_tokenmeta', 'woocommerce_log', ) ); /** * Adding the prefix to the tables array, for backwards compatibility. * * If we changed the tables above to include the prefix, then any filters against that table could break. */ $core_tables = array_map( array( $this, 'add_db_table_prefix' ), $core_tables ); /** * Organize WooCommerce and non-WooCommerce tables separately for display purposes later. * * To ensure we include all WC tables, even if they do not exist, pre-populate the WC array with all the tables. */ $tables = array( 'woocommerce' => array_fill_keys( $core_tables, false ), 'other' => array(), ); $database_size = array( 'data' => 0, 'index' => 0, ); $site_tables_prefix = $wpdb->get_blog_prefix( get_current_blog_id() ); $global_tables = $wpdb->tables( 'global', true ); foreach ( $database_table_information as $table ) { // Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current. if ( is_multisite() && 0 !== strpos( $table->name, $site_tables_prefix ) && ! in_array( $table->name, $global_tables, true ) ) { continue; } $table_type = in_array( $table->name, $core_tables, true ) ? 'woocommerce' : 'other'; $tables[ $table_type ][ $table->name ] = array( 'data' => $table->data, 'index' => $table->index, 'engine' => $table->engine, ); $database_size['data'] += $table->data; $database_size['index'] += $table->index; } } // Return all database info. Described by JSON Schema. return array( 'wc_database_version' => get_option( 'woocommerce_db_version' ), 'database_prefix' => $wpdb->prefix, 'maxmind_geoip_database' => '', 'database_tables' => $tables, 'database_size' => $database_size, ); } /** * Get array of counts of objects. Orders, products, etc. * * @return array */ public function get_post_type_counts() { global $wpdb; $post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type;" ); return is_array( $post_type_counts ) ? $post_type_counts : array(); } /** * Get a list of plugins active on the site. * * @return array */ public function get_active_plugins() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( ! function_exists( 'get_plugin_data' ) ) { return array(); } $active_plugins = (array) get_option( 'active_plugins', array() ); if ( is_multisite() ) { $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); } $active_plugins_data = array(); foreach ( $active_plugins as $plugin ) { $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); $active_plugins_data[] = $this->format_plugin_data( $plugin, $data ); } return $active_plugins_data; } /** * Get a list of inplugins active on the site. * * @return array */ public function get_inactive_plugins() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( ! function_exists( 'get_plugins' ) ) { return array(); } $plugins = get_plugins(); $active_plugins = (array) get_option( 'active_plugins', array() ); if ( is_multisite() ) { $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); } $plugins_data = array(); foreach ( $plugins as $plugin => $data ) { if ( in_array( $plugin, $active_plugins, true ) ) { continue; } $plugins_data[] = $this->format_plugin_data( $plugin, $data ); } return $plugins_data; } /** * Format plugin data, including data on updates, into a standard format. * * @since 3.6.0 * @param string $plugin Plugin directory/file. * @param array $data Plugin data from WP. * @return array Formatted data. */ protected function format_plugin_data( $plugin, $data ) { require_once ABSPATH . 'wp-admin/includes/update.php'; if ( ! function_exists( 'get_plugin_updates' ) ) { return array(); } // Use WP API to lookup latest updates for plugins. WC_Helper injects updates for premium plugins. if ( empty( $this->available_updates ) ) { $this->available_updates = get_plugin_updates(); } $version_latest = $data['Version']; // Find latest version. if ( isset( $this->available_updates[ $plugin ]->update->new_version ) ) { $version_latest = $this->available_updates[ $plugin ]->update->new_version; } return array( 'plugin' => $plugin, 'name' => $data['Name'], 'version' => $data['Version'], 'version_latest' => $version_latest, 'url' => $data['PluginURI'], 'author_name' => $data['AuthorName'], 'author_url' => esc_url_raw( $data['AuthorURI'] ), 'network_activated' => $data['Network'], ); } /** * Get a list of Dropins and MU plugins. * * @since 3.6.0 * @return array */ public function get_dropins_mu_plugins() { $dropins = get_dropins(); $plugins = array( 'dropins' => array(), 'mu_plugins' => array(), ); foreach ( $dropins as $key => $dropin ) { $plugins['dropins'][] = array( 'plugin' => $key, 'name' => $dropin['Name'], ); } $mu_plugins = get_mu_plugins(); foreach ( $mu_plugins as $plugin => $mu_plugin ) { $plugins['mu_plugins'][] = array( 'plugin' => $plugin, 'name' => $mu_plugin['Name'], 'version' => $mu_plugin['Version'], 'url' => $mu_plugin['PluginURI'], 'author_name' => $mu_plugin['AuthorName'], 'author_url' => esc_url_raw( $mu_plugin['AuthorURI'] ), ); } return $plugins; } /** * Get info on the current active theme, info on parent theme (if presnet) * and a list of template overrides. * * @return array */ public function get_theme_info() { $active_theme = wp_get_theme(); // Get parent theme info if this theme is a child theme, otherwise // pass empty info in the response. if ( is_child_theme() ) { $parent_theme = wp_get_theme( $active_theme->template ); $parent_theme_info = array( 'parent_name' => $parent_theme->name, 'parent_version' => $parent_theme->version, 'parent_version_latest' => WC_Admin_Status::get_latest_theme_version( $parent_theme ), 'parent_author_url' => $parent_theme->{'Author URI'}, ); } else { $parent_theme_info = array( 'parent_name' => '', 'parent_version' => '', 'parent_version_latest' => '', 'parent_author_url' => '', ); } /** * Scan the theme directory for all WC templates to see if our theme * overrides any of them. */ $override_files = array(); $outdated_templates = false; $scan_files = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates/' ); // Include *-product_<cat|tag> templates for backwards compatibility. $scan_files[] = 'content-product_cat.php'; $scan_files[] = 'taxonomy-product_cat.php'; $scan_files[] = 'taxonomy-product_tag.php'; foreach ( $scan_files as $file ) { $located = apply_filters( 'wc_get_template', $file, $file, array(), WC()->template_path(), WC()->plugin_path() . '/templates/' ); if ( file_exists( $located ) ) { $theme_file = $located; } elseif ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . $file; } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { $theme_file = get_template_directory() . '/' . $file; } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; } else { $theme_file = false; } if ( ! empty( $theme_file ) ) { $core_file = $file; // Update *-product_<cat|tag> template name before searching in core. if ( false !== strpos( $core_file, '-product_cat' ) || false !== strpos( $core_file, '-product_tag' ) ) { $core_file = str_replace( '_', '-', $core_file ); } $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $core_file ); $theme_version = WC_Admin_Status::get_file_version( $theme_file ); if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) { if ( ! $outdated_templates ) { $outdated_templates = true; } } $override_files[] = array( 'file' => str_replace( WP_CONTENT_DIR . '/themes/', '', $theme_file ), 'version' => $theme_version, 'core_version' => $core_version, ); } } $active_theme_info = array( 'name' => $active_theme->name, 'version' => $active_theme->version, 'version_latest' => WC_Admin_Status::get_latest_theme_version( $active_theme ), 'author_url' => esc_url_raw( $active_theme->{'Author URI'} ), 'is_child_theme' => is_child_theme(), 'has_woocommerce_support' => current_theme_supports( 'woocommerce' ), 'has_woocommerce_file' => ( file_exists( get_stylesheet_directory() . '/woocommerce.php' ) || file_exists( get_template_directory() . '/woocommerce.php' ) ), 'has_outdated_templates' => $outdated_templates, 'overrides' => $override_files, ); return array_merge( $active_theme_info, $parent_theme_info ); } /** * Get some setting values for the site that are useful for debugging * purposes. For full settings access, use the settings api. * * @return array */ public function get_settings() { // Get a list of terms used for product/order taxonomies. $term_response = array(); $terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); foreach ( $terms as $term ) { $term_response[ $term->slug ] = strtolower( $term->name ); } // Get a list of terms used for product visibility. $product_visibility_terms = array(); $terms = get_terms( 'product_visibility', array( 'hide_empty' => 0 ) ); foreach ( $terms as $term ) { $product_visibility_terms[ $term->slug ] = strtolower( $term->name ); } // Check if WooCommerce.com account is connected. $woo_com_connected = 'no'; $helper_options = get_option( 'woocommerce_helper_data', array() ); if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) { $woo_com_connected = 'yes'; } // Return array of useful settings for debugging. return array( 'api_enabled' => 'yes' === get_option( 'woocommerce_api_enabled' ), 'force_ssl' => 'yes' === get_option( 'woocommerce_force_ssl_checkout' ), 'currency' => get_woocommerce_currency(), 'currency_symbol' => get_woocommerce_currency_symbol(), 'currency_position' => get_option( 'woocommerce_currency_pos' ), 'thousand_separator' => wc_get_price_thousand_separator(), 'decimal_separator' => wc_get_price_decimal_separator(), 'number_of_decimals' => wc_get_price_decimals(), 'geolocation_enabled' => in_array( get_option( 'woocommerce_default_customer_address' ), array( 'geolocation_ajax', 'geolocation' ), true ), 'taxonomies' => $term_response, 'product_visibility_terms' => $product_visibility_terms, 'woocommerce_com_connected' => $woo_com_connected, ); } /** * Returns security tips. * * @return array */ public function get_security_info() { $check_page = wc_get_page_permalink( 'shop' ); return array( 'secure_connection' => 'https' === substr( $check_page, 0, 5 ), 'hide_errors' => ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ), ); } /** * Returns a mini-report on WC pages and if they are configured correctly: * Present, visible, and including the correct shortcode or block. * * @return array */ public function get_pages() { // WC pages to check against. $check_pages = array( _x( 'Shop base', 'Page setting', 'woocommerce' ) => array( 'option' => 'woocommerce_shop_page_id', 'shortcode' => '', 'block' => '', ), _x( 'Cart', 'Page setting', 'woocommerce' ) => array( 'option' => 'woocommerce_cart_page_id', 'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', 'block' => 'woocommerce/cart', ), _x( 'Checkout', 'Page setting', 'woocommerce' ) => array( 'option' => 'woocommerce_checkout_page_id', 'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', 'block' => 'woocommerce/checkout', ), _x( 'My account', 'Page setting', 'woocommerce' ) => array( 'option' => 'woocommerce_myaccount_page_id', 'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', 'block' => '', ), _x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array( 'option' => 'woocommerce_terms_page_id', 'shortcode' => '', 'block' => '', ), ); $pages_output = array(); foreach ( $check_pages as $page_name => $values ) { $page_id = get_option( $values['option'] ); $page_set = false; $page_exists = false; $page_visible = false; $shortcode_present = false; $shortcode_required = false; $block_present = false; $block_required = false; // Page checks. if ( $page_id ) { $page_set = true; } if ( get_post( $page_id ) ) { $page_exists = true; } if ( 'publish' === get_post_status( $page_id ) ) { $page_visible = true; } // Shortcode checks. if ( $values['shortcode'] && get_post( $page_id ) ) { $shortcode_required = true; $page = get_post( $page_id ); if ( strstr( $page->post_content, $values['shortcode'] ) ) { $shortcode_present = true; } } // Block checks. if ( $values['block'] && get_post( $page_id ) ) { $block_required = true; $block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] ); } // Wrap up our findings into an output array. $pages_output[] = array( 'page_name' => $page_name, 'page_id' => $page_id, 'page_set' => $page_set, 'page_exists' => $page_exists, 'page_visible' => $page_visible, 'shortcode' => $values['shortcode'], 'block' => $values['block'], 'shortcode_required' => $shortcode_required, 'shortcode_present' => $shortcode_present, 'block_present' => $block_present, 'block_required' => $block_required, ); } return $pages_output; } /** * Get any query params needed. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } /** * Prepare the system status response * * @param array $system_status System status data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_item_for_response( $system_status, $request ) { $data = $this->add_additional_fields_to_object( $system_status, $request ); $data = $this->filter_response_by_context( $data, 'view' ); $response = rest_ensure_response( $data ); /** * Filter the system status returned from the REST API. * * @param WP_REST_Response $response The response object. * @param mixed $system_status System status * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_system_status', $response, $system_status, $request ); } } includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php 0000644 00000016347 15132754523 0025644 0 ustar 00 <?php /** * REST API Product Categories controller * * Handles requests to the products/categories endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Product Categories controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Product_Categories_V1_Controller */ class WC_REST_Product_Categories_V2_Controller extends WC_REST_Product_Categories_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Prepare a single product category output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request Request instance. * @return WP_REST_Response */ public function prepare_item_for_response( $item, $request ) { // Get category display type. $display_type = get_term_meta( $item->term_id, 'display_type', true ); // Get category order. $menu_order = get_term_meta( $item->term_id, 'order', true ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'parent' => (int) $item->parent, 'description' => $item->description, 'display' => $display_type ? $display_type : 'default', 'image' => null, 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); // Get category image. $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); if ( $image_id ) { $attachment = get_post( $image_id ); $data['image'] = array( 'id' => (int) $image_id, 'date_created' => wc_rest_prepare_date_response( $attachment->post_date ), 'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ), 'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), 'src' => wp_get_attachment_url( $image_id ), 'title' => get_the_title( $attachment ), 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Category schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'parent' => array( 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'display' => array( 'description' => __( 'Category archive display type.', 'woocommerce' ), 'type' => 'string', 'default' => 'default', 'enum' => array( 'default', 'products', 'subcategories', 'both' ), 'context' => array( 'view', 'edit' ), ), 'image' => array( 'description' => __( 'Image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified_gmt' => array( 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'title' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php 0000644 00000011552 15132754523 0025621 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks/<webhook_id>/deliveries endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Webhook Deliveries controller class. * * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @package WooCommerce\RestApi * @extends WC_REST_Webhook_Deliveries_V1_Controller */ class WC_REST_Webhook_Deliveries_V2_Controller extends WC_REST_Webhook_Deliveries_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Prepare a single webhook delivery output for response. * * @param stdClass $log Delivery log object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_item_for_response( $log, $request ) { $data = (array) $log; $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $log ) ); /** * Filter webhook delivery object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $log Delivery log object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook_delivery', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'duration' => array( 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'summary' => array( 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'request_url' => array( 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'request_headers' => array( 'description' => __( 'Request headers.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'request_body' => array( 'description' => __( 'Request body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_code' => array( 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_message' => array( 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_headers' => array( 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'response_body' => array( 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created_gmt' => array( 'description' => __( 'The date the webhook delivery was logged, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php 0000644 00000000723 15132754523 0023526 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports endpoint. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API Reports controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Reports_V1_Controller */ class WC_REST_Reports_V2_Controller extends WC_REST_Reports_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; } includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php 0000644 00000006516 15132754524 0023767 0 ustar 00 <?php /** * REST API WC Telemetry controller * * Handles requests to the /wc-telemetry endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Telemetry controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Telemetry_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc-telemetry'; /** * Route base. * * @var string */ protected $rest_base = 'tracker'; /** * Register the route for /tracker */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'record_usage_data' ), 'permission_callback' => array( $this, 'telemetry_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to post telemetry data * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function telemetry_permissions_check( $request ) { if ( ! is_user_logged_in() ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you post telemetry data.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Record WCTracker Data * * @param WP_REST_Request $request Full details about the request. */ public function record_usage_data( $request ) { $new = $this->get_usage_data( $request ); if ( ! $new || ! $new['platform'] ) { return; } $data = get_option( 'woocommerce_mobile_app_usage' ); if ( ! $data ) { $data = array(); } $platform = $new['platform']; if ( ! $data[ $platform ] || version_compare( $new['version'], $data[ $platform ]['version'], '>=' ) ) { $data[ $platform ] = $new; } update_option( 'woocommerce_mobile_app_usage', $data ); } /** * Get usage data from current request * * @param WP_REST_Request $request Full details about the request. * @return Array */ public function get_usage_data( $request ) { $platform = strtolower( $request->get_param( 'platform' ) ); switch ( $platform ) { case 'ios': case 'android': break; default: return; } $version = $request->get_param( 'version' ); if ( ! $version ) { return; } return array( 'platform' => sanitize_text_field( $platform ), 'version' => sanitize_text_field( $version ), 'last_used' => gmdate( 'c' ), ); } /** * Get any query params needed. * * @return array */ public function get_collection_params() { return array( 'platform' => array( 'description' => __( 'Platform to track.', 'woocommerce' ), 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), 'version' => array( 'description' => __( 'Platform version to track.', 'woocommerce' ), 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php 0000644 00000046752 15132754524 0025707 0 ustar 00 <?php /** * REST API Product Attributes controller * * Handles requests to the products/attributes endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Attributes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Product_Attributes_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/attributes'; /** * Attribute name. * * @var string */ protected $attribute = ''; /** * Cached taxonomies by attribute id. * * @var array */ protected $taxonomies_by_id = array(); /** * Register the routes for product attributes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'description' => __( 'Name for the resource.', 'woocommerce' ), 'type' => 'string', 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => true, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check if a given request has access to read the attributes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all attributes. * * @param WP_REST_Request $request The request to get the attributes from. * @return array */ public function get_items( $request ) { $attributes = wc_get_attribute_taxonomies(); $data = array(); foreach ( $attributes as $attribute_obj ) { $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); $attribute = $this->prepare_response_for_collection( $attribute ); $data[] = $attribute; } $response = rest_ensure_response( $data ); // This API call always returns all product attributes due to retrieval from the object cache. $response->header( 'X-WP-Total', count( $data ) ); $response->header( 'X-WP-TotalPages', 1 ); return $response; } /** * Create a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { global $wpdb; $id = wc_create_attribute( array( 'name' => $request['name'], 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), 'type' => ! empty( $request['type'] ) ? $request['type'] : 'select', 'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', 'has_archives' => true === $request['has_archives'], ) ); // Checks for errors. if ( is_wp_error( $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $this->update_additional_fields_for_object( $attribute, $request ); /** * Fires after a single product attribute is created or updated via the REST API. * * @param stdObject $attribute Inserted attribute object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); return $response; } /** * Get a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function get_item( $request ) { $attribute = $this->get_attribute( (int) $request['id'] ); if ( is_wp_error( $attribute ) ) { return $attribute; } $response = $this->prepare_item_for_response( $attribute, $request ); return rest_ensure_response( $response ); } /** * Update a single term from a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { global $wpdb; $id = (int) $request['id']; $edited = wc_update_attribute( $id, array( 'name' => $request['name'], 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), 'type' => $request['type'], 'order_by' => $request['order_by'], 'has_archives' => $request['has_archives'], ) ); // Checks for errors. if ( is_wp_error( $edited ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $this->update_additional_fields_for_object( $attribute, $request ); /** * Fires after a single product attribute is created or updated via the REST API. * * @param stdObject $attribute Inserted attribute object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); return rest_ensure_response( $response ); } /** * Delete a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $attribute = $this->get_attribute( (int) $request['id'] ); if ( is_wp_error( $attribute ) ) { return $attribute; } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $deleted = wc_delete_attribute( $attribute->attribute_id ); if ( false === $deleted ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a single attribute is deleted via the REST API. * * @param stdObject $attribute The deleted attribute. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); return $response; } /** * Prepare a single product attribute output for response. * * @param obj $item Term object. * @param WP_REST_Request $request The request to process. * @return WP_REST_Response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->attribute_id, 'name' => $item->attribute_label, 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), 'type' => $item->attribute_type, 'order_by' => $item->attribute_orderby, 'has_archives' => (bool) $item->attribute_public, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item ) ); /** * Filter a attribute item returned from the API. * * Allows modification of the product attribute data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original attribute object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); } /** * Prepare links for the request. * * @param object $attribute Attribute object. * @return array Links for the given attribute. */ protected function prepare_links( $attribute ) { $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); return $links; } /** * Get the Attribute's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_attribute', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'type' => array( 'description' => __( 'Type of attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'select', 'enum' => array_keys( wc_get_attribute_types() ), 'context' => array( 'view', 'edit' ), ), 'order_by' => array( 'description' => __( 'Default sort order.', 'woocommerce' ), 'type' => 'string', 'default' => 'menu_order', 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ), 'context' => array( 'view', 'edit' ), ), 'has_archives' => array( 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); return $params; } /** * Get attribute name. * * @param WP_REST_Request $request Full details about the request. * @return string */ protected function get_taxonomy( $request ) { $attribute_id = $request['id']; if ( empty( $attribute_id ) ) { return ''; } if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { return $this->taxonomies_by_id[ $attribute_id ]; } $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['id'] ); if ( ! empty( $taxonomy ) ) { $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; } return $taxonomy; } /** * Get attribute data. * * @param int $id Attribute ID. * @return stdClass|WP_Error */ protected function get_attribute( $id ) { global $wpdb; $attribute = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } return $attribute; } /** * Validate attribute slug. * * @deprecated 3.2.0 * @param string $slug The slug to validate. * @param bool $new_data If we are creating new data. * @return bool|WP_Error */ protected function validate_attribute_slug( $slug, $new_data = true ) { if ( strlen( $slug ) >= 28 ) { /* translators: %s: slug being validated */ return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { /* translators: %s: slug being validated */ return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { /* translators: %s: slug being validated */ return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } return true; } /** * Schedule to flush rewrite rules. * * @deprecated 3.2.0 * @since 3.0.0 */ protected function flush_rewrite_rules() { wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); } } includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php 0000644 00000023010 15132754524 0024250 0 ustar 00 <?php /** * REST API Tax Classes controller * * Handles requests to the /taxes/classes endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Tax Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Tax_Classes_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'taxes/classes'; /** * Register the routes for tax classes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)', array( 'args' => array( 'slug' => array( 'description' => __( 'Unique slug for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read tax classes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create tax classes. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a tax. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all tax classes. * * @param WP_REST_Request $request Full details about the request. * @return array */ public function get_items( $request ) { $tax_classes = array(); // Add standard class. $tax_classes[] = array( 'slug' => 'standard', 'name' => __( 'Standard rate', 'woocommerce' ), ); $classes = WC_Tax::get_tax_classes(); foreach ( $classes as $class ) { $tax_classes[] = array( 'slug' => sanitize_title( $class ), 'name' => $class, ); } $data = array(); foreach ( $tax_classes as $tax_class ) { $class = $this->prepare_item_for_response( $tax_class, $request ); $class = $this->prepare_response_for_collection( $class ); $data[] = $class; } return rest_ensure_response( $data ); } /** * Create a single tax class. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { $tax_class = WC_Tax::create_tax_class( $request['name'] ); if ( is_wp_error( $tax_class ) ) { return new WP_Error( 'woocommerce_rest_' . $tax_class->get_error_code(), $tax_class->get_error_message(), array( 'status' => 400 ) ); } $this->update_additional_fields_for_object( $tax_class, $request ); /** * Fires after a tax class is created or updated via the REST API. * * @param stdClass $tax_class Data used to create the tax class. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax class, false when updating tax class. */ do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax_class, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); return $response; } /** * Delete a single tax class. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { global $wpdb; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); $deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); if ( ! $deleted ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( is_wp_error( $deleted ) ) { return new WP_Error( 'woocommerce_rest_' . $deleted->get_error_code(), $deleted->get_error_message(), array( 'status' => 400 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax_class, $request ); /** * Fires after a tax class is deleted via the REST API. * * @param stdClass $tax_class The tax data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); return $response; } /** * Prepare a single tax class output for response. * * @param array $tax_class Tax class data. * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $tax_class, $request ) { $data = $tax_class; $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links() ); /** * Filter tax object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $tax_class Tax object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); } /** * Prepare links for the request. * * @return array Links for the given tax class. */ protected function prepare_links() { $links = array( 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Tax Classes schema, conforming to JSON Schema * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'tax_class', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Tax class name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'required' => true, 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php 0000644 00000007051 15132754524 0024444 0 ustar 00 <?php /** * REST API Product Tags controller * * Handles requests to the products/tags endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Tags controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Tags_V1_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/tags'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_tag'; /** * Prepare a single product tag output for response. * * @param obj $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'count' => (int) $item->count, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Tag's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php 0000644 00000030713 15132754524 0024451 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/sales endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Report Sales controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Report_Sales_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports/sales'; /** * Report instance. * * @var WC_Admin_Report */ protected $report; /** * Register the routes for sales reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read report. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get sales reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { $data = array(); $item = $this->prepare_item_for_response( null, $request ); $data[] = $this->prepare_response_for_collection( $item ); return rest_ensure_response( $data ); } /** * Prepare a report sales object for serialization. * * @param null $_ * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $_, $request ) { // Set date filtering. $filter = array( 'period' => $request['period'], 'date_min' => $request['date_min'], 'date_max' => $request['date_max'], ); $this->setup_report( $filter ); // New customers. $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), 'role' => 'customer', ) ); $customers = $users_query->get_results(); foreach ( $customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { unset( $customers[ $key ] ); } } $total_customers = count( $customers ); $report_data = $this->report->get_report_data(); $period_totals = array(); // Setup period totals by ensuring each period in the interval has data. for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) { switch ( $this->report->chart_groupby ) { case 'day' : $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); break; default : $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); break; } // Set the customer signups for each period. $customer_count = 0; foreach ( $customers as $customer ) { if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { $customer_count++; } } $period_totals[ $time ] = array( 'sales' => wc_format_decimal( 0.00, 2 ), 'orders' => 0, 'items' => 0, 'tax' => wc_format_decimal( 0.00, 2 ), 'shipping' => wc_format_decimal( 0.00, 2 ), 'discount' => wc_format_decimal( 0.00, 2 ), 'customers' => $customer_count, ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $report_data->orders as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); } foreach ( $report_data->order_counts as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['orders'] = (int) $order->count; } // Add total order items for each period. foreach ( $report_data->order_items as $order_item ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // Add total discount for each period. foreach ( $report_data->coupons as $discount ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } $sales_data = array( 'total_sales' => $report_data->total_sales, 'net_sales' => $report_data->net_sales, 'average_sales' => $report_data->average_sales, 'total_orders' => $report_data->total_orders, 'total_items' => $report_data->total_items, 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), 'total_shipping' => $report_data->total_shipping, 'total_refunds' => $report_data->total_refunds, 'total_discount' => $report_data->total_coupons, 'totals_grouped_by' => $this->report->chart_groupby, 'totals' => $period_totals, 'total_customers' => $total_customers, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $sales_data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( array( 'about' => array( 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), ), ) ); /** * Filter a report sales returned from the API. * * Allows modification of the report sales data right before it is returned. * * @param WP_REST_Response $response The response object. * @param stdClass $data The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request ); } /** * Setup the report object and parse any date filtering. * * @param array $filter date filtering */ protected function setup_report( $filter ) { include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); $this->report = new WC_Report_Sales_By_Date(); if ( empty( $filter['period'] ) ) { // Custom date range. $filter['period'] = 'custom'; if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { // Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. $_GET['start_date'] = $filter['date_min']; $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; } else { // Default custom range to today. $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period']; // Change "week" period to "7day". if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; } } $this->report->calculate_current_range( $filter['period'] ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'sales_report', 'type' => 'object', 'properties' => array( 'total_sales' => array( 'description' => __( 'Gross sales in the period.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'net_sales' => array( 'description' => __( 'Net sales in the period.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'average_sales' => array( 'description' => __( 'Average net daily sales.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_orders' => array( 'description' => __( 'Total of orders placed.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_items' => array( 'description' => __( 'Total of items purchased.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Total charged for taxes.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_shipping' => array( 'description' => __( 'Total charged for shipping.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_refunds' => array( 'description' => __( 'Total of refunded orders.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_discount' => array( 'description' => __( 'Total of coupons used.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'totals_grouped_by' => array( 'description' => __( 'Group type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'totals' => array( 'description' => __( 'Totals.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'array', ), 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'period' => array( 'description' => __( 'Report period.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'week', 'month', 'last_month', 'year' ), 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), 'date_min' => array( /* translators: %s: date format */ 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ), 'type' => 'string', 'format' => 'date', 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), 'date_max' => array( /* translators: %s: date format */ 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ), 'type' => 'string', 'format' => 'date', 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php 0000644 00000023020 15132754524 0025611 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks/<webhook_id>/deliveries endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Webhook Deliveries controller class. * * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Webhook_Deliveries_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'webhooks/(?P<webhook_id>[\d]+)/deliveries'; /** * Register the routes for webhook deliveries. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'webhook_id' => array( 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'webhook_id' => array( 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all webhook deliveries. * * @param WP_REST_Request $request * * @return array|WP_Error */ public function get_items( $request ) { $webhook = wc_get_webhook( (int) $request['webhook_id'] ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $logs = array(); $data = array(); foreach ( $logs as $log ) { $delivery = $this->prepare_item_for_response( (object) $log, $request ); $delivery = $this->prepare_response_for_collection( $delivery ); $data[] = $delivery; } return rest_ensure_response( $data ); } /** * Get a single webhook delivery. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $webhook = wc_get_webhook( (int) $request['webhook_id'] ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $log = array(); if ( empty( $id ) || empty( $log ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $delivery = $this->prepare_item_for_response( (object) $log, $request ); $response = rest_ensure_response( $delivery ); return $response; } /** * Prepare a single webhook delivery output for response. * * @param stdClass $log Delivery log object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $log, $request ) { $data = (array) $log; $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $log ) ); /** * Filter webhook delivery object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $log Delivery log object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); } /** * Prepare links for the request. * * @param stdClass $log Delivery log object. * @return array Links for the given webhook delivery. */ protected function prepare_links( $log ) { $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; $base = str_replace( '(?P<webhook_id>[\d]+)', $webhook_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), ), ); return $links; } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook_delivery', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'duration' => array( 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'summary' => array( 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'request_url' => array( 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'request_headers' => array( 'description' => __( 'Request headers.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'request_body' => array( 'description' => __( 'Request body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_code' => array( 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_message' => array( 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_headers' => array( 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'response_body' => array( 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php 0000644 00000034147 15132754524 0024277 0 ustar 00 <?php /** * REST API Order Notes controller * * Handles requests to the /orders/<order_id>/notes endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Order Notes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Order_Notes_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders/(?P<order_id>[\d]+)/notes'; /** * Post type. * * @var string */ protected $post_type = 'shop_order'; /** * Register the routes for order notes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'note' => array( 'type' => 'string', 'description' => __( 'Order note content.', 'woocommerce' ), 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read order notes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create order notes. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $order = wc_get_order( (int) $request['order_id'] ); if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'read', $order->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a order note. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { $order = wc_get_order( (int) $request['order_id'] ); if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $order->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get order notes from an order. * * @param WP_REST_Request $request * * @return array|WP_Error */ public function get_items( $request ) { $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $args = array( 'post_id' => $order->get_id(), 'approve' => 'approve', 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $data = array(); foreach ( $notes as $note ) { $order_note = $this->prepare_item_for_response( $note, $request ); $order_note = $this->prepare_response_for_collection( $order_note ); $data[] = $order_note; } return rest_ensure_response( $data ); } /** * Create a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } // Create the note. $note_id = $order->add_order_note( $request['note'], $request['customer_note'] ); if ( ! $note_id ) { return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); } $note = get_comment( $note_id ); $this->update_additional_fields_for_object( $note, $request ); /** * Fires after a order note is created or updated via the REST API. * * @param WP_Comment $note New order note object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $note, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) ); return $response; } /** * Get a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $note = get_comment( $id ); if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $order_note = $this->prepare_item_for_response( $note, $request ); $response = rest_ensure_response( $order_note ); return $response; } /** * Delete a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order || $this->post_type !== $order->get_type() ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $note = get_comment( $id ); if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $note, $request ); $result = wc_delete_order_note( $note->comment_ID ); if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); } /** * Fires after a order note is deleted or trashed via the REST API. * * @param WP_Comment $note The deleted or trashed order note. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); return $response; } /** * Prepare a single order note output for response. * * @param WP_Comment $note Order note object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => (int) $note->comment_ID, 'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $note ) ); /** * Filter order note object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $note Order note object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); } /** * Prepare links for the request. * * @param WP_Comment $note Delivery order_note object. * @return array Links for the given order note. */ protected function prepare_links( $note ) { $order_id = (int) $note->comment_post_ID; $base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), ), ); return $links; } /** * Get the Order Notes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'order_note', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'note' => array( 'description' => __( 'Order note.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_note' => array( 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php 0000644 00000020460 15132754524 0025632 0 ustar 00 <?php /** * REST API Product Categories controller * * Handles requests to the products/categories endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Categories controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Categories_V1_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/categories'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_cat'; /** * Prepare a single product category output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request Request instance. * @return WP_REST_Response */ public function prepare_item_for_response( $item, $request ) { // Get category display type. $display_type = get_term_meta( $item->term_id, 'display_type', true ); // Get category order. $menu_order = get_term_meta( $item->term_id, 'order', true ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'parent' => (int) $item->parent, 'description' => $item->description, 'display' => $display_type ? $display_type : 'default', 'image' => null, 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); // Get category image. $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); if ( $image_id ) { $attachment = get_post( $image_id ); $data['image'] = array( 'id' => (int) $image_id, 'date_created' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), 'src' => wp_get_attachment_url( $image_id ), 'title' => get_the_title( $attachment ), 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Update term meta fields. * * @param WP_Term $term Term object. * @param WP_REST_Request $request Request instance. * @return bool|WP_Error */ protected function update_term_meta_fields( $term, $request ) { $id = (int) $term->term_id; if ( isset( $request['display'] ) ) { update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); } if ( isset( $request['menu_order'] ) ) { update_term_meta( $id, 'order', $request['menu_order'] ); } if ( isset( $request['image'] ) ) { if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); if ( is_wp_error( $upload ) ) { return $upload; } $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); } else { $image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0; } // Check if image_id is a valid image attachment before updating the term meta. if ( $image_id && wp_attachment_is_image( $image_id ) ) { update_term_meta( $id, 'thumbnail_id', $image_id ); // Set the image alt. if ( ! empty( $request['image']['alt'] ) ) { update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); } // Set the image title. if ( ! empty( $request['image']['title'] ) ) { wp_update_post( array( 'ID' => $image_id, 'post_title' => wc_clean( $request['image']['title'] ), ) ); } } else { delete_term_meta( $id, 'thumbnail_id' ); } } return true; } /** * Get the Category schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'parent' => array( 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'display' => array( 'description' => __( 'Category archive display type.', 'woocommerce' ), 'type' => 'string', 'default' => 'default', 'enum' => array( 'default', 'products', 'subcategories', 'both' ), 'context' => array( 'view', 'edit' ), ), 'image' => array( 'description' => __( 'Image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'title' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php 0000644 00000016707 15132754524 0025671 0 ustar 00 <?php /** * REST API Customer Downloads controller * * Handles requests to the /customers/<customer_id>/downloads endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'customers/(?P<customer_id>[\d]+)/downloads'; /** * Register the routes for customers. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'customer_id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { $customer = get_user_by( 'id', (int) $request['customer_id'] ); if ( ! $customer ) { return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all customer downloads. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] ); $data = array(); foreach ( $downloads as $download_data ) { $download = $this->prepare_item_for_response( (object) $download_data, $request ); $download = $this->prepare_response_for_collection( $download ); $data[] = $download; } return rest_ensure_response( $data ); } /** * Prepare a single download output for response. * * @param stdObject $download Download object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $download, $request ) { $data = (array) $download; $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; // Remove "product_name" since it's new in 3.0. unset( $data['product_name'] ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $download, $request ) ); /** * Filter customer download data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdObject $download Download object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); } /** * Prepare links for the request. * * @param stdClass $download Download object. * @param WP_REST_Request $request Request object. * @return array Links for the given customer download. */ protected function prepare_links( $download, $request ) { $base = str_replace( '(?P<customer_id>[\d]+)', $request['customer_id'], $this->rest_base ); $links = array( 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'product' => array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ), ), 'order' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ), ), ); return $links; } /** * Get the Customer Download's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer_download', 'type' => 'object', 'properties' => array( 'download_url' => array( 'description' => __( 'Download file URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'download_id' => array( 'description' => __( 'Download ID (MD5).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Downloadable product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'download_name' => array( 'description' => __( 'Downloadable file name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'order_id' => array( 'description' => __( 'Order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'downloads_remaining' => array( 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'access_expires' => array( 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File details.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php 0000644 00000017117 15132754524 0026645 0 ustar 00 <?php /** * REST API Product Attribute Terms controller * * Handles requests to the products/attributes/<attribute_id>/terms endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Attribute Terms controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/attributes/(?P<attribute_id>[\d]+)/terms'; /** * Register the routes for terms. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'type' => 'string', 'description' => __( 'Name for the resource.', 'woocommerce' ), 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), )); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 'args' => array( 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Prepare a single product attribute term output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { // Get term order. $menu_order = get_term_meta( $item->term_id, 'order_' . $this->taxonomy, true ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Update term meta fields. * * @param WP_Term $term * @param WP_REST_Request $request * @return bool|WP_Error */ protected function update_term_meta_fields( $term, $request ) { $id = (int) $term->term_id; update_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] ); return true; } /** * Get the Attribute Term's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_attribute_term', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php 0000644 00000261642 15132754524 0023703 0 ustar 00 <?php /** * REST API Products controller * * Handles requests to the /products endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Products controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Posts_Controller */ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products'; /** * Post type. * * @var string */ protected $post_type = 'product'; /** * Initialize product actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); } /** * Register the routes for products. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), 'type' => 'boolean', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get post types. * * @return array */ protected function get_post_types() { return array( 'product', 'product_variation' ); } /** * Query args. * * @param array $args Request args. * @param WP_REST_Request $request Request data. * @return array */ public function query_args( $args, $request ) { // Set post_status. $args['post_status'] = $request['status']; // Taxonomy query to filter products by type, category, // tag, shipping class, and attribute. $tax_query = array(); // Map between taxonomy name and arg's key. $taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', 'product_shipping_class' => 'shipping_class', ); // Set tax_query for each passed arg. foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) { $request[ $key ] = array_filter( $request[ $key ] ); } if ( ! empty( $request[ $key ] ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], ); } } // Filter product type by slug. if ( ! empty( $request['type'] ) ) { $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], ); } // Filter by attribute and term. if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $tax_query[] = array( 'taxonomy' => $request['attribute'], 'field' => 'term_id', 'terms' => $request['attribute_term'], ); } } if ( ! empty( $tax_query ) ) { $args['tax_query'] = $tax_query; } // Filter by sku. if ( ! empty( $request['sku'] ) ) { $skus = explode( ',', $request['sku'] ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $request['sku']; } $args['meta_query'] = $this->add_meta_query( $args, array( 'key' => '_sku', 'value' => $skus, 'compare' => 'IN', ) ); } // Apply all WP_Query filters again. if ( is_array( $request['filter'] ) ) { $args = array_merge( $args, $request['filter'] ); unset( $args['filter'] ); } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = array( 'product', 'product_variation' ); } else { $args['post_type'] = $this->post_type; } return $args; } /** * Get the downloads for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * @return array */ protected function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_downloads() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // MD5 hash. 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get taxonomy terms. * * @param WC_Product $product Product instance. * @param string $taxonomy Taxonomy slug. * @return array */ protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { $terms = array(); foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { $terms[] = array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ); } return $terms; } /** * Get the images for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * @return array */ protected function get_images( $product ) { $images = array(); $attachment_ids = array(); // Add featured image. if ( $product->get_image_id() ) { $attachment_ids[] = $product->get_image_id(); } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'name' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => (int) $position, ); } // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), 'src' => wc_placeholder_img_src(), 'name' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Get attribute taxonomy label. * * @param string $name Taxonomy name. * @return string */ protected function get_attribute_taxonomy_label( $name ) { $tax = get_taxonomy( $name ); $labels = get_taxonomy_labels( $tax ); return $labels->singular_name; } /** * Get default attributes. * * @param WC_Product $product Product instance. * @return array */ protected function get_default_attributes( $product ) { $default = array(); if ( $product->is_type( 'variable' ) ) { foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { if ( 0 === strpos( $key, 'pa_' ) ) { $default[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $key ), 'name' => $this->get_attribute_taxonomy_label( $key ), 'option' => $value, ); } else { $default[] = array( 'id' => 0, 'name' => wc_attribute_taxonomy_slug( $key ), 'option' => $value, ); } } } return $default; } /** * Get attribute options. * * @param int $product_id Product ID. * @param array $attribute Attribute data. * @return array */ protected function get_attribute_options( $product_id, $attribute ) { if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); } elseif ( isset( $attribute['value'] ) ) { return array_map( 'trim', explode( '|', $attribute['value'] ) ); } return array(); } /** * Get the attributes for a product or product variation. * * @param WC_Product|WC_Product_Variation $product Product instance. * @return array */ protected function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // Variation attributes. foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { $name = str_replace( 'attribute_', '', $attribute_name ); if ( ! $attribute ) { continue; } // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { $option_term = get_term_by( 'slug', $attribute, $name ); $attributes[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $name ), 'name' => $this->get_attribute_taxonomy_label( $name ), 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, ); } else { $attributes[] = array( 'id' => 0, 'name' => $name, 'option' => $attribute, ); } } } else { foreach ( $product->get_attributes() as $attribute ) { if ( $attribute['is_taxonomy'] ) { $attributes[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } else { $attributes[] = array( 'id' => 0, 'name' => $attribute['name'], 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } } } return $attributes; } /** * Get product menu order. * * @deprecated 3.0.0 * @param WC_Product $product Product instance. * @return int */ protected function get_product_menu_order( $product ) { return $product->get_menu_order(); } /** * Get product data. * * @param WC_Product $product Product instance. * @return array */ protected function get_product_data( $product ) { $data = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'slug' => $product->get_slug(), 'permalink' => $product->get_permalink(), 'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ), 'type' => $product->get_type(), 'status' => $product->get_status(), 'featured' => $product->is_featured(), 'catalog_visibility' => $product->get_catalog_visibility(), 'description' => wpautop( do_shortcode( $product->get_description() ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), 'sku' => $product->get_sku(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', 'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '', 'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '', 'price_html' => $product->get_price_html(), 'on_sale' => $product->is_on_sale(), 'purchasable' => $product->is_purchasable(), 'total_sales' => $product->get_total_sales(), 'virtual' => $product->is_virtual(), 'downloadable' => $product->is_downloadable(), 'downloads' => $this->get_downloads( $product ), 'download_limit' => $product->get_download_limit(), 'download_expiry' => $product->get_download_expiry(), 'download_type' => 'standard', 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'manage_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders' => $product->get_backorders(), 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'weight' => $product->get_weight(), 'dimensions' => array( 'length' => $product->get_length(), 'width' => $product->get_width(), 'height' => $product->get_height(), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => $product->get_shipping_class_id(), 'reviews_allowed' => $product->get_reviews_allowed(), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), 'parent_id' => $product->get_parent_id(), 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), 'categories' => $this->get_taxonomy_terms( $product ), 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), 'images' => $this->get_images( $product ), 'attributes' => $this->get_attributes( $product ), 'default_attributes' => $this->get_default_attributes( $product ), 'variations' => array(), 'grouped_products' => array(), 'menu_order' => $product->get_menu_order(), ); return $data; } /** * Get an individual variation's data. * * @param WC_Product $product Product instance. * @return array */ protected function get_variation_data( $product ) { $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = wc_get_product( $child_id ); if ( ! $variation || ! $variation->exists() ) { continue; } $variations[] = array( 'id' => $variation->get_id(), 'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price(), 'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '', 'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '', 'on_sale' => $variation->is_on_sale(), 'purchasable' => $variation->is_purchasable(), 'visible' => $variation->is_visible(), 'virtual' => $variation->is_virtual(), 'downloadable' => $variation->is_downloadable(), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1, 'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1, 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'manage_stock' => $variation->managing_stock(), 'stock_quantity' => $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backorders' => $variation->get_backorders(), 'backorders_allowed' => $variation->backorders_allowed(), 'backordered' => $variation->is_on_backorder(), 'weight' => $variation->get_weight(), 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => $variation->get_shipping_class_id(), 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), ); } return $variations; } /** * Prepare a single product output for response. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_item_for_response( $post, $request ) { $product = wc_get_product( $post ); $data = $this->get_product_data( $product ); // Add variations to variable products. if ( $product->is_type( 'variable' ) && $product->has_child() ) { $data['variations'] = $this->get_variation_data( $product ); } // Add grouped products data. if ( $product->is_type( 'grouped' ) && $product->has_child() ) { $data['grouped_products'] = $product->get_children(); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $product, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Prepare links for the request. * * @param WC_Product $product Product object. * @param WP_REST_Request $request Request object. * @return array Links for the given product. */ protected function prepare_links( $product, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( $product->get_parent_id() ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ), ); } return $links; } /** * Prepare a single product for create or update. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; // Type is the most important part here because we need to be using the correct class and methods. if ( isset( $request['type'] ) ) { $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); if ( ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } $product = new $classname( $id ); } elseif ( isset( $request['id'] ) ) { $product = wc_get_product( $id ); } else { $product = new WC_Product_Simple(); } // Post title. if ( isset( $request['name'] ) ) { $product->set_name( wp_filter_post_kses( $request['name'] ) ); } // Post content. if ( isset( $request['description'] ) ) { $product->set_description( wp_filter_post_kses( $request['description'] ) ); } // Post excerpt. if ( isset( $request['short_description'] ) ) { $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); } // Post status. if ( isset( $request['status'] ) ) { $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); } // Post slug. if ( isset( $request['slug'] ) ) { $product->set_slug( $request['slug'] ); } // Menu order. if ( isset( $request['menu_order'] ) ) { $product->set_menu_order( $request['menu_order'] ); } // Comment status. if ( isset( $request['reviews_allowed'] ) ) { $product->set_reviews_allowed( $request['reviews_allowed'] ); } /** * Filter the query_vars used in `get_items` for the constructed query. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for insertion. * * @param WC_Product $product An object representing a single item prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request ); } /** * Create a single product. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $product_id = 0; try { $product_id = $this->save_product( $request ); $post = get_post( $product_id ); $this->update_additional_fields_for_object( $post, $request ); $this->update_post_meta_fields( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( 'woocommerce_rest_insert_product', $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } catch ( WC_Data_Exception $e ) { $this->delete_post( $product_id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { $this->delete_post( $product_id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update a single product. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $post_id = (int) $request['id']; if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } try { $product_id = $this->save_product( $request ); $post = get_post( $product_id ); $this->update_additional_fields_for_object( $post, $request ); $this->update_post_meta_fields( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( 'woocommerce_rest_insert_product', $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Saves a product to the database. * * @param WP_REST_Request $request Full details about the request. * @return int */ public function save_product( $request ) { $product = $this->prepare_item_for_database( $request ); return $product->save(); } /** * Save product images. * * @deprecated 3.0.0 * @param int $product_id * @param array $images * @throws WC_REST_Exception */ protected function save_product_images( $product_id, $images ) { $product = wc_get_product( $product_id ); return set_product_images( $product, $images ); } /** * Set product images. * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param array $images Images data. * @return WC_Product */ protected function set_product_images( $product, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } else { continue; } } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); } if ( ! wp_attachment_is_image( $attachment_id ) ) { throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { $product->set_image_id( $attachment_id ); } else { $gallery[] = $attachment_id; } // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); } } if ( ! empty( $gallery ) ) { $product->set_gallery_image_ids( $gallery ); } } else { $product->set_image_id( '' ); $product->set_gallery_image_ids( array() ); } return $product; } /** * Save product shipping data. * * @param WC_Product $product Product instance. * @param array $data Shipping data. * @return WC_Product */ protected function save_product_shipping_data( $product, $data ) { // Virtual. if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { $product->set_weight( '' ); $product->set_height( '' ); $product->set_length( '' ); $product->set_width( '' ); } else { if ( isset( $data['weight'] ) ) { $product->set_weight( $data['weight'] ); } // Height. if ( isset( $data['dimensions']['height'] ) ) { $product->set_height( $data['dimensions']['height'] ); } // Width. if ( isset( $data['dimensions']['width'] ) ) { $product->set_width( $data['dimensions']['width'] ); } // Length. if ( isset( $data['dimensions']['length'] ) ) { $product->set_length( $data['dimensions']['length'] ); } } // Shipping class. if ( isset( $data['shipping_class'] ) ) { $data_store = $product->get_data_store(); $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } return $product; } /** * Save downloadable files. * * @param WC_Product $product Product instance. * @param array $downloads Downloads data. * @param int $deprecated Deprecated since 3.0. * @return WC_Product */ protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { if ( $deprecated ) { wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); } $files = array(); foreach ( $downloads as $key => $file ) { if ( empty( $file['file'] ) ) { continue; } $download = new WC_Product_Download(); $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); $files[] = $download; } $product->set_downloads( $files ); return $product; } /** * Save taxonomy terms. * * @param WC_Product $product Product instance. * @param array $terms Terms data. * @param string $taxonomy Taxonomy name. * @return WC_Product */ protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { $term_ids = wp_list_pluck( $terms, 'id' ); if ( 'cat' === $taxonomy ) { $product->set_category_ids( $term_ids ); } elseif ( 'tag' === $taxonomy ) { $product->set_tag_ids( $term_ids ); } return $product; } /** * Save default attributes. * * @since 3.0.0 * * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * @return WC_Product */ protected function save_default_attributes( $product, $request ) { if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { $attributes = $product->get_attributes(); $default_attributes = array(); foreach ( $request['default_attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( isset( $attributes[ $attribute_name ] ) ) { $_attribute = $attributes[ $attribute_name ]; if ( $_attribute['is_variation'] ) { $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( ! empty( $_attribute['is_taxonomy'] ) ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $value = $term->slug; } else { $value = sanitize_title( $value ); } } if ( $value ) { $default_attributes[ $attribute_name ] = $value; } } } } $product->set_default_attributes( $default_attributes ); } return $product; } /** * Save product meta. * * @deprecated 3.0.0 * @param WC_Product $product * @param WP_REST_Request $request * @return bool * @throws WC_REST_Exception */ protected function save_product_meta( $product, $request ) { $product = $this->set_product_meta( $product, $request ); $product->save(); return true; } /** * Set product meta. * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * @return WC_Product */ protected function set_product_meta( $product, $request ) { // Virtual. if ( isset( $request['virtual'] ) ) { $product->set_virtual( $request['virtual'] ); } // Tax status. if ( isset( $request['tax_status'] ) ) { $product->set_tax_status( $request['tax_status'] ); } // Tax Class. if ( isset( $request['tax_class'] ) ) { $product->set_tax_class( $request['tax_class'] ); } // Catalog Visibility. if ( isset( $request['catalog_visibility'] ) ) { $product->set_catalog_visibility( $request['catalog_visibility'] ); } // Purchase Note. if ( isset( $request['purchase_note'] ) ) { $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); } // Featured Product. if ( isset( $request['featured'] ) ) { $product->set_featured( $request['featured'] ); } // Shipping data. $product = $this->save_product_shipping_data( $product, $request ); // SKU. if ( isset( $request['sku'] ) ) { $product->set_sku( wc_clean( $request['sku'] ) ); } // Attributes. if ( isset( $request['attributes'] ) ) { $attributes = array(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = wc_clean( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( $attribute_id ) { if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names. $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Custom attribute - Add attribute to array and set the values. if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; } else { $values = explode( WC_DELIMITER, $attribute['options'] ); } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $request['regular_price'] ) ) { $product->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $product->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_to'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to'] ); } } // Product parent ID for groups. if ( isset( $request['parent_id'] ) ) { $product->set_parent_id( $request['parent_id'] ); } // Sold individually. if ( isset( $request['sold_individually'] ) ) { $product->set_sold_individually( $request['sold_individually'] ); } // Stock status. if ( isset( $request['in_stock'] ) ) { $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; } else { $stock_status = $product->get_stock_status(); } // Stock data. if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $product->set_manage_stock( $request['manage_stock'] ); } // Backorders. if ( isset( $request['backorders'] ) ) { $product->set_backorders( $request['backorders'] ); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( $product->get_manage_stock() ) { // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity. if ( isset( $request['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells. if ( isset( $request['upsell_ids'] ) ) { $upsells = array(); $ids = $request['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } } $product->set_upsell_ids( $upsells ); } // Cross sells. if ( isset( $request['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $request['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } } $product->set_cross_sell_ids( $crosssells ); } // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); } // Downloadable. if ( isset( $request['downloadable'] ) ) { $product->set_downloadable( $request['downloadable'] ); } // Downloadable options. if ( $product->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $product->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $product->set_download_expiry( $request['download_expiry'] ); } } // Product url and button text for external products. if ( $product->is_type( 'external' ) ) { if ( isset( $request['external_url'] ) ) { $product->set_product_url( $request['external_url'] ); } if ( isset( $request['button_text'] ) ) { $product->set_button_text( $request['button_text'] ); } } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $request ); } return $product; } /** * Save variations. * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * @return bool */ protected function save_variations_data( $product, $request ) { foreach ( $request['variations'] as $menu_order => $data ) { $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); // Create initial name and status. if ( ! $variation->get_slug() ) { /* translators: 1: variation id 2: product name */ $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); } // Parent ID. $variation->set_parent_id( $product->get_id() ); // Menu order. $variation->set_menu_order( $menu_order ); // Status. if ( isset( $data['visible'] ) ) { $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); } // SKU. if ( isset( $data['sku'] ) ) { $variation->set_sku( wc_clean( $data['sku'] ) ); } // Thumbnail. if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { $image = $data['image']; $image = current( $image ); if ( is_array( $image ) ) { $image['position'] = 0; } $variation = $this->set_product_images( $variation, array( $image ) ); } // Virtual variation. if ( isset( $data['virtual'] ) ) { $variation->set_virtual( $data['virtual'] ); } // Downloadable variation. if ( isset( $data['downloadable'] ) ) { $variation->set_downloadable( $data['downloadable'] ); } // Downloads. if ( $variation->get_downloadable() ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { $variation->set_download_limit( $data['download_limit'] ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { $variation->set_download_expiry( $data['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $data ); // Stock handling. if ( isset( $data['manage_stock'] ) ) { $variation->set_manage_stock( $data['manage_stock'] ); } if ( isset( $data['in_stock'] ) ) { $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); } if ( isset( $data['backorders'] ) ) { $variation->set_backorders( $data['backorders'] ); } if ( $variation->get_manage_stock() ) { if ( isset( $data['stock_quantity'] ) ) { $variation->set_stock_quantity( $data['stock_quantity'] ); } elseif ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); $variation->set_stock_quantity( $stock_quantity ); } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); } // Regular Price. if ( isset( $data['regular_price'] ) ) { $variation->set_regular_price( $data['regular_price'] ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $variation->set_sale_price( $data['sale_price'] ); } if ( isset( $data['date_on_sale_from'] ) ) { $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); } if ( isset( $data['date_on_sale_to'] ) ) { $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); } // Tax class. if ( isset( $data['tax_class'] ) ) { $variation->set_tax_class( $data['tax_class'] ); } // Description. if ( isset( $data['description'] ) ) { $variation->set_description( wp_kses_post( $data['description'] ) ); } // Update taxonomies. if ( isset( $data['attributes'] ) ) { $attributes = array(); $parent_attributes = $product->get_attributes(); foreach ( $data['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { continue; } $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $attribute_value = $term->slug; } else { $attribute_value = sanitize_title( $attribute_value ); } } $attributes[ $attribute_key ] = $attribute_value; } $variation->set_attributes( $attributes ); } $variation->save(); do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); } return true; } /** * Add post meta fields. * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request data. * @return bool|WP_Error */ protected function add_post_meta_fields( $post, $request ) { return $this->update_post_meta_fields( $post, $request ); } /** * Update post meta fields. * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request data. * @return bool|WP_Error */ protected function update_post_meta_fields( $post, $request ) { $product = wc_get_product( $post ); // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $product = $this->set_product_images( $product, $request['images'] ); } // Save product meta fields. $product = $this->set_product_meta( $product, $request ); // Save the product data. $product->save(); // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { $this->save_variations_data( $product, $request ); } } // Clear caches here so in sync with any new variations/children. wc_delete_product_transients( $product->get_id() ); wp_cache_delete( 'product-' . $product->get_id(), 'products' ); return true; } /** * Clear cache/transients. * * @param WP_Post $post Post data. */ public function clear_transients( $post ) { wc_delete_product_transients( $post->ID ); } /** * Delete post. * * @param int|WP_Post $id Post ID or WP_Post instance. */ protected function delete_post( $id ) { if ( ! empty( $id->ID ) ) { $id = $id->ID; } elseif ( ! is_numeric( $id ) || 0 >= $id ) { return; } // Delete product attachments. $attachments = get_posts( array( 'post_parent' => $id, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product. $product = wc_get_product( $id ); $product->delete( true ); } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = (bool) $request['force']; $post = get_post( $id ); $product = wc_get_product( $id ); if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0; /** * Filter whether an item is trashable. * * Return false to disable trash support for the item. * * @param boolean $supports_trash Whether the item type support trashing. * @param WP_Post $post The Post object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); // If we're forcing, then delete permanently. if ( $force ) { if ( $product->is_type( 'variable' ) ) { foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->delete( true ); } } } else { // For other product types, if the product has children, remove the relationship. foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->set_parent_id( 0 ); $child->save(); } } } $product->delete( true ); $result = ! ( $product->get_id() > 0 ); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); } // Otherwise, only trash if we haven't already. if ( 'trash' === $post->post_status ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); } // (Note that internally this falls through to `wp_delete_post` if // the trash is disabled.) $product->delete(); $result = 'trash' === $product->get_status(); } if ( ! $result ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } // Delete parent product transients. if ( $parent_id = wp_get_post_parent_id( $id ) ) { wc_delete_product_transients( $parent_id ); } /** * Fires after a single item is deleted or trashed via the REST API. * * @param object $post The deleted or trashed item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); return $response; } /** * Get the Product's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'slug' => array( 'description' => __( 'Product slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'default' => 'simple', 'enum' => array_keys( wc_get_product_types() ), 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Product status (post status).', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), 'context' => array( 'view', 'edit' ), ), 'featured' => array( 'description' => __( 'Featured product.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'catalog_visibility' => array( 'description' => __( 'Catalog visibility.', 'woocommerce' ), 'type' => 'string', 'default' => 'visible', 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Product description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'short_description' => array( 'description' => __( 'Product short description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Product regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Product sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( 'End date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price_html' => array( 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'on_sale' => array( 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_sales' => array( 'description' => __( 'Amount of sales.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the product is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the product is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_type' => array( 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), 'type' => 'string', 'default' => 'standard', 'enum' => array( 'standard' ), 'context' => array( 'view', 'edit' ), ), 'external_url' => array( 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'button_text' => array( 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sold_individually' => array( 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Product dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_taxable' => array( 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reviews_allowed' => array( 'description' => __( 'Allow reviews.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'average_rating' => array( 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rating_count' => array( 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'related_ids' => array( 'description' => __( 'List of related products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'upsell_ids' => array( 'description' => __( 'List of upsell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'cross_sell_ids' => array( 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'parent_id' => array( 'description' => __( 'Product parent ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'purchase_note' => array( 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'categories' => array( 'description' => __( 'List of categories.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Category ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Category slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'tags' => array( 'description' => __( 'List of tags.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tag ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Tag slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'images' => array( 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Attribute position.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'visible' => array( 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'variation' => array( 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'options' => array( 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), ), ), ), ), 'default_attributes' => array( 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'variations' => array( 'description' => __( 'List of variations.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Variation ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'permalink' => array( 'description' => __( 'Variation URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current variation price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Variation regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Variation sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( 'End date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'on_sale' => array( 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'visible' => array( 'description' => __( 'If the variation is visible.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), ), 'virtual' => array( 'description' => __( 'If the variation is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at variation level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Variation dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'image' => array( 'description' => __( 'Variation image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'grouped_products' => array( 'description' => __( 'List of grouped products ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['slug'] = array( 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any', 'future' ), array_keys( get_post_statuses() ) ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['type'] = array( 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_types() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['category'] = array( 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag'] = array( 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['shipping_class'] = array( 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute'] = array( 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute_term'] = array( 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['sku'] = array( 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php 0000644 00000042102 15132754524 0024603 0 ustar 00 <?php /** * REST API Order Refunds controller * * Handles requests to the /orders/<order_id>/refunds endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Order Refunds controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Orders_V1_Controller */ class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds'; /** * Post type. * * @var string */ protected $post_type = 'shop_order_refund'; /** * Order refunds actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for order refunds. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => true, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Prepare a single order refund output for response. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * * @return WP_Error|WP_REST_Response */ public function prepare_item_for_response( $post, $request ) { $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order ) { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } $refund = wc_get_order( $post ); if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); } $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); $data = array( 'id' => $refund->get_id(), 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), 'reason' => $refund->get_reason(), 'line_items' => array(), ); // Add line items. foreach ( $refund->get_items() as $item_id => $item ) { $product = $item->get_product(); $product_id = 0; $variation_id = 0; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = $item->get_product_id(); $variation_id = $item->get_variation_id(); $product_sku = $product->get_sku(); } $item_meta = array(); $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta->key, 'label' => $formatted_meta->display_key, 'value' => wc_clean( $formatted_meta->display_value ), ); } $line_item = array( 'id' => $item_id, 'name' => $item['name'], 'sku' => $product_sku, 'product_id' => (int) $product_id, 'variation_id' => (int) $variation_id, 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'taxes' => array(), 'meta' => $item_meta, ); $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); if ( isset( $item_line_taxes['total'] ) ) { $line_tax = array(); foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ]['subtotal'] = $tax; } $line_item['taxes'] = array_values( $line_tax ); } $data['line_items'][] = $line_item; } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $refund, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Prepare links for the request. * * @param WC_Order_Refund $refund Comment object. * @param WP_REST_Request $request Request object. * @return array Links for the given order refund. */ protected function prepare_links( $refund, $request ) { $order_id = $refund->get_parent_id(); $base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), ), ); return $links; } /** * Query args. * * @param array $args Request args. * @param WP_REST_Request $request Request object. * @return array */ public function query_args( $args, $request ) { $args['post_status'] = array_keys( wc_get_order_statuses() ); $args['post_parent__in'] = array( absint( $request['order_id'] ) ); return $args; } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order_data = get_post( (int) $request['order_id'] ); if ( empty( $order_data ) ) { return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); } if ( 0 > $request['amount'] ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); } // Create the refund. $refund = wc_create_refund( array( 'order_id' => $order_data->ID, 'amount' => $request['amount'], 'reason' => empty( $request['reason'] ) ? null : $request['reason'], 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, 'restock_items' => true, ) ); if ( is_wp_error( $refund ) ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); } if ( ! $refund ) { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } $post = get_post( $refund->get_id() ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'amount' => array( 'description' => __( 'Refund amount.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'reason' => array( 'description' => __( 'Reason for refund.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta' => array( 'description' => __( 'Line item meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Meta label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['dp'] = array( 'default' => wc_get_price_decimals(), 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php 0000644 00000047475 15132754524 0025210 0 ustar 00 <?php /** * REST API Product Reviews Controller * * Handles requests to /products/<product_id>/reviews. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Reviews Controller Class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews'; /** * Register the routes for product reviews. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'review' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Review content.', 'woocommerce' ), ), 'name' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Name of the reviewer.', 'woocommerce' ), ), 'email' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Email of the reviewer.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read webhook deliveries. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $post = get_post( (int) $request['product_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create a new product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { $post = get_post( (int) $request['product_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { $post = get_post( (int) $request['product_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { $post = get_post( (int) $request['product_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all reviews from a product. * * @param WP_REST_Request $request * * @return array|WP_Error */ public function get_items( $request ) { $product_id = (int) $request['product_id']; if ( 'product' !== get_post_type( $product_id ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $reviews = get_approved_comments( $product_id ); $data = array(); foreach ( $reviews as $review_data ) { $review = $this->prepare_item_for_response( $review_data, $request ); $review = $this->prepare_response_for_collection( $review ); $data[] = $review; } return rest_ensure_response( $data ); } /** * Get a single product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $product_id = (int) $request['product_id']; if ( 'product' !== get_post_type( $product_id ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $review = get_comment( $id ); if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $delivery = $this->prepare_item_for_response( $review, $request ); $response = rest_ensure_response( $delivery ); return $response; } /** * Create a product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { $product_id = (int) $request['product_id']; if ( 'product' !== get_post_type( $product_id ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $prepared_review = $this->prepare_item_for_database( $request ); /** * Filter a product review (comment) before it is inserted via the REST API. * * Allows modification of the comment right before it is inserted via `wp_insert_comment`. * * @param array $prepared_review The prepared comment data for `wp_insert_comment`. * @param WP_REST_Request $request Request used to insert the comment. */ $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); $product_review_id = wp_insert_comment( $prepared_review ); if ( ! $product_review_id ) { return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); } update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); $product_review = get_comment( $product_review_id ); $this->update_additional_fields_for_object( $product_review, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Comment $product_review Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $product_review, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); return $response; } /** * Update a single product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $product_review_id = (int) $request['id']; $product_id = (int) $request['product_id']; if ( 'product' !== get_post_type( $product_id ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $review = get_comment( $product_review_id ); if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $prepared_review = $this->prepare_item_for_database( $request ); $updated = wp_update_comment( $prepared_review ); if ( 0 === $updated ) { return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); } if ( ! empty( $request['rating'] ) ) { update_comment_meta( $product_review_id, 'rating', $request['rating'] ); } $product_review = get_comment( $product_review_id ); $this->update_additional_fields_for_object( $product_review, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Comment $comment Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $product_review, $request ); return rest_ensure_response( $response ); } /** * Delete a product review. * * @param WP_REST_Request $request Full details about the request * * @return bool|WP_Error|WP_REST_Response */ public function delete_item( $request ) { $product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); $force = isset( $request['force'] ) ? (bool) $request['force'] : false; $product_review = get_comment( $product_review_id ); if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); } /** * Filter whether a product review is trashable. * * Return false to disable trash support for the product review. * * @param boolean $supports_trash Whether the object supports trashing. * @param WP_Post $product_review The object being considered for trashing support. */ $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $product_review, $request ); if ( $force ) { $result = wp_delete_comment( $product_review_id, true ); } else { if ( ! $supports_trash ) { return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } if ( 'trash' === $product_review->comment_approved ) { return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); } $result = wp_trash_comment( $product_review->comment_ID ); } if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a product review is deleted via the REST API. * * @param object $product_review The deleted item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'rest_delete_product_review', $product_review, $response, $request ); return $response; } /** * Prepare a single product review output for response. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $review, $request ) { $data = array( 'id' => (int) $review->comment_ID, 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), 'review' => $review->comment_content, 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), 'name' => $review->comment_author, 'email' => $review->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $review, $request ) ); /** * Filter product reviews object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); } /** * Prepare a single product review to be inserted into the database. * * @param WP_REST_Request $request Request object. * @return array|WP_Error $prepared_review */ protected function prepare_item_for_database( $request ) { $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); if ( isset( $request['id'] ) ) { $prepared_review['comment_ID'] = (int) $request['id']; } if ( isset( $request['review'] ) ) { $prepared_review['comment_content'] = $request['review']; } if ( isset( $request['product_id'] ) ) { $prepared_review['comment_post_ID'] = (int) $request['product_id']; } if ( isset( $request['name'] ) ) { $prepared_review['comment_author'] = $request['name']; } if ( isset( $request['email'] ) ) { $prepared_review['comment_author_email'] = $request['email']; } if ( isset( $request['date_created'] ) ) { $prepared_review['comment_date'] = $request['date_created']; } if ( isset( $request['date_created_gmt'] ) ) { $prepared_review['comment_date_gmt'] = $request['date_created_gmt']; } return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); } /** * Prepare links for the request. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return array Links for the given product review. */ protected function prepare_links( $review, $request ) { $product_id = (int) $request['product_id']; $base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), ), ); return $links; } /** * Get the Product Review's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_review', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'review' => array( 'description' => __( 'The content of the review.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'rating' => array( 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Reviewer name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Reviewer email.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'verified' => array( 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php 0000644 00000076003 15132754524 0024057 0 ustar 00 <?php /** * REST API Customers controller * * Handles requests to the /customers endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Customers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Customers_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'customers'; /** * Register the routes for customers. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'email' => array( 'required' => true, 'type' => 'string', 'description' => __( 'New user email address.', 'woocommerce' ), ), 'username' => array( 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), 'description' => __( 'New user username.', 'woocommerce' ), 'type' => 'string', ), 'password' => array( 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), 'description' => __( 'New user password.', 'woocommerce' ), 'type' => 'string', ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), 'reassign' => array( 'default' => 0, 'type' => 'integer', 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create customers. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access update a customer. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function update_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a customer. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $prepared_args = array(); $prepared_args['exclude'] = $request['exclude']; $prepared_args['include'] = $request['include']; $prepared_args['order'] = $request['order']; $prepared_args['number'] = $request['per_page']; if ( ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } $orderby_possibles = array( 'id' => 'ID', 'include' => 'include', 'name' => 'display_name', 'registered_date' => 'registered', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['search'] = $request['search']; if ( '' !== $prepared_args['search'] ) { $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } // Filter by email. if ( ! empty( $request['email'] ) ) { $prepared_args['search'] = $request['email']; $prepared_args['search_columns'] = array( 'user_email' ); } // Filter by role. if ( 'all' !== $request['role'] ) { $prepared_args['role'] = $request['role']; } /** * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. * * @see https://developer.wordpress.org/reference/classes/wp_user_query/ * * @param array $prepared_args Array of arguments for WP_User_Query. * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); $query = new WP_User_Query( $prepared_args ); $users = array(); foreach ( $query->results as $user ) { $data = $this->prepare_item_for_response( $user, $request ); $users[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $users ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $prepared_args['fields'] = 'ID'; $total_users = $query->get_total(); if ( $total_users < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'] ); unset( $prepared_args['offset'] ); $count_query = new WP_User_Query( $prepared_args ); $total_users = $count_query->get_total(); } $response->header( 'X-WP-Total', (int) $total_users ); $max_pages = ceil( $total_users / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Create a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { try { if ( ! empty( $request['id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); } // Sets the username. $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; // Sets the password. $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; // Create customer. $customer = new WC_Customer; $customer->set_username( $request['username'] ); $customer->set_password( $request['password'] ); $customer->set_email( $request['email'] ); $this->update_customer_meta_fields( $customer, $request ); $customer->save(); if ( ! $customer->get_id() ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); } $user_data = get_userdata( $customer->get_id() ); $this->update_additional_fields_for_object( $user_data, $request ); /** * Fires after a customer is created or updated via the REST API. * * @param WP_User $user_data Data used to create the customer. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating customer, false when updating customer. */ do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $user_data, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); return $response; } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $user_data = get_userdata( $id ); if ( empty( $id ) || empty( $user_data->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $customer = $this->prepare_item_for_response( $user_data, $request ); $response = rest_ensure_response( $customer ); return $response; } /** * Update a single user. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { try { $id = (int) $request['id']; $customer = new WC_Customer( $id ); if ( ! $customer->get_id() ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); } if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); } if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); } // Customer email. if ( isset( $request['email'] ) ) { $customer->set_email( sanitize_email( $request['email'] ) ); } // Customer password. if ( isset( $request['password'] ) ) { $customer->set_password( $request['password'] ); } $this->update_customer_meta_fields( $customer, $request ); $customer->save(); $user_data = get_userdata( $customer->get_id() ); $this->update_additional_fields_for_object( $user_data, $request ); if ( ! is_user_member_of_blog( $user_data->ID ) ) { $user_data->add_role( 'customer' ); } /** * Fires after a customer is created or updated via the REST API. * * @param WP_User $customer Data used to create the customer. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating customer, false when updating customer. */ do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $user_data, $request ); $response = rest_ensure_response( $response ); return $response; } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { $id = (int) $request['id']; $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $user_data = get_userdata( $id ); if ( ! $user_data ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); } } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $user_data, $request ); /** Include admin customer functions to get access to wp_delete_user() */ require_once ABSPATH . 'wp-admin/includes/user.php'; $customer = new WC_Customer( $id ); if ( ! is_null( $reassign ) ) { $result = $customer->delete_and_reassign( $reassign ); } else { $result = $customer->delete(); } if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a customer is deleted via the REST API. * * @param WP_User $user_data User data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); return $response; } /** * Prepare a single customer output for response. * * @param WP_User $user_data User object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $user_data, $request ) { $customer = new WC_Customer( $user_data->ID ); $_data = $customer->get_data(); $last_order = wc_get_customer_last_order( $customer->get_id() ); $format_date = array( 'date_created', 'date_modified' ); // Format date values. foreach ( $format_date as $key ) { $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. } $data = array( 'id' => $_data['id'], 'date_created' => $_data['date_created'], 'date_modified' => $_data['date_modified'], 'email' => $_data['email'], 'first_name' => $_data['first_name'], 'last_name' => $_data['last_name'], 'username' => $_data['username'], 'last_order' => array( 'id' => is_object( $last_order ) ? $last_order->get_id() : null, 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. ), 'orders_count' => $customer->get_order_count(), 'total_spent' => $customer->get_total_spent(), 'avatar_url' => $customer->get_avatar_url(), 'billing' => $_data['billing'], 'shipping' => $_data['shipping'], ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $user_data ) ); /** * Filter customer data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_User $user_data User object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); } /** * Update customer meta fields. * * @param WC_Customer $customer * @param WP_REST_Request $request */ protected function update_customer_meta_fields( $customer, $request ) { $schema = $this->get_item_schema(); // Customer first name. if ( isset( $request['first_name'] ) ) { $customer->set_first_name( wc_clean( $request['first_name'] ) ); } // Customer last name. if ( isset( $request['last_name'] ) ) { $customer->set_last_name( wc_clean( $request['last_name'] ) ); } // Customer billing address. if ( isset( $request['billing'] ) ) { foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); } } } // Customer shipping address. if ( isset( $request['shipping'] ) ) { foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); } } } } /** * Prepare links for the request. * * @param WP_User $customer Customer object. * @return array Links for the given customer. */ protected function prepare_links( $customer ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Customer's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'email' => array( 'description' => __( 'The email address for the customer.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'first_name' => array( 'description' => __( 'Customer first name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Customer last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'username' => array( 'description' => __( 'Customer login name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_user', ), ), 'password' => array( 'description' => __( 'Customer password.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'last_order' => array( 'description' => __( 'Last order data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => array( 'id' => array( 'description' => __( 'Last order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date' => array( 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), 'orders_count' => array( 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_spent' => array( 'description' => __( 'Total amount spent.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'avatar_url' => array( 'description' => __( 'Avatar URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing' => array( 'description' => __( 'List of billing address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'List of shipping address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get role names. * * @return array */ protected function get_role_names() { global $wp_roles; return array_keys( $wp_roles->role_names ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'default' => 'name', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( 'id', 'include', 'name', 'registered_date', ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['email'] = array( 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'validate_callback' => 'rest_validate_request_arg', ); $params['role'] = array( 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), 'type' => 'string', 'default' => 'customer', 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php 0000644 00000047153 15132754524 0023525 0 ustar 00 <?php /** * REST API Coupons controller * * Handles requests to the /coupons endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Coupons controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Posts_Controller */ class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'coupons'; /** * Post type. * * @var string */ protected $post_type = 'shop_coupon'; /** * Coupons actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for coupons. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'required' => true, 'type' => 'string', ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Query args. * * @param array $args Query args * @param WP_REST_Request $request Request data. * @return array */ public function query_args( $args, $request ) { if ( ! empty( $request['code'] ) ) { $id = wc_get_coupon_id_by_code( $request['code'] ); $args['post__in'] = array( $id ); } return $args; } /** * Prepare a single coupon output for response. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $data */ public function prepare_item_for_response( $post, $request ) { $coupon = new WC_Coupon( (int) $post->ID ); $_data = $coupon->get_data(); $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); $format_date = array( 'date_created', 'date_modified' ); $format_date_utc = array( 'date_expires' ); $format_null = array( 'usage_limit', 'usage_limit_per_user' ); // Format decimal values. foreach ( $format_decimal as $key ) { $_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 ); } // Format date values. foreach ( $format_date as $key ) { $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null; } foreach ( $format_date_utc as $key ) { $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; } // Format null values. foreach ( $format_null as $key ) { $_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null; } $data = array( 'id' => $_data['id'], 'code' => $_data['code'], 'date_created' => $_data['date_created'], 'date_modified' => $_data['date_modified'], 'discount_type' => $_data['discount_type'], 'description' => $_data['description'], 'amount' => $_data['amount'], 'expiry_date' => $_data['date_expires'], 'usage_count' => $_data['usage_count'], 'individual_use' => $_data['individual_use'], 'product_ids' => $_data['product_ids'], 'exclude_product_ids' => $_data['excluded_product_ids'], 'usage_limit' => $_data['usage_limit'], 'usage_limit_per_user' => $_data['usage_limit_per_user'], 'limit_usage_to_x_items' => $_data['limit_usage_to_x_items'], 'free_shipping' => $_data['free_shipping'], 'product_categories' => $_data['product_categories'], 'excluded_product_categories' => $_data['excluded_product_categories'], 'exclude_sale_items' => $_data['exclude_sale_items'], 'minimum_amount' => $_data['minimum_amount'], 'maximum_amount' => $_data['maximum_amount'], 'email_restrictions' => $_data['email_restrictions'], 'used_by' => $_data['used_by'], ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $post, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Only return writable props from schema. * @param array $schema * @return bool */ protected function filter_writable_props( $schema ) { return empty( $schema['readonly'] ); } /** * Prepare a single coupon for create or update. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $coupon = new WC_Coupon( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Update to schema to make compatible with CRUD schema. if ( $request['exclude_product_ids'] ) { $request['excluded_product_ids'] = $request['exclude_product_ids']; } if ( $request['expiry_date'] ) { $request['date_expires'] = $request['expiry_date']; } // Validate required POST fields. if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { if ( empty( $request['code'] ) ) { return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); } } // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'code' : $coupon_code = wc_format_coupon_code( $value ); $id = $coupon->get_id() ? $coupon->get_id() : 0; $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); } $coupon->set_code( $coupon_code ); break; case 'description' : $coupon->set_description( wp_filter_post_kses( $value ) ); break; case 'expiry_date' : $coupon->set_date_expires( $value ); break; default : if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { $coupon->{"set_{$key}"}( $value ); } break; } } } /** * Filter the query_vars used in `get_items` for the constructed query. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for insertion. * * @param WC_Coupon $coupon The coupon object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $coupon_id = $this->save_coupon( $request ); if ( is_wp_error( $coupon_id ) ) { return $coupon_id; } $post = get_post( $coupon_id ); $this->update_additional_fields_for_object( $post, $request ); $this->add_post_meta_fields( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } /** * Update a single coupon. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { try { $post_id = (int) $request['id']; if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $coupon_id = $this->save_coupon( $request ); if ( is_wp_error( $coupon_id ) ) { return $coupon_id; } $post = get_post( $coupon_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Saves a coupon to the database. * * @since 3.0.0 * @param WP_REST_Request $request Full details about the request. * @return WP_Error|int */ protected function save_coupon( $request ) { try { $coupon = $this->prepare_item_for_database( $request ); if ( is_wp_error( $coupon ) ) { return $coupon; } $coupon->save(); return $coupon->get_id(); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the Coupon's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Coupon description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount_type' => array( 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), 'type' => 'string', 'default' => 'fixed_cart', 'enum' => array_keys( wc_get_coupon_types() ), 'context' => array( 'view', 'edit' ), ), 'amount' => array( 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'expiry_date' => array( 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'usage_count' => array( 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'individual_use' => array( 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_ids' => array( 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'exclude_product_ids' => array( 'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'usage_limit' => array( 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'usage_limit_per_user' => array( 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'limit_usage_to_x_items' => array( 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'free_shipping' => array( 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_categories' => array( 'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'excluded_product_categories' => array( 'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'exclude_sale_items' => array( 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'minimum_amount' => array( 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'maximum_amount' => array( 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email_restrictions' => array( 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ), 'used_by' => array( 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['code'] = array( 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php 0000644 00000063356 15132754524 0023663 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Webhooks controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'webhooks'; /** * Post type. * * @var string */ protected $post_type = 'shop_webhook'; /** * Register the routes for webhooks. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'topic' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Webhook topic.', 'woocommerce' ), ), 'delivery_url' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read webhooks. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create webhooks. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access update a webhook. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function update_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a webhook. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'webhooks', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get the default REST API version. * * @since 3.0.0 * @return string */ protected function get_default_api_version() { return 'wp_api_v1'; } /** * Get all webhooks. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $args = array(); $args['order'] = $request['order']; $args['orderby'] = $request['orderby']; $args['status'] = 'all' === $request['status'] ? '' : $request['status']; $args['include'] = implode( ',', $request['include'] ); $args['exclude'] = implode( ',', $request['exclude'] ); $args['limit'] = $request['per_page']; $args['search'] = $request['search']; $args['before'] = $request['before']; $args['after'] = $request['after']; if ( empty( $request['offset'] ) ) { $args['offset'] = 1 < $request['page'] ? ( $request['page'] - 1 ) * $args['limit'] : 0; } /** * Filter arguments, before passing to WC_Webhook_Data_Store->search_webhooks, when querying webhooks via the REST API. * * @param array $args Array of arguments for $wpdb->get_results(). * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_webhook_query', $args, $request ); unset( $prepared_args['page'] ); $prepared_args['paginate'] = true; // Get the webhooks. $webhooks = array(); $data_store = WC_Data_Store::load( 'webhook' ); $results = $data_store->search_webhooks( $prepared_args ); $webhook_ids = $results->webhooks; foreach ( $webhook_ids as $webhook_id ) { $data = $this->prepare_item_for_response( $webhook_id, $request ); $webhooks[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $webhooks ); $per_page = (int) $prepared_args['limit']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $total_webhooks = $results->total; $max_pages = $results->max_num_pages; $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); $response->header( 'X-WP-Total', $total_webhooks ); $response->header( 'X-WP-TotalPages', $max_pages ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Get a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; if ( empty( $id ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = $this->prepare_item_for_response( $id, $request ); $response = rest_ensure_response( $data ); return $response; } /** * Create a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } // Validate topic. if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } // Validate delivery URL. if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } $webhook = new WC_Webhook(); $webhook->set_name( $post->post_title ); $webhook->set_user_id( $post->post_author ); $webhook->set_status( 'publish' === $post->post_status ? 'active' : 'disabled' ); $webhook->set_topic( $request['topic'] ); $webhook->set_delivery_url( $request['delivery_url'] ); $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : wp_generate_password( 50, true, true ) ); $webhook->set_api_version( $this->get_default_api_version() ); $webhook->save(); $this->update_additional_fields_for_object( $webhook, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WC_Webhook $webhook Webhook data. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $webhook->get_id() ) ) ); // Send ping. $webhook->deliver_ping(); return $response; } /** * Update a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $webhook = wc_get_webhook( $id ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } // Update topic. if ( ! empty( $request['topic'] ) ) { if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { $webhook->set_topic( $request['topic'] ); } else { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } } // Update delivery URL. if ( ! empty( $request['delivery_url'] ) ) { if ( wc_is_valid_url( $request['delivery_url'] ) ) { $webhook->set_delivery_url( $request['delivery_url'] ); } else { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); } } // Update secret. if ( ! empty( $request['secret'] ) ) { $webhook->set_secret( $request['secret'] ); } // Update status. if ( ! empty( $request['status'] ) ) { if ( wc_is_webhook_valid_status( strtolower( $request['status'] ) ) ) { $webhook->set_status( $request['status'] ); } else { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_status", __( 'Webhook status must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } if ( isset( $post->post_title ) ) { $webhook->set_name( $post->post_title ); } $webhook->save(); $this->update_additional_fields_for_object( $webhook, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WC_Webhook $webhook Webhook data. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); return rest_ensure_response( $response ); } /** * Delete a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $webhook = wc_get_webhook( $id ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $webhook, $request ); $result = $webhook->delete( true ); if ( ! $result ) { /* translators: %s: post type */ return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } /** * Fires after a single item is deleted or trashed via the REST API. * * @param WC_Webhook $webhook The deleted or trashed item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_webhook_object", $webhook, $response, $request ); return $response; } /** * Prepare a single webhook for create or update. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { $data = new stdClass; // Post ID. if ( isset( $request['id'] ) ) { $data->ID = absint( $request['id'] ); } // Validate required POST fields. if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine // Post author. $data->post_author = get_current_user_id(); // Post password. $data->post_password = 'webhook_' . wp_generate_password(); // Post status. $data->post_status = 'publish'; } else { // Allow edit post title. if ( ! empty( $request['name'] ) ) { $data->post_title = $request['name']; } } // Comment status. $data->comment_status = 'closed'; // Ping status. $data->ping_status = 'closed'; /** * Filter the query_vars used in `get_items` for the constructed query. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for insertion. * * @param stdClass $data An object representing a single item prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); } /** * Prepare a single webhook output for response. * * @param int $id Webhook ID or object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $id, $request ) { $webhook = wc_get_webhook( $id ); if ( empty( $webhook ) || is_null( $webhook ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $data = array( 'id' => $webhook->get_id(), 'name' => $webhook->get_name(), 'status' => $webhook->get_status(), 'topic' => $webhook->get_topic(), 'resource' => $webhook->get_resource(), 'event' => $webhook->get_event(), 'hooks' => $webhook->get_hooks(), 'delivery_url' => $webhook->get_delivery_url(), 'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created() ), 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $webhook->get_id() ) ); /** * Filter webhook object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Webhook $webhook Webhook object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); } /** * Prepare links for the request. * * @param int $id Webhook ID. * @return array */ protected function prepare_links( $id ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Webhook status.', 'woocommerce' ), 'type' => 'string', 'default' => 'active', 'enum' => array_keys( wc_get_webhook_statuses() ), 'context' => array( 'view', 'edit' ), ), 'topic' => array( 'description' => __( 'Webhook topic.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'resource' => array( 'description' => __( 'Webhook resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'event' => array( 'description' => __( 'Webhook event.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'hooks' => array( 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'delivery_url' => array( 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'secret' => array( 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'date_created' => array( 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['after'] = array( 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['before'] = array( 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'id', 'title', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['status'] = array( 'default' => 'all', 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'all', 'active', 'paused', 'disabled' ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php 0000644 00000007217 15132754524 0026766 0 ustar 00 <?php /** * REST API Product Shipping Classes controller * * Handles requests to the products/shipping_classes endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Shipping Classes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Shipping_Classes_V1_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/shipping_classes'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_shipping_class'; /** * Prepare a single product shipping class output for response. * * @param obj $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'count' => (int) $item->count, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Shipping Class schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Shipping class name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php 0000644 00000056633 15132754524 0023166 0 ustar 00 <?php /** * REST API Taxes controller * * Handles requests to the /taxes endpoint. * * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Taxes controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'taxes'; /** * Register the routes for taxes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create taxes. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access update a tax. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function update_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a tax. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { global $wpdb; $prepared_args = array(); $prepared_args['order'] = $request['order']; $prepared_args['number'] = $request['per_page']; if ( ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } $orderby_possibles = array( 'id' => 'tax_rate_id', 'order' => 'tax_rate_order', 'priority' => 'tax_rate_priority', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['class'] = $request['class']; /** * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. * * @param array $prepared_args Array of arguments for $wpdb->get_results(). * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] ); $query = " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates %s ORDER BY {$orderby} LIMIT %%d, %%d "; $wpdb_prepare_args = array( $prepared_args['offset'], $prepared_args['number'], ); // Filter by tax class. if ( empty( $prepared_args['class'] ) ) { $query = sprintf( $query, '' ); } else { $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; array_unshift( $wpdb_prepare_args, $class ); $query = sprintf( $query, 'WHERE tax_rate_class = %s' ); } // Query taxes. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $wpdb->prepare( $query, $wpdb_prepare_args ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared $taxes = array(); foreach ( $results as $tax ) { $data = $this->prepare_item_for_response( $tax, $request ); $taxes[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $taxes ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); // Query only for ids. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ); $wpdb->get_results( $wpdb->prepare( $query, $wpdb_prepare_args ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared // Calculate totals. $total_taxes = (int) $wpdb->num_rows; $response->header( 'X-WP-Total', (int) $total_taxes ); $max_pages = ceil( $total_taxes / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Take tax data from the request and return the updated or newly created rate. * * @param WP_REST_Request $request Full details about the request. * @param stdClass|null $current Existing tax object. * @return object */ protected function create_or_update_tax( $request, $current = null ) { $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); $data = array(); $fields = array( 'tax_rate_country', 'tax_rate_state', 'tax_rate', 'tax_rate_name', 'tax_rate_priority', 'tax_rate_compound', 'tax_rate_shipping', 'tax_rate_order', 'tax_rate_class', ); foreach ( $fields as $field ) { // Keys via API differ from the stored names returned by _get_tax_rate. $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); // Remove data that was not posted. if ( ! isset( $request[ $key ] ) ) { continue; } // Test new data against current data. if ( $current && $current->$field === $request[ $key ] ) { continue; } // Add to data array. switch ( $key ) { case 'tax_rate_priority': case 'tax_rate_compound': case 'tax_rate_shipping': case 'tax_rate_order': $data[ $field ] = absint( $request[ $key ] ); break; case 'tax_rate_class': $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; break; default: $data[ $field ] = wc_clean( $request[ $key ] ); break; } } if ( ! $id ) { $id = WC_Tax::_insert_tax_rate( $data ); } elseif ( $data ) { WC_Tax::_update_tax_rate( $id, $data ); } // Add locales. if ( ! empty( $request['postcode'] ) ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); } if ( ! empty( $request['city'] ) ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); } return WC_Tax::_get_tax_rate( $id, OBJECT ); } /** * Create a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); } $tax = $this->create_or_update_tax( $request ); $this->update_additional_fields_for_object( $tax, $request ); /** * Fires after a tax is created or updated via the REST API. * * @param stdClass $tax Data used to create the tax. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax, false when updating tax. */ do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); return $response; } /** * Get a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax_obj ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $tax = $this->prepare_item_for_response( $tax_obj, $request ); $response = rest_ensure_response( $tax ); return $response; } /** * Update a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax_obj ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $tax = $this->create_or_update_tax( $request, $tax_obj ); $this->update_additional_fields_for_object( $tax, $request ); /** * Fires after a tax is created or updated via the REST API. * * @param stdClass $tax Data used to create the tax. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax, false when updating tax. */ do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Delete a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { global $wpdb; $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); WC_Tax::_delete_tax_rate( $id ); if ( 0 === $wpdb->rows_affected ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a tax is deleted via the REST API. * * @param stdClass $tax The tax data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); return $response; } /** * Prepare a single tax output for response. * * @param stdClass $tax Tax object. * @param WP_REST_Request $request Request object. * * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $tax, $request ) { $id = (int) $tax->tax_rate_id; $data = array( 'id' => $id, 'country' => $tax->tax_rate_country, 'state' => $tax->tax_rate_state, 'postcode' => '', 'city' => '', 'rate' => $tax->tax_rate, 'name' => $tax->tax_rate_name, 'priority' => (int) $tax->tax_rate_priority, 'compound' => (bool) $tax->tax_rate_compound, 'shipping' => (bool) $tax->tax_rate_shipping, 'order' => (int) $tax->tax_rate_order, 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', ); $data = $this->add_tax_rate_locales( $data, $tax ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $tax ) ); /** * Filter tax object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $tax Tax object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); } /** * Prepare links for the request. * * @param stdClass $tax Tax object. * @return array Links for the given tax. */ protected function prepare_links( $tax ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Add tax rate locales to the response array. * * @param array $data Response data. * @param stdClass $tax Tax object. * * @return array */ protected function add_tax_rate_locales( $data, $tax ) { global $wpdb; // Get locales from a tax rate. $locales = $wpdb->get_results( $wpdb->prepare( " SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d ", $tax->tax_rate_id ) ); if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { foreach ( $locales as $locale ) { $data[ $locale->location_type ] = $locale->location_code; } } return $data; } /** * Get the Taxes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'tax', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'country' => array( 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'State code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'rate' => array( 'description' => __( 'Tax rate.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tax rate name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'priority' => array( 'description' => __( 'Tax priority.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'context' => array( 'view', 'edit' ), ), 'compound' => array( 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'shipping' => array( 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'order' => array( 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'default' => 'standard', 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; $params['page'] = array( 'description' => __( 'Current page of the collection.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'default' => 'order', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( 'id', 'order', 'priority', ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['class'] = array( 'description' => __( 'Sort by tax class.', 'woocommerce' ), 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), 'sanitize_callback' => 'sanitize_title', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php 0000644 00000154403 15132754524 0023332 0 ustar 00 <?php /** * REST API Orders controller * * Handles requests to the /orders endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Posts_Controller */ class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders'; /** * Post type. * * @var string */ protected $post_type = 'shop_order'; /** * Initialize orders actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for orders. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Prepare a single order output for response. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $data */ public function prepare_item_for_response( $post, $request ) { $order = wc_get_order( $post ); $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); $data = array( 'id' => $order->get_id(), 'parent_id' => $order->get_parent_id(), 'status' => $order->get_status(), 'order_key' => $order->get_order_key(), 'number' => $order->get_order_number(), 'currency' => $order->get_currency(), 'version' => $order->get_version(), 'prices_include_tax' => $order->get_prices_include_tax(), 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. 'customer_id' => $order->get_customer_id(), 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), 'total' => wc_format_decimal( $order->get_total(), $dp ), 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), 'billing' => array(), 'shipping' => array(), 'payment_method' => $order->get_payment_method(), 'payment_method_title' => $order->get_payment_method_title(), 'transaction_id' => $order->get_transaction_id(), 'customer_ip_address' => $order->get_customer_ip_address(), 'customer_user_agent' => $order->get_customer_user_agent(), 'created_via' => $order->get_created_via(), 'customer_note' => $order->get_customer_note(), 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. 'cart_hash' => $order->get_cart_hash(), 'line_items' => array(), 'tax_lines' => array(), 'shipping_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), 'refunds' => array(), ); // Add addresses. $data['billing'] = $order->get_address( 'billing' ); $data['shipping'] = $order->get_address( 'shipping' ); // Add line items. foreach ( $order->get_items() as $item_id => $item ) { $product = $item->get_product(); $product_id = 0; $variation_id = 0; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = $item->get_product_id(); $variation_id = $item->get_variation_id(); $product_sku = $product->get_sku(); } $item_meta = array(); $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta->key, 'label' => $formatted_meta->display_key, 'value' => wc_clean( $formatted_meta->display_value ), ); } $line_item = array( 'id' => $item_id, 'name' => $item['name'], 'sku' => $product_sku, 'product_id' => (int) $product_id, 'variation_id' => (int) $variation_id, 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'taxes' => array(), 'meta' => $item_meta, ); $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); if ( isset( $item_line_taxes['total'] ) ) { $line_tax = array(); foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ]['subtotal'] = $tax; } $line_item['taxes'] = array_values( $line_tax ); } $data['line_items'][] = $line_item; } // Add taxes. foreach ( $order->get_items( 'tax' ) as $key => $tax ) { $tax_line = array( 'id' => $key, 'rate_code' => $tax['name'], 'rate_id' => $tax['rate_id'], 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], 'compound' => (bool) $tax['compound'], 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), ); $data['tax_lines'][] = $tax_line; } // Add shipping. foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $shipping_line = array( 'id' => $shipping_item_id, 'method_title' => $shipping_item['name'], 'method_id' => $shipping_item['method_id'], 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), 'total_tax' => wc_format_decimal( '', $dp ), 'taxes' => array(), ); $shipping_taxes = $shipping_item->get_taxes(); if ( ! empty( $shipping_taxes['total'] ) ) { $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { $shipping_line['taxes'][] = array( 'id' => $tax_rate_id, 'total' => $tax, ); } } $data['shipping_lines'][] = $shipping_line; } // Add fees. foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $fee_line = array( 'id' => $fee_item_id, 'name' => $fee_item['name'], 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', 'tax_status' => 'taxable', 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), 'taxes' => array(), ); $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); if ( isset( $fee_line_taxes['total'] ) ) { $fee_tax = array(); foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { $fee_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } if ( isset( $fee_line_taxes['subtotal'] ) ) { foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; } } $fee_line['taxes'] = array_values( $fee_tax ); } $data['fee_lines'][] = $fee_line; } // Add coupons. foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $coupon_line = array( 'id' => $coupon_item_id, 'code' => $coupon_item['name'], 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), ); $data['coupon_lines'][] = $coupon_line; } // Add refunds. foreach ( $order->get_refunds() as $refund ) { $data['refunds'][] = array( 'id' => $refund->get_id(), 'refund' => $refund->get_reason() ? $refund->get_reason() : '', 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $order, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Prepare links for the request. * * @param WC_Order $order Order object. * @param WP_REST_Request $request Request object. * @return array Links for the given order. */ protected function prepare_links( $order, $request ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( 0 !== (int) $order->get_user_id() ) { $links['customer'] = array( 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), ); } if ( 0 !== (int) $order->get_parent_id() ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), ); } return $links; } /** * Query args. * * @param array $args * @param WP_REST_Request $request * @return array */ public function query_args( $args, $request ) { global $wpdb; // Set post_status. if ( 'any' !== $request['status'] ) { $args['post_status'] = 'wc-' . $request['status']; } else { $args['post_status'] = 'any'; } if ( isset( $request['customer'] ) ) { if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); } $args['meta_query'][] = array( 'key' => '_customer_user', 'value' => $request['customer'], 'type' => 'NUMERIC', ); } // Search by product. if ( ! empty( $request['product'] ) ) { $order_ids = $wpdb->get_col( $wpdb->prepare( " SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) AND order_item_type = 'line_item' ", $request['product'] ) ); // Force WP_Query return empty if don't found any order. $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); $args['post__in'] = $order_ids; } // Search. if ( ! empty( $args['s'] ) ) { $order_ids = wc_order_search( $args['s'] ); if ( ! empty( $order_ids ) ) { unset( $args['s'] ); $args['post__in'] = array_merge( $order_ids, array( 0 ) ); } } return $args; } /** * Prepare a single order for create. * * @param WP_REST_Request $request Request object. * @return WP_Error|WC_Order $data Object. */ protected function prepare_item_for_database( $request ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $order = new WC_Order( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Handle all writable props foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'billing' : case 'shipping' : $this->update_address( $order, $value, $key ); break; case 'line_items' : case 'shipping_lines' : case 'fee_lines' : case 'coupon_lines' : if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( is_array( $item ) ) { if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { $order->remove_item( $item['id'] ); } else { $this->set_item( $order, $key, $item ); } } } } break; default : if ( is_callable( array( $order, "set_{$key}" ) ) ) { $order->{"set_{$key}"}( $value ); } break; } } } /** * Filter the data for the insert. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WC_Order $order The order object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); } /** * Create base WC Order object. * @deprecated 3.0.0 * @param array $data * @return WC_Order */ protected function create_base_order( $data ) { return wc_create_order( $data ); } /** * Only return writable props from schema. * @param array $schema * @return bool */ protected function filter_writable_props( $schema ) { return empty( $schema['readonly'] ); } /** * Create order. * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function create_order( $request ) { try { // Make sure customer exists. if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } // Make sure customer is part of blog. if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); } $order = $this->prepare_item_for_database( $request ); $order->set_created_via( 'rest-api' ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->calculate_totals(); $order->save(); // Handle set paid. if ( true === $request['set_paid'] ) { $order->payment_complete( $request['transaction_id'] ); } return $order->get_id(); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update order. * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function update_order( $request ) { try { $order = $this->prepare_item_for_database( $request ); $order->save(); // Handle set paid. if ( $order->needs_payment() && true === $request['set_paid'] ) { $order->payment_complete( $request['transaction_id'] ); } // If items have changed, recalculate order totals. if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { $order->calculate_totals( true ); } return $order->get_id(); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update address. * * @param WC_Order $order * @param array $posted * @param string $type */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { $order->{"set_{$type}_{$key}"}( $value ); } } } /** * Gets the product ID from the SKU or posted ID. * * @throws WC_REST_Exception When SKU or ID is not valid. * @param array $posted Request data. * @param string $action 'create' to add line item or 'update' to update it. * @return int */ protected function get_product_id( $posted, $action = 'create' ) { if ( ! empty( $posted['sku'] ) ) { $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { $product_id = (int) $posted['product_id']; } elseif ( ! empty( $posted['variation_id'] ) ) { $product_id = (int) $posted['variation_id']; } elseif ( 'update' === $action ) { $product_id = 0; } else { throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); } return $product_id; } /** * Maybe set an item prop if the value was posted. * @param WC_Order_Item $item * @param string $prop * @param array $posted Request data. */ protected function maybe_set_item_prop( $item, $prop, $posted ) { if ( isset( $posted[ $prop ] ) ) { $item->{"set_$prop"}( $posted[ $prop ] ); } } /** * Maybe set item props if the values were posted. * @param WC_Order_Item $item * @param string[] $props * @param array $posted Request data. */ protected function maybe_set_item_props( $item, $props, $posted ) { foreach ( $props as $prop ) { $this->maybe_set_item_prop( $item, $prop, $posted ); } } /** * Create or update a line item. * * @param array $posted Line item data. * @param string $action 'create' to add line item or 'update' to update it. * * @return WC_Order_Item_Product * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_line_items( $posted, $action = 'create' ) { $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); $product = wc_get_product( $this->get_product_id( $posted, $action ) ); if ( $product && $product !== $item->get_product() ) { $item->set_product( $product ); if ( 'create' === $action ) { $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); $item->set_total( $total ); $item->set_subtotal( $total ); } } $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); return $item; } /** * Create or update an order shipping method. * * @param $posted $shipping Item data. * @param string $action 'create' to add shipping or 'update' to update it. * * @return WC_Order_Item_Shipping * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_shipping_lines( $posted, $action ) { $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); if ( 'create' === $action ) { if ( empty( $posted['method_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); return $item; } /** * Create or update an order fee. * * @param array $posted Item data. * @param string $action 'create' to add fee or 'update' to update it. * * @return WC_Order_Item_Fee * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_fee_lines( $posted, $action ) { $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); if ( 'create' === $action ) { if ( empty( $posted['name'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); return $item; } /** * Create or update an order coupon. * * @param array $posted Item data. * @param string $action 'create' to add coupon or 'update' to update it. * * @return WC_Order_Item_Coupon * @throws WC_REST_Exception Invalid data, server error. */ protected function prepare_coupon_lines( $posted, $action ) { $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); if ( 'create' === $action ) { if ( empty( $posted['code'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } } $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); return $item; } /** * Wrapper method to create/update order items. * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @param WC_Order $order order * @param string $item_type * @param array $posted item provided in the request body * @throws WC_REST_Exception If item ID is not associated with order */ protected function set_item( $order, $item_type, $posted ) { global $wpdb; if ( ! empty( $posted['id'] ) ) { $action = 'update'; } else { $action = 'create'; } $method = 'prepare_' . $item_type; // Verify provided line item ID is associated with order. if ( 'update' === $action ) { $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", absint( $posted['id'] ), absint( $order->get_id() ) ) ); if ( is_null( $result ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); } } // Prepare item data $item = $this->$method( $posted, $action ); /** * Action hook to adjust item before save. * @since 3.0.0 */ do_action( 'woocommerce_rest_set_order_item', $item, $posted ); // Save or add to order if ( 'create' === $action ) { $order->add_item( $item ); } else { $item->save(); } } /** * Helper method to check if the resource ID associated with the provided item is null. * Items can be deleted by setting the resource ID to null. * * @param array $item Item provided in the request body. * @return bool True if the item resource ID is null, false otherwise. */ protected function item_is_null( $item ) { $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); foreach ( $keys as $key ) { if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { return true; } } return false; } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { /* translators: %s: post type */ return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order_id = $this->create_order( $request ); if ( is_wp_error( $order_id ) ) { return $order_id; } $post = get_post( $order_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } /** * Update a single order. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { try { $post_id = (int) $request['id']; if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $order_id = $this->update_order( $request ); if ( is_wp_error( $order_id ) ) { return $order_id; } $post = get_post( $order_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get order statuses without prefixes. * @return array */ protected function get_order_statuses() { $order_statuses = array(); foreach ( array_keys( wc_get_order_statuses() ) as $status ) { $order_statuses[] = str_replace( 'wc-', '', $status ); } return $order_statuses; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'parent_id' => array( 'description' => __( 'Parent order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Order status.', 'woocommerce' ), 'type' => 'string', 'default' => 'pending', 'enum' => $this->get_order_statuses(), 'context' => array( 'view', 'edit' ), ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'number' => array( 'description' => __( 'Order number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'currency' => array( 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), 'type' => 'string', 'default' => get_woocommerce_currency(), 'enum' => array_keys( get_woocommerce_currencies() ), 'context' => array( 'view', 'edit' ), ), 'version' => array( 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'prices_include_tax' => array( 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_id' => array( 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), 'type' => 'integer', 'default' => 0, 'context' => array( 'view', 'edit' ), ), 'discount_total' => array( 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_tax' => array( 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_total' => array( 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax' => array( 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_tax' => array( 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Grand total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Sum of all taxes.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing' => array( 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'payment_method' => array( 'description' => __( 'Payment method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'payment_method_title' => array( 'description' => __( 'Payment method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'set_paid' => array( 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'edit' ), ), 'transaction_id' => array( 'description' => __( 'Unique transaction ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_ip_address' => array( 'description' => __( "Customer's IP address.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_user_agent' => array( 'description' => __( 'User agent of the customer.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'created_via' => array( 'description' => __( 'Shows where the order was created.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_note' => array( 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_completed' => array( 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_paid' => array( 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_hash' => array( 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta' => array( 'description' => __( 'Line item meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Meta label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'tax_lines' => array( 'description' => __( 'Tax lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_code' => array( 'description' => __( 'Tax rate code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Tax rate label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'compound' => array( 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tax_total' => array( 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax_total' => array( 'description' => __( 'Shipping tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'shipping_lines' => array( 'description' => __( 'Shipping lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_title' => array( 'description' => __( 'Shipping method name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'method_id' => array( 'description' => __( 'Shipping method ID.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'fee_lines' => array( 'description' => __( 'Fee lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Fee name.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'enum' => array( 'taxable', 'none' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'coupon_lines' => array( 'description' => __( 'Coupons line data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'mixed', 'context' => array( 'view', 'edit' ), ), 'discount' => array( 'description' => __( 'Discount total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount_tax' => array( 'description' => __( 'Discount total tax.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'refunds' => array( 'description' => __( 'List of refunds.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Refund ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reason' => array( 'description' => __( 'Refund reason.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Refund total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['customer'] = array( 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['product'] = array( 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['dp'] = array( 'default' => wc_get_price_decimals(), 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php 0000644 00000011153 15132754524 0025610 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/top_sellers endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Report Top Sellers controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Report_Sales_V1_Controller */ class WC_REST_Report_Top_Sellers_V1_Controller extends WC_REST_Report_Sales_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports/top_sellers'; /** * Get sales reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { // Set date filtering. $filter = array( 'period' => $request['period'], 'date_min' => $request['date_min'], 'date_max' => $request['date_max'], ); $this->setup_report( $filter ); $report_data = $this->report->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); $top_sellers = array(); foreach ( $report_data as $item ) { $product = wc_get_product( $item->product_id ); if ( $product ) { $top_sellers[] = array( 'name' => $product->get_name(), 'product_id' => (int) $item->product_id, 'quantity' => wc_stock_amount( $item->order_item_qty ), ); } } $data = array(); foreach ( $top_sellers as $top_seller ) { $item = $this->prepare_item_for_response( (object) $top_seller, $request ); $data[] = $this->prepare_response_for_collection( $item ); } return rest_ensure_response( $data ); } /** * Prepare a report sales object for serialization. * * @param stdClass $top_seller * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $top_seller, $request ) { $data = array( 'name' => $top_seller->name, 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->quantity, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( array( 'about' => array( 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), ), 'product' => array( 'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ), ), ) ); /** * Filter a report top sellers returned from the API. * * Allows modification of the report top sellers data right before it is returned. * * @param WP_REST_Response $response The response object. * @param stdClass $top_seller The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'top_sellers_report', 'type' => 'object', 'properties' => array( 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'quantity' => array( 'description' => __( 'Total number of purchases.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php 0000644 00000011251 15132754524 0023523 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Reports controller class. * * @package WooCommerce\RestApi * @extends WC_REST_Controller */ class WC_REST_Reports_V1_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports'; /** * Register the routes for reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read reports. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get reports list. * * @since 3.5.0 * @return array */ protected function get_reports() { return array( array( 'slug' => 'sales', 'description' => __( 'List of sales reports.', 'woocommerce' ), ), array( 'slug' => 'top_sellers', 'description' => __( 'List of top sellers products.', 'woocommerce' ), ), ); } /** * Get all reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { $data = array(); $reports = $this->get_reports(); foreach ( $reports as $report ) { $item = $this->prepare_item_for_response( (object) $report, $request ); $data[] = $this->prepare_response_for_collection( $item ); } return rest_ensure_response( $data ); } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'description' => $report->description, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ) ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } includes/rest-api/Server.php 0000644 00000021126 15132754524 0012067 0 ustar 00 <?php /** * Initialize this version of the REST API. * * @package WooCommerce\RestApi */ namespace Automattic\WooCommerce\RestApi; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\RestApi\Utilities\SingletonTrait; /** * Class responsible for loading the REST API and all REST API namespaces. */ class Server { use SingletonTrait; /** * REST API namespaces and endpoints. * * @var array */ protected $controllers = array(); /** * Hook into WordPress ready to init the REST API as needed. */ public function init() { add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 ); } /** * Register REST API routes. */ public function register_rest_routes() { foreach ( $this->get_rest_namespaces() as $namespace => $controllers ) { foreach ( $controllers as $controller_name => $controller_class ) { $this->controllers[ $namespace ][ $controller_name ] = new $controller_class(); $this->controllers[ $namespace ][ $controller_name ]->register_routes(); } } } /** * Get API namespaces - new namespaces should be registered here. * * @return array List of Namespaces and Main controller classes. */ protected function get_rest_namespaces() { return apply_filters( 'woocommerce_rest_api_get_rest_namespaces', array( 'wc/v1' => $this->get_v1_controllers(), 'wc/v2' => $this->get_v2_controllers(), 'wc/v3' => $this->get_v3_controllers(), 'wc-telemetry' => $this->get_telemetry_controllers(), ) ); } /** * List of controllers in the wc/v1 namespace. * * @return array */ protected function get_v1_controllers() { return array( 'coupons' => 'WC_REST_Coupons_V1_Controller', 'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller', 'customers' => 'WC_REST_Customers_V1_Controller', 'order-notes' => 'WC_REST_Order_Notes_V1_Controller', 'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller', 'orders' => 'WC_REST_Orders_V1_Controller', 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller', 'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller', 'product-categories' => 'WC_REST_Product_Categories_V1_Controller', 'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller', 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller', 'product-tags' => 'WC_REST_Product_Tags_V1_Controller', 'products' => 'WC_REST_Products_V1_Controller', 'reports-sales' => 'WC_REST_Report_Sales_V1_Controller', 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller', 'reports' => 'WC_REST_Reports_V1_Controller', 'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller', 'taxes' => 'WC_REST_Taxes_V1_Controller', 'webhooks' => 'WC_REST_Webhooks_V1_Controller', 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller', ); } /** * List of controllers in the wc/v2 namespace. * * @return array */ protected function get_v2_controllers() { return array( 'coupons' => 'WC_REST_Coupons_V2_Controller', 'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller', 'customers' => 'WC_REST_Customers_V2_Controller', 'network-orders' => 'WC_REST_Network_Orders_V2_Controller', 'order-notes' => 'WC_REST_Order_Notes_V2_Controller', 'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller', 'orders' => 'WC_REST_Orders_V2_Controller', 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller', 'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller', 'product-categories' => 'WC_REST_Product_Categories_V2_Controller', 'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller', 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller', 'product-tags' => 'WC_REST_Product_Tags_V2_Controller', 'products' => 'WC_REST_Products_V2_Controller', 'product-variations' => 'WC_REST_Product_Variations_V2_Controller', 'reports-sales' => 'WC_REST_Report_Sales_V2_Controller', 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller', 'reports' => 'WC_REST_Reports_V2_Controller', 'settings' => 'WC_REST_Settings_V2_Controller', 'settings-options' => 'WC_REST_Setting_Options_V2_Controller', 'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller', 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller', 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller', 'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller', 'taxes' => 'WC_REST_Taxes_V2_Controller', 'webhooks' => 'WC_REST_Webhooks_V2_Controller', 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller', 'system-status' => 'WC_REST_System_Status_V2_Controller', 'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller', 'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller', 'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller', ); } /** * List of controllers in the wc/v3 namespace. * * @return array */ protected function get_v3_controllers() { return array( 'coupons' => 'WC_REST_Coupons_Controller', 'customer-downloads' => 'WC_REST_Customer_Downloads_Controller', 'customers' => 'WC_REST_Customers_Controller', 'network-orders' => 'WC_REST_Network_Orders_Controller', 'order-notes' => 'WC_REST_Order_Notes_Controller', 'order-refunds' => 'WC_REST_Order_Refunds_Controller', 'orders' => 'WC_REST_Orders_Controller', 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller', 'product-attributes' => 'WC_REST_Product_Attributes_Controller', 'product-categories' => 'WC_REST_Product_Categories_Controller', 'product-reviews' => 'WC_REST_Product_Reviews_Controller', 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller', 'product-tags' => 'WC_REST_Product_Tags_Controller', 'products' => 'WC_REST_Products_Controller', 'product-variations' => 'WC_REST_Product_Variations_Controller', 'reports-sales' => 'WC_REST_Report_Sales_Controller', 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller', 'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller', 'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller', 'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller', 'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller', 'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller', 'reports' => 'WC_REST_Reports_Controller', 'settings' => 'WC_REST_Settings_Controller', 'settings-options' => 'WC_REST_Setting_Options_Controller', 'shipping-zones' => 'WC_REST_Shipping_Zones_Controller', 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller', 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller', 'tax-classes' => 'WC_REST_Tax_Classes_Controller', 'taxes' => 'WC_REST_Taxes_Controller', 'webhooks' => 'WC_REST_Webhooks_Controller', 'system-status' => 'WC_REST_System_Status_Controller', 'system-status-tools' => 'WC_REST_System_Status_Tools_Controller', 'shipping-methods' => 'WC_REST_Shipping_Methods_Controller', 'payment-gateways' => 'WC_REST_Payment_Gateways_Controller', 'data' => 'WC_REST_Data_Controller', 'data-continents' => 'WC_REST_Data_Continents_Controller', 'data-countries' => 'WC_REST_Data_Countries_Controller', 'data-currencies' => 'WC_REST_Data_Currencies_Controller', ); } /** * List of controllers in the telemetry namespace. * * @return array */ protected function get_telemetry_controllers() { return array( 'tracker' => 'WC_REST_Telemetry_Controller', ); } /** * Return the path to the package. * * @return string */ public static function get_path() { return dirname( __DIR__ ); } } includes/rest-api/Utilities/SingletonTrait.php 0000644 00000001537 15132754524 0015546 0 ustar 00 <?php /** * Singleton class trait. * * @package WooCommerce\Utilities */ namespace Automattic\WooCommerce\RestApi\Utilities; /** * Singleton trait. */ trait SingletonTrait { /** * The single instance of the class. * * @var object */ protected static $instance = null; /** * Constructor * * @return void */ protected function __construct() {} /** * Get class instance. * * @return object Instance. */ final public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Prevent cloning. */ private function __clone() {} /** * Prevent unserializing. */ final public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '4.6' ); die(); } } includes/rest-api/Utilities/ImageAttachment.php 0000644 00000003705 15132754524 0015632 0 ustar 00 <?php /** * Helper to upload files via the REST API. * * @package WooCommerce\Utilities */ namespace Automattic\WooCommerce\RestApi\Utilities; /** * ImageAttachment class. */ class ImageAttachment { /** * Attachment ID. * * @var integer */ public $id = 0; /** * Object attached to. * * @var integer */ public $object_id = 0; /** * Constructor. * * @param integer $id Attachment ID. * @param integer $object_id Object ID. */ public function __construct( $id = 0, $object_id = 0 ) { $this->id = (int) $id; $this->object_id = (int) $object_id; } /** * Upload an attachment file. * * @throws \WC_REST_Exception REST API exceptions. * @param string $src URL to file. */ public function upload_image_from_src( $src ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $src ) ); if ( is_wp_error( $upload ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $this->object_id, $images ) ) { throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } else { return; } } $this->id = wc_rest_set_uploaded_image_as_attachment( $upload, $this->object_id ); if ( ! wp_attachment_is_image( $this->id ) ) { /* translators: %s: image ID */ throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $this->id ), 400 ); } } /** * Update attachment alt text. * * @param string $text Text to set. */ public function update_alt_text( $text ) { if ( ! $this->id ) { return; } update_post_meta( $this->id, '_wp_attachment_image_alt', wc_clean( $text ) ); } /** * Update attachment name. * * @param string $text Text to set. */ public function update_name( $text ) { if ( ! $this->id ) { return; } wp_update_post( array( 'ID' => $this->id, 'post_title' => $text, ) ); } } includes/rest-api/Package.php 0000644 00000003027 15132754524 0012154 0 ustar 00 <?php /** * Deprecated notice: This class is deprecated as of version 4.5.0. WooCommerce API is now part of core and not packaged seperately. * * Returns information about the package and handles init. * * @package WooCommerce\RestApi */ namespace Automattic\WooCommerce\RestApi; defined( 'ABSPATH' ) || exit; /** * Main package class. * * @deprecated Use \Automattic\WooCommerce\RestApi\Server directly. */ class Package { /** * Version. * * @deprecated since 4.5.0. This tracks WooCommerce version now. * @var string */ const VERSION = WC_VERSION; /** * Init the package - load the REST API Server class. * * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::instance()->init() */ public static function init() { wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' ); \Automattic\WooCommerce\RestApi\Server::instance()->init(); } /** * Return the version of the package. * * @deprecated since 4.5.0. This tracks WooCommerce version now. * @return string */ public static function get_version() { wc_deprecated_function( 'WC()->version', '4.5.0' ); return WC()->version; } /** * Return the path to the package. * * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path() * @return string */ public static function get_path() { wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' ); return \Automattic\WooCommerce\RestApi\Server::get_path(); } } includes/wc-term-functions.php 0000644 00000051163 15132754524 0012465 0 ustar 00 <?php /** * WooCommerce Terms * * Functions for handling terms/term meta. * * @package WooCommerce\Functions * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; /** * Change get terms defaults for attributes to order by the sorting setting, or default to menu_order for sortable taxonomies. * * @since 3.6.0 Sorting options are now set as the default automatically, so you no longer have to request to orderby menu_order. * * @param array $defaults An array of default get_terms() arguments. * @param array $taxonomies An array of taxonomies. * @return array */ function wc_change_get_terms_defaults( $defaults, $taxonomies ) { if ( is_array( $taxonomies ) && 1 < count( $taxonomies ) ) { return $defaults; } $taxonomy = is_array( $taxonomies ) ? (string) current( $taxonomies ) : $taxonomies; $orderby = 'name'; if ( taxonomy_is_product_attribute( $taxonomy ) ) { $orderby = wc_attribute_orderby( $taxonomy ); } elseif ( in_array( $taxonomy, apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ), true ) ) { $orderby = 'menu_order'; } // Change defaults. Invalid values will be changed later @see wc_change_pre_get_terms. // These are in place so we know if a specific order was requested. switch ( $orderby ) { case 'menu_order': case 'name_num': case 'parent': $defaults['orderby'] = $orderby; break; } return $defaults; } add_filter( 'get_terms_defaults', 'wc_change_get_terms_defaults', 10, 2 ); /** * Adds support to get_terms for menu_order argument. * * @since 3.6.0 * @param WP_Term_Query $terms_query Instance of WP_Term_Query. */ function wc_change_pre_get_terms( $terms_query ) { $args = &$terms_query->query_vars; // Put back valid orderby values. if ( 'menu_order' === $args['orderby'] ) { $args['orderby'] = 'name'; $args['force_menu_order_sort'] = true; } if ( 'name_num' === $args['orderby'] ) { $args['orderby'] = 'name'; $args['force_numeric_name'] = true; } // When COUNTING, disable custom sorting. if ( 'count' === $args['fields'] ) { return; } // Support menu_order arg used in previous versions. if ( ! empty( $args['menu_order'] ) ) { $args['order'] = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC'; $args['force_menu_order_sort'] = true; } if ( ! empty( $args['force_menu_order_sort'] ) ) { $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'order'; // phpcs:ignore $terms_query->meta_query->parse_query_vars( $args ); } } add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 ); /** * Adjust term query to handle custom sorting parameters. * * @param array $clauses Clauses. * @param array $taxonomies Taxonomies. * @param array $args Arguments. * @return array */ function wc_terms_clauses( $clauses, $taxonomies, $args ) { global $wpdb; // No need to filter when counting. if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) { return $clauses; } // Force numeric sort if using name_num custom sorting param. if ( ! empty( $args['force_numeric_name'] ) ) { $clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] ); } // For sorting, force left join in case order meta is missing. if ( ! empty( $args['force_menu_order_sort'] ) ) { $clauses['join'] = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] ); $clauses['where'] = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] ); $clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] ); } return $clauses; } add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 ); /** * Helper to get cached object terms and filter by field using wp_list_pluck(). * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms(). * * @since 3.0.0 * @param int $object_id Object ID. * @param string $taxonomy Taxonomy slug. * @param string $field Field name. * @param string $index_key Index key name. * @return array */ function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) { // Test if terms exists. get_the_terms() return false when it finds no terms. $terms = get_the_terms( $object_id, $taxonomy ); if ( ! $terms || is_wp_error( $terms ) ) { return array(); } return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key ); } /** * Cached version of wp_get_post_terms(). * This is a private function (internal use ONLY). * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @param array $args Query arguments. * @return array */ function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) { $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) ); $cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id; $terms = wp_cache_get( $cache_key, $cache_group ); if ( false !== $terms ) { return $terms; } $terms = wp_get_post_terms( $product_id, $taxonomy, $args ); wp_cache_add( $cache_key, $terms, $cache_group ); return $terms; } /** * Wrapper used to get terms for a product. * * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @param array $args Query arguments. * @return array */ function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) { if ( ! taxonomy_exists( $taxonomy ) ) { return array(); } return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args ); } /** * Sort by name (numeric). * * @param WP_Post $a First item to compare. * @param WP_Post $b Second item to compare. * @return int */ function _wc_get_product_terms_name_num_usort_callback( $a, $b ) { $a_name = (float) $a->name; $b_name = (float) $b->name; if ( abs( $a_name - $b_name ) < 0.001 ) { return 0; } return ( $a_name < $b_name ) ? -1 : 1; } /** * Sort by parent. * * @param WP_Post $a First item to compare. * @param WP_Post $b Second item to compare. * @return int */ function _wc_get_product_terms_parent_usort_callback( $a, $b ) { if ( $a->parent === $b->parent ) { return 0; } return ( $a->parent < $b->parent ) ? 1 : -1; } /** * WooCommerce Dropdown categories. * * @param array $args Args to control display of dropdown. */ function wc_product_dropdown_categories( $args = array() ) { global $wp_query; $args = wp_parse_args( $args, array( 'pad_counts' => 1, 'show_count' => 1, 'hierarchical' => 1, 'hide_empty' => 1, 'show_uncategorized' => 1, 'orderby' => 'name', 'selected' => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '', 'show_option_none' => __( 'Select a category', 'woocommerce' ), 'option_none_value' => '', 'value_field' => 'slug', 'taxonomy' => 'product_cat', 'name' => 'product_cat', 'class' => 'dropdown_product_cat', ) ); if ( 'order' === $args['orderby'] ) { $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'order'; // phpcs:ignore } wp_dropdown_categories( $args ); } /** * Custom walker for Product Categories. * * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core. * * @param mixed ...$args Variable number of parameters to be passed to the walker. * @return mixed */ function wc_walk_category_dropdown_tree( ...$args ) { if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; } // The user's options are the third parameter. if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) { $walker = new WC_Product_Cat_Dropdown_Walker(); } else { $walker = $args[2]['walker']; } return $walker->walk( ...$args ); } /** * Migrate data from WC term meta to WP term meta. * * When the database is updated to support term meta, migrate WC term meta data across. * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added). * * @param string $wp_db_version The new $wp_db_version. * @param string $wp_current_db_version The old (current) $wp_db_version. */ function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) { if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) { global $wpdb; if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); } } } add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 ); /** * Move a term before the a given element of its hierarchy level. * * @param int $the_term Term ID. * @param int $next_id The id of the next sibling element in save hierarchy level. * @param string $taxonomy Taxnomy. * @param int $index Term index (default: 0). * @param mixed $terms List of terms. (default: null). * @return int */ function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { if ( ! $terms ) { $terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' ); } if ( empty( $terms ) ) { return $index; } $id = intval( $the_term->term_id ); $term_in_level = false; // Flag: is our term to order in this level of terms. foreach ( $terms as $term ) { $term_id = intval( $term->term_id ); if ( $term_id === $id ) { // Our term to order, we skip. $term_in_level = true; continue; // Our term to order, we skip. } // the nextid of our term to order, lets move our term here. if ( null !== $next_id && $term_id === $next_id ) { $index++; $index = wc_set_term_order( $id, $index, $taxonomy, true ); } // Set order. $index++; $index = wc_set_term_order( $term_id, $index, $taxonomy ); /** * After a term has had it's order set. */ do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy ); // If that term has children we walk through them. $children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" ); if ( ! empty( $children ) ) { $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children ); } } // No nextid meaning our term is in last position. if ( $term_in_level && null === $next_id ) { $index = wc_set_term_order( $id, $index + 1, $taxonomy, true ); } return $index; } /** * Set the sort order of a term. * * @param int $term_id Term ID. * @param int $index Index. * @param string $taxonomy Taxonomy. * @param bool $recursive Recursive (default: false). * @return int */ function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { $term_id = (int) $term_id; $index = (int) $index; update_term_meta( $term_id, 'order', $index ); if ( ! $recursive ) { return $index; } $children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" ); foreach ( $children as $term ) { $index++; $index = wc_set_term_order( $term->term_id, $index, $taxonomy, true ); } clean_term_cache( $term_id, $taxonomy ); return $index; } /** * Function for recounting product terms, ignoring hidden products. * * @param array $terms List of terms. * @param object $taxonomy Taxonomy. * @param bool $callback Callback. * @param bool $terms_are_term_taxonomy_ids If terms are from term_taxonomy_id column. */ function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) { global $wpdb; /** * Filter to allow/prevent recounting of terms as it could be expensive. * A likely scenario for this is when bulk importing products. We could * then prevent it from recounting per product but instead recount it once * when import is done. Of course this means the import logic has to support this. * * @since 5.2 * @param bool */ if ( ! apply_filters( 'woocommerce_product_recount_terms', '__return_true' ) ) { return; } // Standard callback. if ( $callback ) { _update_post_term_count( $terms, $taxonomy ); } $exclude_term_ids = array(); $product_visibility_term_ids = wc_get_product_visibility_term_ids(); if ( $product_visibility_term_ids['exclude-from-catalog'] ) { $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; } $query = array( 'fields' => " SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p ", 'join' => '', 'where' => " WHERE 1=1 AND p.post_status = 'publish' AND p.post_type = 'product' ", ); if ( count( $exclude_term_ids ) ) { $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; $query['where'] .= ' AND exclude_join.object_id IS NULL'; } // Pre-process term taxonomy ids. if ( ! $terms_are_term_taxonomy_ids ) { // We passed in an array of TERMS in format id=>parent. $terms = array_filter( (array) array_keys( $terms ) ); } else { // If we have term taxonomy IDs we need to get the term ID. $term_taxonomy_ids = $terms; $terms = array(); foreach ( $term_taxonomy_ids as $term_taxonomy_id ) { $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name ); $terms[] = $term->term_id; } } // Exit if we have no terms to count. if ( empty( $terms ) ) { return; } // Ancestors need counting. if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { foreach ( $terms as $term_id ) { $terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) ); } } // Unique terms only. $terms = array_unique( $terms ); // Count the terms. foreach ( $terms as $term_id ) { $terms_to_count = array( absint( $term_id ) ); if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { // We need to get the $term's hierarchy so we can count its children too. $children = get_term_children( $term_id, $taxonomy->name ); if ( $children && ! is_wp_error( $children ) ) { $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) ); } } // Generate term query. $term_query = $query; $term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; // Get the count. // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $count = $wpdb->get_var( implode( ' ', $term_query ) ); // Update the count. update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) ); } delete_transient( 'wc_term_counts' ); } /** * Recount terms after the stock amount changes. * * @param int $product_id Product ID. */ function wc_recount_after_stock_change( $product_id ) { if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) { return; } _wc_recount_terms_by_product( $product_id ); } add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' ); /** * Overrides the original term count for product categories and tags with the product count. * that takes catalog visibility into account. * * @param array $terms List of terms. * @param string|array $taxonomies Single taxonomy or list of taxonomies. * @return array */ function wc_change_term_counts( $terms, $taxonomies ) { if ( is_admin() || is_ajax() ) { return $terms; } if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ), true ) ) { return $terms; } $o_term_counts = get_transient( 'wc_term_counts' ); $term_counts = $o_term_counts; foreach ( $terms as &$term ) { if ( is_object( $term ) ) { $term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_term_meta( $term->term_id, 'product_count_' . $taxonomies[0], true ); if ( '' !== $term_counts[ $term->term_id ] ) { $term->count = absint( $term_counts[ $term->term_id ] ); } } } // Update transient. if ( $term_counts !== $o_term_counts ) { set_transient( 'wc_term_counts', $term_counts, DAY_IN_SECONDS * 30 ); } return $terms; } add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 ); /** * Return products in a given term, and cache value. * * To keep in sync, product_count will be cleared on "set_object_terms". * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy. * @return array */ function wc_get_term_product_ids( $term_id, $taxonomy ) { $product_ids = get_term_meta( $term_id, 'product_ids', true ); if ( false === $product_ids || ! is_array( $product_ids ) ) { $product_ids = get_objects_in_term( $term_id, $taxonomy ); update_term_meta( $term_id, 'product_ids', $product_ids ); } return $product_ids; } /** * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids. * * @param int $object_id Object ID. * @param array $terms An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { foreach ( $old_tt_ids as $term_id ) { delete_term_meta( $term_id, 'product_ids' ); } foreach ( $tt_ids as $term_id ) { delete_term_meta( $term_id, 'product_ids' ); } } add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 ); /** * Get full list of product visibilty term ids. * * @since 3.0.0 * @return int[] */ function wc_get_product_visibility_term_ids() { if ( ! taxonomy_exists( 'product_visibility' ) ) { wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' ); return array(); } return array_map( 'absint', wp_parse_args( wp_list_pluck( get_terms( array( 'taxonomy' => 'product_visibility', 'hide_empty' => false, ) ), 'term_taxonomy_id', 'name' ), array( 'exclude-from-catalog' => 0, 'exclude-from-search' => 0, 'featured' => 0, 'outofstock' => 0, 'rated-1' => 0, 'rated-2' => 0, 'rated-3' => 0, 'rated-4' => 0, 'rated-5' => 0, ) ) ); } /** * Recounts all terms. * * @since 5.2 * @return void */ function wc_recount_all_terms() { $product_cats = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'id=>parent', ) ); _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); $product_tags = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'id=>parent', ) ); _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); } /** * Recounts terms by product. * * @since 5.2 * @param int $product_id The ID of the product. * @return void */ function _wc_recount_terms_by_product( $product_id = '' ) { if ( empty( $product_id ) ) { return; } $product_terms = get_the_terms( $product_id, 'product_cat' ); if ( $product_terms ) { $product_cats = array(); foreach ( $product_terms as $term ) { $product_cats[ $term->term_id ] = $term->parent; } _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false ); } $product_terms = get_the_terms( $product_id, 'product_tag' ); if ( $product_terms ) { $product_tags = array(); foreach ( $product_terms as $term ) { $product_tags[ $term->term_id ] = $term->parent; } _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false ); } } includes/class-wc-privacy-exporters.php 0000644 00000035304 15132754524 0014320 0 ustar 00 <?php /** * Personal data exporters. * * @since 3.4.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Privacy_Exporters Class. */ class WC_Privacy_Exporters { /** * Finds and exports customer data by email address. * * @since 3.4.0 * @param string $email_address The user email address. * @return array An array of personal data in name value pairs */ public static function customer_data_exporter( $email_address ) { $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $data_to_export = array(); if ( $user instanceof WP_User ) { $customer_personal_data = self::get_customer_personal_data( $user ); if ( ! empty( $customer_personal_data ) ) { $data_to_export[] = array( 'group_id' => 'woocommerce_customer', 'group_label' => __( 'Customer Data', 'woocommerce' ), 'group_description' => __( 'User’s WooCommerce customer data.', 'woocommerce' ), 'item_id' => 'user', 'data' => $customer_personal_data, ); } } return array( 'data' => $data_to_export, 'done' => true, ); } /** * Finds and exports data which could be used to identify a person from WooCommerce data associated with an email address. * * Orders are exported in blocks of 10 to avoid timeouts. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function order_data_exporter( $email_address, $page ) { $done = true; $page = (int) $page; $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $data_to_export = array(); $order_query = array( 'limit' => 10, 'page' => $page, 'customer' => array( $email_address ), ); if ( $user instanceof WP_User ) { $order_query['customer'][] = (int) $user->ID; } $orders = wc_get_orders( $order_query ); if ( 0 < count( $orders ) ) { foreach ( $orders as $order ) { $data_to_export[] = array( 'group_id' => 'woocommerce_orders', 'group_label' => __( 'Orders', 'woocommerce' ), 'group_description' => __( 'User’s WooCommerce orders data.', 'woocommerce' ), 'item_id' => 'order-' . $order->get_id(), 'data' => self::get_order_personal_data( $order ), ); } $done = 10 > count( $orders ); } return array( 'data' => $data_to_export, 'done' => $done, ); } /** * Finds and exports customer download logs by email address. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @throws Exception When WC_Data_Store validation fails. * @return array An array of personal data in name value pairs */ public static function download_data_exporter( $email_address, $page ) { $done = true; $page = (int) $page; $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $data_to_export = array(); $downloads_query = array( 'limit' => 10, 'page' => $page, ); if ( $user instanceof WP_User ) { $downloads_query['user_id'] = (int) $user->ID; } else { $downloads_query['user_email'] = $email_address; } $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); $customer_download_log_data_store = WC_Data_Store::load( 'customer-download-log' ); $downloads = $customer_download_data_store->get_downloads( $downloads_query ); if ( 0 < count( $downloads ) ) { foreach ( $downloads as $download ) { $data_to_export[] = array( 'group_id' => 'woocommerce_downloads', /* translators: This is the headline for a list of downloads purchased from the store for a given user. */ 'group_label' => __( 'Purchased Downloads', 'woocommerce' ), 'group_description' => __( 'User’s WooCommerce purchased downloads data.', 'woocommerce' ), 'item_id' => 'download-' . $download->get_id(), 'data' => self::get_download_personal_data( $download ), ); $download_logs = $customer_download_log_data_store->get_download_logs_for_permission( $download->get_id() ); foreach ( $download_logs as $download_log ) { $data_to_export[] = array( 'group_id' => 'woocommerce_download_logs', /* translators: This is the headline for a list of access logs for downloads purchased from the store for a given user. */ 'group_label' => __( 'Access to Purchased Downloads', 'woocommerce' ), 'group_description' => __( 'User’s WooCommerce access to purchased downloads data.', 'woocommerce' ), 'item_id' => 'download-log-' . $download_log->get_id(), 'data' => array( array( 'name' => __( 'Download ID', 'woocommerce' ), 'value' => $download_log->get_permission_id(), ), array( 'name' => __( 'Timestamp', 'woocommerce' ), 'value' => $download_log->get_timestamp(), ), array( 'name' => __( 'IP Address', 'woocommerce' ), 'value' => $download_log->get_user_ip_address(), ), ), ); } } $done = 10 > count( $downloads ); } return array( 'data' => $data_to_export, 'done' => $done, ); } /** * Get personal data (key/value pairs) for a user object. * * @since 3.4.0 * @param WP_User $user user object. * @throws Exception If customer cannot be read/found and $data is set to WC_Customer class. * @return array */ protected static function get_customer_personal_data( $user ) { $personal_data = array(); $customer = new WC_Customer( $user->ID ); if ( ! $customer ) { return array(); } $props_to_export = apply_filters( 'woocommerce_privacy_export_customer_personal_data_props', array( 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), 'billing_company' => __( 'Billing Company', 'woocommerce' ), 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), 'billing_city' => __( 'Billing City', 'woocommerce' ), 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), 'billing_state' => __( 'Billing State', 'woocommerce' ), 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), 'billing_phone' => __( 'Billing Phone Number', 'woocommerce' ), 'billing_email' => __( 'Email Address', 'woocommerce' ), 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), 'shipping_city' => __( 'Shipping City', 'woocommerce' ), 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), 'shipping_state' => __( 'Shipping State', 'woocommerce' ), 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), ), $customer ); foreach ( $props_to_export as $prop => $description ) { $value = ''; if ( is_callable( array( $customer, 'get_' . $prop ) ) ) { $value = $customer->{"get_$prop"}( 'edit' ); } $value = apply_filters( 'woocommerce_privacy_export_customer_personal_data_prop_value', $value, $prop, $customer ); if ( $value ) { $personal_data[] = array( 'name' => $description, 'value' => $value, ); } } /** * Allow extensions to register their own personal data for this customer for the export. * * @since 3.4.0 * @param array $personal_data Array of name value pairs. * @param WC_Order $order A customer object. */ $personal_data = apply_filters( 'woocommerce_privacy_export_customer_personal_data', $personal_data, $customer ); return $personal_data; } /** * Get personal data (key/value pairs) for an order object. * * @since 3.4.0 * @param WC_Order $order Order object. * @return array */ protected static function get_order_personal_data( $order ) { $personal_data = array(); $props_to_export = apply_filters( 'woocommerce_privacy_export_order_personal_data_props', array( 'order_number' => __( 'Order Number', 'woocommerce' ), 'date_created' => __( 'Order Date', 'woocommerce' ), 'total' => __( 'Order Total', 'woocommerce' ), 'items' => __( 'Items Purchased', 'woocommerce' ), 'customer_ip_address' => __( 'IP Address', 'woocommerce' ), 'customer_user_agent' => __( 'Browser User Agent', 'woocommerce' ), 'formatted_billing_address' => __( 'Billing Address', 'woocommerce' ), 'formatted_shipping_address' => __( 'Shipping Address', 'woocommerce' ), 'billing_phone' => __( 'Phone Number', 'woocommerce' ), 'billing_email' => __( 'Email Address', 'woocommerce' ), 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), ), $order ); foreach ( $props_to_export as $prop => $name ) { $value = ''; switch ( $prop ) { case 'items': $item_names = array(); foreach ( $order->get_items() as $item ) { $item_names[] = $item->get_name() . ' x ' . $item->get_quantity(); } $value = implode( ', ', $item_names ); break; case 'date_created': $value = wc_format_datetime( $order->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ); break; case 'formatted_billing_address': case 'formatted_shipping_address': $value = preg_replace( '#<br\s*/?>#i', ', ', $order->{"get_$prop"}() ); break; default: if ( is_callable( array( $order, 'get_' . $prop ) ) ) { $value = $order->{"get_$prop"}(); } break; } $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_prop', $value, $prop, $order ); if ( $value ) { $personal_data[] = array( 'name' => $name, 'value' => $value, ); } } // Export meta data. $meta_to_export = apply_filters( 'woocommerce_privacy_export_order_personal_data_meta', array( 'Payer first name' => __( 'Payer first name', 'woocommerce' ), 'Payer last name' => __( 'Payer last name', 'woocommerce' ), 'Payer PayPal address' => __( 'Payer PayPal address', 'woocommerce' ), 'Transaction ID' => __( 'Transaction ID', 'woocommerce' ), ) ); if ( ! empty( $meta_to_export ) && is_array( $meta_to_export ) ) { foreach ( $meta_to_export as $meta_key => $name ) { $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_meta_value', $order->get_meta( $meta_key ), $meta_key, $order ); if ( $value ) { $personal_data[] = array( 'name' => $name, 'value' => $value, ); } } } /** * Allow extensions to register their own personal data for this order for the export. * * @since 3.4.0 * @param array $personal_data Array of name value pairs to expose in the export. * @param WC_Order $order An order object. */ $personal_data = apply_filters( 'woocommerce_privacy_export_order_personal_data', $personal_data, $order ); return $personal_data; } /** * Get personal data (key/value pairs) for a download object. * * @since 3.4.0 * @param WC_Order $download Download object. * @return array */ protected static function get_download_personal_data( $download ) { $personal_data = array( array( 'name' => __( 'Download ID', 'woocommerce' ), 'value' => $download->get_id(), ), array( 'name' => __( 'Order ID', 'woocommerce' ), 'value' => $download->get_order_id(), ), array( 'name' => __( 'Product', 'woocommerce' ), 'value' => get_the_title( $download->get_product_id() ), ), array( 'name' => __( 'User email', 'woocommerce' ), 'value' => $download->get_user_email(), ), array( 'name' => __( 'Downloads remaining', 'woocommerce' ), 'value' => $download->get_downloads_remaining(), ), array( 'name' => __( 'Download count', 'woocommerce' ), 'value' => $download->get_download_count(), ), array( 'name' => __( 'Access granted', 'woocommerce' ), 'value' => gmdate( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), ), array( 'name' => __( 'Access expires', 'woocommerce' ), 'value' => ! is_null( $download->get_access_expires( 'edit' ) ) ? gmdate( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, ), ); /** * Allow extensions to register their own personal data for this download for the export. * * @since 3.4.0 * @param array $personal_data Array of name value pairs to expose in the export. * @param WC_Order $order An order object. */ $personal_data = apply_filters( 'woocommerce_privacy_export_download_personal_data', $personal_data, $download ); return $personal_data; } /** * Finds and exports payment tokens by email address for a customer. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function customer_tokens_exporter( $email_address, $page ) { $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $data_to_export = array(); if ( ! $user instanceof WP_User ) { return array( 'data' => $data_to_export, 'done' => true, ); } $tokens = WC_Payment_Tokens::get_tokens( array( 'user_id' => $user->ID, 'limit' => 10, 'page' => $page, ) ); if ( 0 < count( $tokens ) ) { foreach ( $tokens as $token ) { $data_to_export[] = array( 'group_id' => 'woocommerce_tokens', 'group_label' => __( 'Payment Tokens', 'woocommerce' ), 'group_description' => __( 'User’s WooCommerce payment tokens data.', 'woocommerce' ), 'item_id' => 'token-' . $token->get_id(), 'data' => array( array( 'name' => __( 'Token', 'woocommerce' ), 'value' => $token->get_display_name(), ), ), ); } $done = 10 > count( $tokens ); } else { $done = true; } return array( 'data' => $data_to_export, 'done' => $done, ); } } includes/class-wc-payment-tokens.php 0000644 00000013634 15132754524 0013572 0 ustar 00 <?php /** * WooCommerce Payment Tokens * * An API for storing and managing tokens for gateways and customers. * * @package WooCommerce\Classes * @version 3.0.0 * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Payment tokens class. */ class WC_Payment_Tokens { /** * Gets valid tokens from the database based on user defined criteria. * * @since 2.6.0 * @param array $args Query arguments { * Array of query parameters. * * @type string $token_id Token ID. * @type string $user_id User ID. * @type string $gateway_id Gateway ID. * @type string $type Token type. * } * @return WC_Payment_Token[] */ public static function get_tokens( $args ) { $args = wp_parse_args( $args, array( 'token_id' => '', 'user_id' => '', 'gateway_id' => '', 'type' => '', ) ); $data_store = WC_Data_Store::load( 'payment-token' ); $token_results = $data_store->get_tokens( $args ); $tokens = array(); if ( ! empty( $token_results ) ) { foreach ( $token_results as $token_result ) { $_token = self::get( $token_result->token_id, $token_result ); if ( ! empty( $_token ) ) { $tokens[ $token_result->token_id ] = $_token; } } } return $tokens; } /** * Returns an array of payment token objects associated with the passed customer ID. * * @since 2.6.0 * @param int $customer_id Customer ID. * @param string $gateway_id Optional Gateway ID for getting tokens for a specific gateway. * @return WC_Payment_Token[] Array of token objects. */ public static function get_customer_tokens( $customer_id, $gateway_id = '' ) { if ( $customer_id < 1 ) { return array(); } $tokens = self::get_tokens( array( 'user_id' => $customer_id, 'gateway_id' => $gateway_id, ) ); return apply_filters( 'woocommerce_get_customer_payment_tokens', $tokens, $customer_id, $gateway_id ); } /** * Returns a customers default token or NULL if there is no default token. * * @since 2.6.0 * @param int $customer_id Customer ID. * @return WC_Payment_Token|null */ public static function get_customer_default_token( $customer_id ) { if ( $customer_id < 1 ) { return null; } $data_store = WC_Data_Store::load( 'payment-token' ); $token = $data_store->get_users_default_token( $customer_id ); if ( $token ) { return self::get( $token->token_id, $token ); } else { return null; } } /** * Returns an array of payment token objects associated with the passed order ID. * * @since 2.6.0 * @param int $order_id Order ID. * @return WC_Payment_Token[] Array of token objects. */ public static function get_order_tokens( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return array(); } $token_ids = $order->get_payment_tokens(); if ( empty( $token_ids ) ) { return array(); } $tokens = self::get_tokens( array( 'token_id' => $token_ids, ) ); return apply_filters( 'woocommerce_get_order_payment_tokens', $tokens, $order_id ); } /** * Get a token object by ID. * * @since 2.6.0 * * @param int $token_id Token ID. * @param object $token_result Token result. * @return null|WC_Payment_Token Returns a valid payment token or null if no token can be found. */ public static function get( $token_id, $token_result = null ) { $data_store = WC_Data_Store::load( 'payment-token' ); if ( is_null( $token_result ) ) { $token_result = $data_store->get_token_by_id( $token_id ); // Still empty? Token doesn't exist? Don't continue. if ( empty( $token_result ) ) { return null; } } $token_class = self::get_token_classname( $token_result->type ); if ( class_exists( $token_class ) ) { $meta = $data_store->get_metadata( $token_id ); $passed_meta = array(); if ( ! empty( $meta ) ) { foreach ( $meta as $meta_key => $meta_value ) { $passed_meta[ $meta_key ] = $meta_value[0]; } } return new $token_class( $token_id, (array) $token_result, $passed_meta ); } return null; } /** * Remove a payment token from the database by ID. * * @since 2.6.0 * @param int $token_id Token ID. */ public static function delete( $token_id ) { $type = self::get_token_type_by_id( $token_id ); if ( ! empty( $type ) ) { $class = self::get_token_classname( $type ); $token = new $class( $token_id ); $token->delete(); } } /** * Loops through all of a users payment tokens and sets is_default to false for all but a specific token. * * @since 2.6.0 * @param int $user_id User to set a default for. * @param int $token_id The ID of the token that should be default. */ public static function set_users_default( $user_id, $token_id ) { $data_store = WC_Data_Store::load( 'payment-token' ); $users_tokens = self::get_customer_tokens( $user_id ); foreach ( $users_tokens as $token ) { if ( $token_id === $token->get_id() ) { $data_store->set_default_status( $token->get_id(), true ); do_action( 'woocommerce_payment_token_set_default', $token_id, $token ); } else { $data_store->set_default_status( $token->get_id(), false ); } } } /** * Returns what type (credit card, echeck, etc) of token a token is by ID. * * @since 2.6.0 * @param int $token_id Token ID. * @return string Type. */ public static function get_token_type_by_id( $token_id ) { $data_store = WC_Data_Store::load( 'payment-token' ); return $data_store->get_token_type_by_id( $token_id ); } /** * Get classname based on token type. * * @since 3.8.0 * @param string $type Token type. * @return string */ protected static function get_token_classname( $type ) { /** * Filter payment token class per type. * * @since 3.8.0 * @param string $class Payment token class. * @param string $type Token type. */ return apply_filters( 'woocommerce_payment_token_class', 'WC_Payment_Token_' . $type, $type ); } } includes/wc-cart-functions.php 0000644 00000042240 15132754524 0012443 0 ustar 00 <?php /** * WooCommerce Cart Functions * * Functions for cart specific things. * * @package WooCommerce\Functions * @version 2.5.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Prevent password protected products being added to the cart. * * @param bool $passed Validation. * @param int $product_id Product ID. * @return bool */ function wc_protected_product_add_to_cart( $passed, $product_id ) { if ( post_password_required( $product_id ) ) { $passed = false; wc_add_notice( __( 'This product is protected and cannot be purchased.', 'woocommerce' ), 'error' ); } return $passed; } add_filter( 'woocommerce_add_to_cart_validation', 'wc_protected_product_add_to_cart', 10, 2 ); /** * Clears the cart session when called. */ function wc_empty_cart() { if ( ! isset( WC()->cart ) || '' === WC()->cart ) { WC()->cart = new WC_Cart(); } WC()->cart->empty_cart( false ); } /** * Load the persistent cart. * * @param string $user_login User login. * @param WP_User $user User data. * @deprecated 2.3 */ function wc_load_persistent_cart( $user_login, $user ) { if ( ! $user || ! apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { return; } $saved_cart = get_user_meta( $user->ID, '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); if ( ! $saved_cart ) { return; } $cart = WC()->session->cart; if ( empty( $cart ) || ! is_array( $cart ) || 0 === count( $cart ) ) { WC()->session->cart = $saved_cart['cart']; } } /** * Retrieves unvalidated referer from '_wp_http_referer' or HTTP referer. * * Do not use for redirects, use {@see wp_get_referer()} instead. * * @since 2.6.1 * @return string|false Referer URL on success, false on failure. */ function wc_get_raw_referer() { if ( function_exists( 'wp_get_raw_referer' ) ) { return wp_get_raw_referer(); } if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok. return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok. } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok. return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok. } return false; } /** * Add to cart messages. * * @param int|array $products Product ID list or single product ID. * @param bool $show_qty Should qty's be shown? Added in 2.6.0. * @param bool $return Return message rather than add it. * * @return mixed */ function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) { $titles = array(); $count = 0; if ( ! is_array( $products ) ) { $products = array( $products => 1 ); $show_qty = false; } if ( ! $show_qty ) { $products = array_fill_keys( array_keys( $products ), 1 ); } foreach ( $products as $product_id => $qty ) { /* translators: %s: product name */ $titles[] = apply_filters( 'woocommerce_add_to_cart_qty_html', ( $qty > 1 ? absint( $qty ) . ' × ' : '' ), $product_id ) . apply_filters( 'woocommerce_add_to_cart_item_name_in_quotes', sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), strip_tags( get_the_title( $product_id ) ) ), $product_id ); $count += $qty; } $titles = array_filter( $titles ); /* translators: %s: product name */ $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); // Output success messages. if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); $message = sprintf( '<a href="%s" tabindex="1" class="button wc-forward">%s</a> %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); } else { $message = sprintf( '<a href="%s" tabindex="1" class="button wc-forward">%s</a> %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); } if ( has_filter( 'wc_add_to_cart_message' ) ) { wc_deprecated_function( 'The wc_add_to_cart_message filter', '3.0', 'wc_add_to_cart_message_html' ); $message = apply_filters( 'wc_add_to_cart_message', $message, $product_id ); } $message = apply_filters( 'wc_add_to_cart_message_html', $message, $products, $show_qty ); if ( $return ) { return $message; } else { wc_add_notice( $message, apply_filters( 'woocommerce_add_to_cart_notice_type', 'success' ) ); } } /** * Comma separate a list of item names, and replace final comma with 'and'. * * @param array $items Cart items. * @return string */ function wc_format_list_of_items( $items ) { $item_string = ''; foreach ( $items as $key => $item ) { $item_string .= $item; if ( count( $items ) === $key + 2 ) { $item_string .= ' ' . __( 'and', 'woocommerce' ) . ' '; } elseif ( count( $items ) !== $key + 1 ) { $item_string .= ', '; } } return $item_string; } /** * Clear cart after payment. */ function wc_clear_cart_after_payment() { global $wp; if ( ! empty( $wp->query_vars['order-received'] ) ) { $order_id = absint( $wp->query_vars['order-received'] ); $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. if ( $order_id > 0 ) { $order = wc_get_order( $order_id ); if ( $order && hash_equals( $order->get_order_key(), $order_key ) ) { WC()->cart->empty_cart(); } } } if ( WC()->session->order_awaiting_payment > 0 ) { $order = wc_get_order( WC()->session->order_awaiting_payment ); if ( $order && $order->get_id() > 0 ) { // If the order has not failed, or is not pending, the order must have gone through. if ( ! $order->has_status( array( 'failed', 'pending', 'cancelled' ) ) ) { WC()->cart->empty_cart(); } } } } add_action( 'get_header', 'wc_clear_cart_after_payment' ); /** * Get the subtotal. */ function wc_cart_totals_subtotal_html() { echo WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get shipping methods. */ function wc_cart_totals_shipping_html() { $packages = WC()->shipping()->get_packages(); $first = true; foreach ( $packages as $i => $package ) { $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : ''; $product_names = array(); if ( count( $packages ) > 1 ) { foreach ( $package['contents'] as $item_id => $values ) { $product_names[ $item_id ] = $values['data']->get_name() . ' ×' . $values['quantity']; } $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package ); } wc_get_template( 'cart/cart-shipping.php', array( 'package' => $package, 'available_methods' => $package['rates'], 'show_package_details' => count( $packages ) > 1, 'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ), 'package_details' => implode( ', ', $product_names ), /* translators: %d: shipping package number */ 'package_name' => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ), 'index' => $i, 'chosen_method' => $chosen_method, 'formatted_destination' => WC()->countries->get_formatted_address( $package['destination'], ', ' ), 'has_calculated_shipping' => WC()->customer->has_calculated_shipping(), ) ); $first = false; } } /** * Get taxes total. */ function wc_cart_totals_taxes_total_html() { echo apply_filters( 'woocommerce_cart_totals_taxes_total_html', wc_price( WC()->cart->get_taxes_total() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get a coupon label. * * @param string|WC_Coupon $coupon Coupon data or code. * @param bool $echo Echo or return. * * @return string */ function wc_cart_totals_coupon_label( $coupon, $echo = true ) { if ( is_string( $coupon ) ) { $coupon = new WC_Coupon( $coupon ); } /* translators: %s: coupon code */ $label = apply_filters( 'woocommerce_cart_totals_coupon_label', sprintf( esc_html__( 'Coupon: %s', 'woocommerce' ), $coupon->get_code() ), $coupon ); if ( $echo ) { echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { return $label; } } /** * Get coupon display HTML. * * @param string|WC_Coupon $coupon Coupon data or code. */ function wc_cart_totals_coupon_html( $coupon ) { if ( is_string( $coupon ) ) { $coupon = new WC_Coupon( $coupon ); } $discount_amount_html = ''; $amount = WC()->cart->get_coupon_discount_amount( $coupon->get_code(), WC()->cart->display_cart_ex_tax ); $discount_amount_html = '-' . wc_price( $amount ); if ( $coupon->get_free_shipping() && empty( $amount ) ) { $discount_amount_html = __( 'Free shipping coupon', 'woocommerce' ); } $discount_amount_html = apply_filters( 'woocommerce_coupon_discount_amount_html', $discount_amount_html, $coupon ); $coupon_html = $discount_amount_html . ' <a href="' . esc_url( add_query_arg( 'remove_coupon', rawurlencode( $coupon->get_code() ), Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) ? wc_get_checkout_url() : wc_get_cart_url() ) ) . '" class="woocommerce-remove-coupon" data-coupon="' . esc_attr( $coupon->get_code() ) . '">' . __( '[Remove]', 'woocommerce' ) . '</a>'; echo wp_kses( apply_filters( 'woocommerce_cart_totals_coupon_html', $coupon_html, $coupon, $discount_amount_html ), array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'data-coupon' => true ) ) ) ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replace_recursiveFound } /** * Get order total html including inc tax if needed. */ function wc_cart_totals_order_total_html() { $value = '<strong>' . WC()->cart->get_total() . '</strong> '; // If prices are tax inclusive, show taxes here. if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) { $tax_string_array = array(); $cart_tax_totals = WC()->cart->get_tax_totals(); if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) { foreach ( $cart_tax_totals as $code => $tax ) { $tax_string_array[] = sprintf( '%s %s', $tax->formatted_amount, $tax->label ); } } elseif ( ! empty( $cart_tax_totals ) ) { $tax_string_array[] = sprintf( '%s %s', wc_price( WC()->cart->get_taxes_total( true, true ) ), WC()->countries->tax_or_vat() ); } if ( ! empty( $tax_string_array ) ) { $taxable_address = WC()->customer->get_taxable_address(); if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) { $country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ]; /* translators: 1: tax amount 2: country name */ $tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) ); } else { /* translators: %s: tax amount */ $tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) ); } $value .= '<small class="includes_tax">' . $tax_text . '</small>'; } } echo apply_filters( 'woocommerce_cart_totals_order_total_html', $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get the fee value. * * @param object $fee Fee data. */ function wc_cart_totals_fee_html( $fee ) { $cart_totals_fee_html = WC()->cart->display_prices_including_tax() ? wc_price( $fee->total + $fee->tax ) : wc_price( $fee->total ); echo apply_filters( 'woocommerce_cart_totals_fee_html', $cart_totals_fee_html, $fee ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get a shipping methods full label including price. * * @param WC_Shipping_Rate $method Shipping method rate data. * @return string */ function wc_cart_totals_shipping_method_label( $method ) { $label = $method->get_label(); $has_cost = 0 < $method->cost; $hide_cost = ! $has_cost && in_array( $method->get_method_id(), array( 'free_shipping', 'local_pickup' ), true ); if ( $has_cost && ! $hide_cost ) { if ( WC()->cart->display_prices_including_tax() ) { $label .= ': ' . wc_price( $method->cost + $method->get_shipping_tax() ); if ( $method->get_shipping_tax() > 0 && ! wc_prices_include_tax() ) { $label .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>'; } } else { $label .= ': ' . wc_price( $method->cost ); if ( $method->get_shipping_tax() > 0 && wc_prices_include_tax() ) { $label .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } } } return apply_filters( 'woocommerce_cart_shipping_method_full_label', $label, $method ); } /** * Round discount. * * @param double $value Amount to round. * @param int $precision DP to round. * @return float */ function wc_cart_round_discount( $value, $precision ) { return wc_round_discount( $value, $precision ); } /** * Gets chosen shipping method IDs from chosen_shipping_methods session, without instance IDs. * * @since 2.6.2 * @return string[] */ function wc_get_chosen_shipping_method_ids() { $method_ids = array(); $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); foreach ( $chosen_methods as $chosen_method ) { $chosen_method = explode( ':', $chosen_method ); $method_ids[] = current( $chosen_method ); } return $method_ids; } /** * Get chosen method for package from session. * * @since 3.2.0 * @param int $key Key of package. * @param array $package Package data array. * @return string|bool */ function wc_get_chosen_shipping_method_for_package( $key, $package ) { $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); $chosen_method = isset( $chosen_methods[ $key ] ) ? $chosen_methods[ $key ] : false; $changed = wc_shipping_methods_have_changed( $key, $package ); // This is deprecated but here for BW compat. TODO: Remove in 4.0.0. $method_counts = WC()->session->get( 'shipping_method_counts' ); if ( ! empty( $method_counts[ $key ] ) ) { $method_count = absint( $method_counts[ $key ] ); } else { $method_count = 0; } // If not set, not available, or available methods have changed, set to the DEFAULT option. if ( ! $chosen_method || $changed || ! isset( $package['rates'][ $chosen_method ] ) || count( $package['rates'] ) !== $method_count ) { $chosen_method = wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ); $chosen_methods[ $key ] = $chosen_method; $method_counts[ $key ] = count( $package['rates'] ); WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); WC()->session->set( 'shipping_method_counts', $method_counts ); do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); } return $chosen_method; } /** * Choose the default method for a package. * * @since 3.2.0 * @param int $key Key of package. * @param array $package Package data array. * @param string $chosen_method Chosen method id. * @return string */ function wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ) { $rate_keys = array_keys( $package['rates'] ); $default = current( $rate_keys ); $coupons = WC()->cart->get_coupons(); foreach ( $coupons as $coupon ) { if ( $coupon->get_free_shipping() ) { foreach ( $rate_keys as $rate_key ) { if ( 0 === stripos( $rate_key, 'free_shipping' ) ) { $default = $rate_key; break; } } break; } } return apply_filters( 'woocommerce_shipping_chosen_method', $default, $package['rates'], $chosen_method ); } /** * See if the methods have changed since the last request. * * @since 3.2.0 * @param int $key Key of package. * @param array $package Package data array. * @return bool */ function wc_shipping_methods_have_changed( $key, $package ) { // Lookup previous methods from session. $previous_shipping_methods = WC()->session->get( 'previous_shipping_methods' ); // Get new and old rates. $new_rates = array_keys( $package['rates'] ); $prev_rates = isset( $previous_shipping_methods[ $key ] ) ? $previous_shipping_methods[ $key ] : false; // Update session. $previous_shipping_methods[ $key ] = $new_rates; WC()->session->set( 'previous_shipping_methods', $previous_shipping_methods ); return $new_rates !== $prev_rates; } /** * Gets a hash of important product data that when changed should cause cart items to be invalidated. * * The woocommerce_cart_item_data_to_validate filter can be used to add custom properties. * * @param WC_Product $product Product object. * @return string */ function wc_get_cart_item_data_hash( $product ) { return md5( wp_json_encode( apply_filters( 'woocommerce_cart_item_data_to_validate', array( 'type' => $product->get_type(), 'attributes' => 'variation' === $product->get_type() ? $product->get_variation_attributes() : '', ), $product ) ) ); } includes/wc-product-functions.php 0000644 00000140177 15132754524 0013202 0 ustar 00 <?php /** * WooCommerce Product Functions * * Functions for product specific things. * * @package WooCommerce\Functions * @version 3.0.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * Standard way of retrieving products based on certain parameters. * * This function should be used for product retrieval so that we have a data agnostic * way to get a list of products. * * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query * * @since 3.0.0 * @param array $args Array of args (above). * @return array|stdClass Number of pages and an array of product objects if * paginate is true, or just an array of values. */ function wc_get_products( $args ) { // Handle some BW compatibility arg names where wp_query args differ in naming. $map_legacy = array( 'numberposts' => 'limit', 'post_status' => 'status', 'post_parent' => 'parent', 'posts_per_page' => 'limit', 'paged' => 'page', ); foreach ( $map_legacy as $from => $to ) { if ( isset( $args[ $from ] ) ) { $args[ $to ] = $args[ $from ]; } } $query = new WC_Product_Query( $args ); return $query->get_products(); } /** * Main function for returning products, uses the WC_Product_Factory class. * * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting * registered during the init action. * * @since 2.2.0 * * @param mixed $the_product Post object or post ID of the product. * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. * @return WC_Product|null|false */ function wc_get_product( $the_product = false, $deprecated = array() ) { if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) { /* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */ wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' ); return false; } if ( ! empty( $deprecated ) ) { wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' ); } return WC()->product_factory->get_product( $the_product, $deprecated ); } /** * Get a product object. * * @see WC_Product_Factory::get_product_classname * @since 3.9.0 * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned. * @param int $product_id Product ID. * @return WC_Product */ function wc_get_product_object( $product_type, $product_id = 0 ) { $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); return new $classname( $product_id ); } /** * Returns whether or not SKUS are enabled. * * @return bool */ function wc_product_sku_enabled() { return apply_filters( 'wc_product_sku_enabled', true ); } /** * Returns whether or not product weights are enabled. * * @return bool */ function wc_product_weight_enabled() { return apply_filters( 'wc_product_weight_enabled', true ); } /** * Returns whether or not product dimensions (HxWxD) are enabled. * * @return bool */ function wc_product_dimensions_enabled() { return apply_filters( 'wc_product_dimensions_enabled', true ); } /** * Clear transient cache for product data. * * @param int $post_id (default: 0) The product ID. */ function wc_delete_product_transients( $post_id = 0 ) { // Transient data to clear with a fixed name which may be stale after product updates. $transients_to_clear = array( 'wc_products_onsale', 'wc_featured_products', 'wc_outofstock_count', 'wc_low_stock_count', ); foreach ( $transients_to_clear as $transient ) { delete_transient( $transient ); } if ( $post_id > 0 ) { // Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID. $post_transient_names = array( 'wc_product_children_', 'wc_var_prices_', 'wc_related_', 'wc_child_has_weight_', 'wc_child_has_dimensions_', ); foreach ( $post_transient_names as $transient ) { delete_transient( $transient . $post_id ); } } // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'product', true ); do_action( 'woocommerce_delete_product_transients', $post_id ); } /** * Function that returns an array containing the IDs of the products that are on sale. * * @since 2.0 * @return array */ function wc_get_product_ids_on_sale() { // Load from cache. $product_ids_on_sale = get_transient( 'wc_products_onsale' ); // Valid cache found. if ( false !== $product_ids_on_sale ) { return $product_ids_on_sale; } $data_store = WC_Data_Store::load( 'product' ); $on_sale_products = $data_store->get_on_sale_products(); $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); return $product_ids_on_sale; } /** * Function that returns an array containing the IDs of the featured products. * * @since 2.1 * @return array */ function wc_get_featured_product_ids() { // Load from cache. $featured_product_ids = get_transient( 'wc_featured_products' ); // Valid cache found. if ( false !== $featured_product_ids ) { return $featured_product_ids; } $data_store = WC_Data_Store::load( 'product' ); $featured = $data_store->get_featured_product_ids(); $product_ids = array_keys( $featured ); $parent_ids = array_values( array_filter( $featured ) ); $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); return $featured_product_ids; } /** * Filter to allow product_cat in the permalinks for products. * * @param string $permalink The existing permalink URL. * @param WP_Post $post WP_Post object. * @return string */ function wc_product_post_type_link( $permalink, $post ) { // Abort if post is not a product. if ( 'product' !== $post->post_type ) { return $permalink; } // Abort early if the placeholder rewrite tag isn't in the generated URL. if ( false === strpos( $permalink, '%' ) ) { return $permalink; } // Get the custom taxonomy terms in use by this post. $terms = get_the_terms( $post->ID, 'product_cat' ); if ( ! empty( $terms ) ) { $terms = wp_list_sort( $terms, array( 'parent' => 'DESC', 'term_id' => 'ASC', ) ); $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); $product_cat = $category_object->slug; if ( $category_object->parent ) { $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); foreach ( $ancestors as $ancestor ) { $ancestor_object = get_term( $ancestor, 'product_cat' ); if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) { $product_cat = $ancestor_object->slug; } else { $product_cat = $ancestor_object->slug . '/' . $product_cat; } } } } else { // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); } $find = array( '%year%', '%monthnum%', '%day%', '%hour%', '%minute%', '%second%', '%post_id%', '%category%', '%product_cat%', ); $replace = array( date_i18n( 'Y', strtotime( $post->post_date ) ), date_i18n( 'm', strtotime( $post->post_date ) ), date_i18n( 'd', strtotime( $post->post_date ) ), date_i18n( 'H', strtotime( $post->post_date ) ), date_i18n( 'i', strtotime( $post->post_date ) ), date_i18n( 's', strtotime( $post->post_date ) ), $post->ID, $product_cat, $product_cat, ); $permalink = str_replace( $find, $replace, $permalink ); return $permalink; } add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); /** * Get the placeholder image URL either from media, or use the fallback image. * * @param string $size Thumbnail size to use. * @return string */ function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) { $src = WC()->plugin_url() . '/assets/images/placeholder.png'; $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); if ( ! empty( $placeholder_image ) ) { if ( is_numeric( $placeholder_image ) ) { $image = wp_get_attachment_image_src( $placeholder_image, $size ); if ( ! empty( $image[0] ) ) { $src = $image[0]; } } else { $src = $placeholder_image; } } return apply_filters( 'woocommerce_placeholder_img_src', $src ); } /** * Get the placeholder image. * * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness. * * @param string $size Image size. * @param string|array $attr Optional. Attributes for the image markup. Default empty. * @return string */ function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { $dimensions = wc_get_image_size( $size ); $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); $default_attr = array( 'class' => 'woocommerce-placeholder wp-post-image', 'alt' => __( 'Placeholder', 'woocommerce' ), ); $attr = wp_parse_args( $attr, $default_attr ); if ( wp_attachment_is_image( $placeholder_image ) ) { $image_html = wp_get_attachment_image( $placeholder_image, $size, false, $attr ); } else { $image = wc_placeholder_img_src( $size ); $hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] ); $attributes = array(); foreach ( $attr as $name => $value ) { $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; } $image_html = '<img src="' . esc_url( $image ) . '" ' . $hwstring . implode( ' ', $attribute ) . '/>'; } return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); } /** * Variation Formatting. * * Gets a formatted version of variation data or item meta. * * @param array|WC_Product_Variation $variation Variation object. * @param bool $flat Should this be a flat list or HTML list? (default: false). * @param bool $include_names include attribute names/labels in the list. * @param bool $skip_attributes_in_name Do not list attributes already part of the variation name. * @return string */ function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) { $return = ''; if ( is_a( $variation, 'WC_Product_Variation' ) ) { $variation_attributes = $variation->get_attributes(); $product = $variation; $variation_name = $variation->get_name(); } else { $product = false; $variation_name = ''; // Remove attribute_ prefix from names. $variation_attributes = array(); if ( is_array( $variation ) ) { foreach ( $variation as $key => $value ) { $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; } } } $list_type = $include_names ? 'dl' : 'ul'; if ( is_array( $variation_attributes ) ) { if ( ! $flat ) { $return = '<' . $list_type . ' class="variation">'; } $variation_list = array(); foreach ( $variation_attributes as $name => $value ) { // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $name ) ) { $term = get_term_by( 'slug', $value, $name ); if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { $value = $term->name; } } // Do not list attributes already part of the variation name. if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) { continue; } if ( $include_names ) { if ( $flat ) { $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); } else { $variation_list[] = '<dt>' . wc_attribute_label( $name, $product ) . ':</dt><dd>' . rawurldecode( $value ) . '</dd>'; } } else { if ( $flat ) { $variation_list[] = rawurldecode( $value ); } else { $variation_list[] = '<li>' . rawurldecode( $value ) . '</li>'; } } } if ( $flat ) { $return .= implode( ', ', $variation_list ); } else { $return .= implode( '', $variation_list ); } if ( ! $flat ) { $return .= '</' . $list_type . '>'; } } return $return; } /** * Function which handles the start and end of scheduled sales via cron. */ function wc_scheduled_sales() { $data_store = WC_Data_Store::load( 'product' ); // Sales which are due to start. $product_ids = $data_store->get_starting_sales(); if ( $product_ids ) { do_action( 'wc_before_products_starting_sales', $product_ids ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $sale_price = $product->get_sale_price(); if ( $sale_price ) { $product->set_price( $sale_price ); $product->set_date_on_sale_from( '' ); } else { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); } $product->save(); } } do_action( 'wc_after_products_starting_sales', $product_ids ); WC_Cache_Helper::get_transient_version( 'product', true ); delete_transient( 'wc_products_onsale' ); } // Sales which are due to end. $product_ids = $data_store->get_ending_sales(); if ( $product_ids ) { do_action( 'wc_before_products_ending_sales', $product_ids ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $regular_price = $product->get_regular_price(); $product->set_price( $regular_price ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->save(); } } do_action( 'wc_after_products_ending_sales', $product_ids ); WC_Cache_Helper::get_transient_version( 'product', true ); delete_transient( 'wc_products_onsale' ); } } add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); /** * Get attachment image attributes. * * @param array $attr Image attributes. * @return array */ function wc_get_attachment_image_attributes( $attr ) { /* * If the user can manage woocommerce, allow them to * see the image content. */ if ( current_user_can( 'manage_woocommerce' ) ) { return $attr; } /* * If the user does not have the right capabilities, * filter out the image source and replace with placeholder * image. */ if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) { $attr['src'] = wc_placeholder_img_src(); if ( isset( $attr['srcset'] ) ) { $attr['srcset'] = ''; } } return $attr; } add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); /** * Prepare attachment for JavaScript. * * @param array $response JS version of a attachment post object. * @return array */ function wc_prepare_attachment_for_js( $response ) { /* * If the user can manage woocommerce, allow them to * see the image content. */ if ( current_user_can( 'manage_woocommerce' ) ) { return $response; } /* * If the user does not have the right capabilities, * filter out the image source and replace with placeholder * image. */ if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { $response['full']['url'] = wc_placeholder_img_src(); if ( isset( $response['sizes'] ) ) { foreach ( $response['sizes'] as $size => $value ) { $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); } } } return $response; } add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); /** * Track product views. */ function wc_track_product_view() { if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { return; } global $post; if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine. $viewed_products = array(); } else { $viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine. } // Unset if already in viewed products list. $keys = array_flip( $viewed_products ); if ( isset( $keys[ $post->ID ] ) ) { unset( $viewed_products[ $keys[ $post->ID ] ] ); } $viewed_products[] = $post->ID; if ( count( $viewed_products ) > 15 ) { array_shift( $viewed_products ); } // Store for session only. wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); } add_action( 'template_redirect', 'wc_track_product_view', 20 ); /** * Get product types. * * @since 2.2 * @return array */ function wc_get_product_types() { return (array) apply_filters( 'product_type_selector', array( 'simple' => __( 'Simple product', 'woocommerce' ), 'grouped' => __( 'Grouped product', 'woocommerce' ), 'external' => __( 'External/Affiliate product', 'woocommerce' ), 'variable' => __( 'Variable product', 'woocommerce' ), ) ); } /** * Check if product sku is unique. * * @since 2.2 * @param int $product_id Product ID. * @param string $sku Product SKU. * @return bool */ function wc_product_has_unique_sku( $product_id, $sku ) { $data_store = WC_Data_Store::load( 'product' ); $sku_found = $data_store->is_existing_sku( $product_id, $sku ); if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { return false; } return true; } /** * Force a unique SKU. * * @since 3.0.0 * @param integer $product_id Product ID. */ function wc_product_force_unique_sku( $product_id ) { $product = wc_get_product( $product_id ); $current_sku = $product ? $product->get_sku( 'edit' ) : ''; if ( $current_sku ) { try { $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); if ( $current_sku !== $new_sku ) { $product->set_sku( $new_sku ); $product->save(); } } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. } } /** * Recursively appends a suffix until a unique SKU is found. * * @since 3.0.0 * @param integer $product_id Product ID. * @param string $sku Product SKU. * @param integer $index An optional index that can be added to the product SKU. * @return string */ function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); } return $generated_sku; } /** * Get product ID by SKU. * * @since 2.3.0 * @param string $sku Product SKU. * @return int */ function wc_get_product_id_by_sku( $sku ) { $data_store = WC_Data_Store::load( 'product' ); return $data_store->get_product_id_by_sku( $sku ); } /** * Get attributes/data for an individual variation from the database and maintain it's integrity. * * @since 2.4.0 * @param int $variation_id Variation ID. * @return array */ function wc_get_product_variation_attributes( $variation_id ) { // Build variation data from meta. $all_meta = get_post_meta( $variation_id ); $parent_id = wp_get_post_parent_id( $variation_id ); $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); $found_parent_attributes = array(); $variation_attributes = array(); // Compare to parent variable product attributes and ensure they match. foreach ( $parent_attributes as $attribute_name => $options ) { if ( ! empty( $options['is_variation'] ) ) { $attribute = 'attribute_' . sanitize_title( $attribute_name ); $found_parent_attributes[] = $attribute; if ( ! array_key_exists( $attribute, $variation_attributes ) ) { $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed. } } } // Get the variation attributes from meta. foreach ( $all_meta as $name => $value ) { // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level. if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) { unset( $variation_attributes[ $name ] ); continue; } /** * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. * Attempt to get full version of the text attribute from the parent. */ if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { foreach ( $parent_attributes as $attribute ) { if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { continue; } $text_attributes = wc_get_text_attributes( $attribute['value'] ); foreach ( $text_attributes as $text_attribute ) { if ( sanitize_title( $text_attribute ) === $value[0] ) { $value[0] = $text_attribute; break; } } } } $variation_attributes[ $name ] = $value[0]; } return $variation_attributes; } /** * Get all product cats for a product by ID, including hierarchy * * @since 2.5.0 * @param int $product_id Product ID. * @return array */ function wc_get_product_cat_ids( $product_id ) { $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); foreach ( $product_cats as $product_cat ) { $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); } return $product_cats; } /** * Gets data about an attachment, such as alt text and captions. * * @since 2.6.0 * * @param int|null $attachment_id Attachment ID. * @param WC_Product|bool $product WC_Product object. * * @return array */ function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { $props = array( 'title' => '', 'caption' => '', 'url' => '', 'alt' => '', 'src' => '', 'srcset' => false, 'sizes' => false, ); $attachment = get_post( $attachment_id ); if ( $attachment && 'attachment' === $attachment->post_type ) { $props['title'] = wp_strip_all_tags( $attachment->post_title ); $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); $props['url'] = wp_get_attachment_url( $attachment_id ); // Alt text. $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); if ( $product && $product instanceof WC_Product ) { $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); } $alt_text = array_filter( $alt_text ); $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; // Large version. $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); $src = wp_get_attachment_image_src( $attachment_id, $full_size ); $props['full_src'] = $src[0]; $props['full_src_w'] = $src[1]; $props['full_src_h'] = $src[2]; // Gallery thumbnail. $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); $props['gallery_thumbnail_src'] = $src[0]; $props['gallery_thumbnail_src_w'] = $src[1]; $props['gallery_thumbnail_src_h'] = $src[2]; // Thumbnail version. $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); $props['thumb_src'] = $src[0]; $props['thumb_src_w'] = $src[1]; $props['thumb_src_h'] = $src[2]; // Image source. $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); $src = wp_get_attachment_image_src( $attachment_id, $image_size ); $props['src'] = $src[0]; $props['src_w'] = $src[1]; $props['src_h'] = $src[2]; $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; } return $props; } /** * Get product visibility options. * * @since 3.0.0 * @return array */ function wc_get_product_visibility_options() { return apply_filters( 'woocommerce_product_visibility_options', array( 'visible' => __( 'Shop and search results', 'woocommerce' ), 'catalog' => __( 'Shop only', 'woocommerce' ), 'search' => __( 'Search results only', 'woocommerce' ), 'hidden' => __( 'Hidden', 'woocommerce' ), ) ); } /** * Get product tax class options. * * @since 3.0.0 * @return array */ function wc_get_product_tax_class_options() { $tax_classes = WC_Tax::get_tax_classes(); $tax_class_options = array(); $tax_class_options[''] = __( 'Standard', 'woocommerce' ); if ( ! empty( $tax_classes ) ) { foreach ( $tax_classes as $class ) { $tax_class_options[ sanitize_title( $class ) ] = $class; } } return $tax_class_options; } /** * Get stock status options. * * @since 3.0.0 * @return array */ function wc_get_product_stock_status_options() { return apply_filters( 'woocommerce_product_stock_status_options', array( 'instock' => __( 'In stock', 'woocommerce' ), 'outofstock' => __( 'Out of stock', 'woocommerce' ), 'onbackorder' => __( 'On backorder', 'woocommerce' ), ) ); } /** * Get backorder options. * * @since 3.0.0 * @return array */ function wc_get_product_backorder_options() { return array( 'no' => __( 'Do not allow', 'woocommerce' ), 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), 'yes' => __( 'Allow', 'woocommerce' ), ); } /** * Get related products based on product category and tags. * * @since 3.0.0 * @param int $product_id Product ID. * @param int $limit Limit of results. * @param array $exclude_ids Exclude IDs from the results. * @return array */ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { $product_id = absint( $product_id ); $limit = $limit >= -1 ? $limit : 5; $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); $transient_name = 'wc_related_' . $product_id; $query_args = http_build_query( array( 'limit' => $limit, 'exclude_ids' => $exclude_ids, ) ); $transient = get_transient( $transient_name ); $related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; // We want to query related posts if they are not cached, or we don't have enough. if ( false === $related_posts || count( $related_posts ) < $limit ) { $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { $related_posts = array(); } else { $data_store = WC_Data_Store::load( 'product' ); $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); } if ( $transient ) { $transient[ $query_args ] = $related_posts; } else { $transient = array( $query_args => $related_posts ); } set_transient( $transient_name, $transient, DAY_IN_SECONDS ); } $related_posts = apply_filters( 'woocommerce_related_products', $related_posts, $product_id, array( 'limit' => $limit, 'excluded_ids' => $exclude_ids, ) ); if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { shuffle( $related_posts ); } return array_slice( $related_posts, 0, $limit ); } /** * Retrieves product term ids for a taxonomy. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @return array */ function wc_get_product_term_ids( $product_id, $taxonomy ) { $terms = get_the_terms( $product_id, $taxonomy ); return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); } /** * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax included, or an empty string if price calculation failed. */ function wc_get_price_including_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( '' === $price ) { return ''; } elseif ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; $return_price = $line_price; if ( $product->is_taxable() ) { if ( ! wc_prices_include_tax() ) { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes_total = array_sum( $taxes ); } else { $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); } $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); /** * If the customer is excempt from VAT, remove the taxes here. * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. */ if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $remove_taxes_total = array_sum( $remove_taxes ); } else { $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); } $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); /** * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. */ } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $base_taxes_total = array_sum( $base_taxes ); $modded_taxes_total = array_sum( $modded_taxes ); } else { $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); } $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); } } } return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); } /** * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax excluded, or an empty string if price calculation failed. */ function wc_get_price_excluding_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( '' === $price ) { return ''; } elseif ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; if ( $product->is_taxable() && wc_prices_include_tax() ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $customer_id = $order ? $order->get_customer_id() : 0; if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) || ! $customer_id ) { $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); } else { $customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ); $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); } $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. } else { $return_price = $line_price; } return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); } /** * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float */ function wc_get_price_to_display( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => 1, 'price' => $product->get_price(), ) ); $price = $args['price']; $qty = $args['qty']; return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? wc_get_price_including_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ) : wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ); } /** * Returns the product categories in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); } /** * Returns the product tags in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); } /** * Callback for array filter to get visible only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); } /** * Callback for array filter to get visible grouped products only. * * @since 3.1.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible_grouped( $product ) { return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) ); } /** * Callback for array filter to get products the user can edit only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_editable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); } /** * Callback for array filter to get products the user can view only. * * @since 3.4.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_readable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); } /** * Sort an array of products by a value. * * @since 3.0.0 * * @param array $products List of products to be ordered. * @param string $orderby Optional order criteria. * @param string $order Ascending or descending order. * * @return array */ function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { $orderby = strtolower( $orderby ); $order = strtolower( $order ); switch ( $orderby ) { case 'title': case 'id': case 'date': case 'modified': case 'menu_order': case 'price': usort( $products, 'wc_products_array_orderby_' . $orderby ); break; case 'none': break; default: shuffle( $products ); break; } if ( 'desc' === $order ) { $products = array_reverse( $products ); } return $products; } /** * Sort by title. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_title( $a, $b ) { return strcasecmp( $a->get_name(), $b->get_name() ); } /** * Sort by id. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_id( $a, $b ) { if ( $a->get_id() === $b->get_id() ) { return 0; } return ( $a->get_id() < $b->get_id() ) ? -1 : 1; } /** * Sort by date. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_date( $a, $b ) { if ( $a->get_date_created() === $b->get_date_created() ) { return 0; } return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; } /** * Sort by modified. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_modified( $a, $b ) { if ( $a->get_date_modified() === $b->get_date_modified() ) { return 0; } return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; } /** * Sort by menu order. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_menu_order( $a, $b ) { if ( $a->get_menu_order() === $b->get_menu_order() ) { return 0; } return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; } /** * Sort by price low to high. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_price( $a, $b ) { if ( $a->get_price() === $b->get_price() ) { return 0; } return ( $a->get_price() < $b->get_price() ) ? -1 : 1; } /** * Queue a product for syncing at the end of the request. * * @param int $product_id Product ID. */ function wc_deferred_product_sync( $product_id ) { global $wc_deferred_product_sync; if ( empty( $wc_deferred_product_sync ) ) { $wc_deferred_product_sync = array(); } $wc_deferred_product_sync[] = $product_id; } /** * See if the lookup table is being generated already. * * @since 3.6.0 * @return bool */ function wc_update_product_lookup_tables_is_running() { $table_updates_pending = WC()->queue()->search( array( 'status' => 'pending', 'group' => 'wc_update_product_lookup_tables', 'per_page' => 1, ) ); return (bool) count( $table_updates_pending ); } /** * Populate lookup table data for products. * * @since 3.6.0 */ function wc_update_product_lookup_tables() { global $wpdb; $is_cli = Constants::is_true( 'WP_CLI' ); if ( ! $is_cli ) { WC_Admin_Notices::add_notice( 'regenerating_lookup_table' ); } // Note that the table is not yet generated. update_option( 'woocommerce_product_lookup_table_is_generating', true ); // Make a row per product in lookup table. $wpdb->query( " INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) SELECT posts.ID FROM {$wpdb->posts} posts WHERE posts.post_type IN ('product', 'product_variation') " ); // List of column names in the lookup table we need to populate. $columns = array( 'min_max_price', 'stock_quantity', 'sku', 'stock_status', 'average_rating', 'total_sales', 'downloadable', 'virtual', 'onsale', 'tax_class', 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. ); foreach ( $columns as $index => $column ) { if ( $is_cli ) { wc_update_product_lookup_tables_column( $column ); } else { WC()->queue()->schedule_single( time() + $index, 'wc_update_product_lookup_tables_column', array( 'column' => $column, ), 'wc_update_product_lookup_tables' ); } } // Rating counts are serialised so they have to be unserialised before populating the lookup table. if ( $is_cli ) { $rating_count_rows = $wpdb->get_results( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ", ARRAY_A ); wc_update_product_lookup_tables_rating_count( $rating_count_rows ); } else { WC()->queue()->schedule_single( time() + 10, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => 0, 'limit' => 50, ), 'wc_update_product_lookup_tables' ); } } /** * Populate lookup table column data. * * @since 3.6.0 * @param string $column Column name to set. */ function wc_update_product_lookup_tables_column( $column ) { if ( empty( $column ) ) { return; } global $wpdb; switch ( $column ) { case 'min_max_price': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN ( SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price FROM {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' WHERE meta1.meta_value <> '' GROUP BY lookup_table.product_id ) as source on source.product_id = lookup_table.product_id SET lookup_table.min_price = source.min_price, lookup_table.max_price = source.max_price " ); break; case 'stock_quantity': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' SET lookup_table.stock_quantity = meta2.meta_value WHERE meta1.meta_value = 'yes' " ); break; case 'sku': case 'stock_status': case 'average_rating': case 'total_sales': case 'tax_class': case 'tax_status': if ( 'total_sales' === $column ) { $meta_key = 'total_sales'; } elseif ( 'average_rating' === $column ) { $meta_key = '_wc_average_rating'; } else { $meta_key = '_' . $column; } $column = esc_sql( $column ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s SET lookup_table.`{$column}` = meta.meta_value ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'downloadable': case 'virtual': $column = esc_sql( $column ); $meta_key = '_' . $column; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s SET lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'onsale': $column = esc_sql( $column ); $decimals = absint( wc_get_price_decimals() ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' SET lookup_table.`{$column}` = IF ( CAST( meta1.meta_value AS DECIMAL ) >= 0 AND CAST( meta2.meta_value AS CHAR ) != '' AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) , 1, 0 ) ", $decimals, $decimals ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; } // Final column - mark complete. if ( 'tax_status' === $column ) { delete_option( 'woocommerce_product_lookup_table_is_generating' ); } } add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); /** * Populate rating count lookup table data for products. * * @since 3.6.0 * @param array $rows Rows of rating counts to update in lookup table. */ function wc_update_product_lookup_tables_rating_count( $rows ) { if ( ! $rows || ! is_array( $rows ) ) { return; } global $wpdb; foreach ( $rows as $row ) { $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); $wpdb->update( $wpdb->wc_product_meta_lookup, array( 'rating_count' => absint( $count ), ), array( 'product_id' => absint( $row['post_id'] ), ) ); } } /** * Populate a batch of rating count lookup table data for products. * * @since 3.6.2 * @param array $offset Offset to query. * @param array $limit Limit to query. */ function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { global $wpdb; if ( ! $limit ) { return; } $rating_count_rows = $wpdb->get_results( $wpdb->prepare( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ORDER BY post_id ASC LIMIT %d, %d ", $offset, $limit ), ARRAY_A ); if ( $rating_count_rows ) { wc_update_product_lookup_tables_rating_count( $rating_count_rows ); WC()->queue()->schedule_single( time() + 1, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => $offset + $limit, 'limit' => $limit, ), 'wc_update_product_lookup_tables' ); } } add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); includes/class-wc-order-item-fee.php 0000644 00000021276 15132754524 0013421 0 ustar 00 <?php /** * Order Line Item (fee) * * Fee is an amount of money charged for a particular piece of work * or for a particular right or service, and not supposed to be negative. * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item fee. */ class WC_Order_Item_Fee extends WC_Order_Item { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $extra_data = array( 'tax_class' => '', 'tax_status' => 'taxable', 'amount' => '', 'total' => '', 'total_tax' => '', 'taxes' => array( 'total' => array(), ), ); /** * Get item costs grouped by tax class. * * @since 3.2.0 * @param WC_Order $order Order object. * @return array */ protected function get_tax_class_costs( $order ) { $order_item_tax_classes = $order->get_items_tax_classes(); $costs = array_fill_keys( $order_item_tax_classes, 0 ); $costs['non-taxable'] = 0; foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) { if ( 0 > $item->get_total() ) { continue; } if ( 'taxable' !== $item->get_tax_status() ) { $costs['non-taxable'] += $item->get_total(); } elseif ( 'inherit' === $item->get_tax_class() ) { $inherit_class = reset( $order_item_tax_classes ); $costs[ $inherit_class ] += $item->get_total(); } else { $costs[ $item->get_tax_class() ] += $item->get_total(); } } return array_filter( $costs ); } /** * Calculate item taxes. * * @since 3.2.0 * @param array $calculate_tax_for Location data to get taxes for. Required. * @return bool True if taxes were calculated. */ public function calculate_taxes( $calculate_tax_for = array() ) { if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { return false; } // Use regular calculation unless the fee is negative. if ( 0 <= $this->get_total() ) { return parent::calculate_taxes( $calculate_tax_for ); } if ( wc_tax_enabled() && $this->get_order() ) { // Apportion taxes to order items, shipping, and fees. $order = $this->get_order(); $tax_class_costs = $this->get_tax_class_costs( $order ); $total_costs = array_sum( $tax_class_costs ); $discount_taxes = array(); if ( $total_costs ) { foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { if ( 'non-taxable' === $tax_class ) { continue; } $proportion = $tax_class_cost / $total_costs; $cart_discount_proportion = $this->get_total() * $proportion; $calculate_tax_for['tax_class'] = $tax_class; $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); $discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates ) ); } } $this->set_taxes( array( 'total' => $discount_taxes ) ); } else { $this->set_taxes( false ); } do_action( 'woocommerce_order_item_fee_after_calculate_taxes', $this, $calculate_tax_for ); return true; } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set fee amount. * * @param string $value Amount. */ public function set_amount( $value ) { $this->set_prop( 'amount', wc_format_decimal( $value ) ); } /** * Set tax class. * * @param string $value Tax class. */ public function set_tax_class( $value ) { if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); } $this->set_prop( 'tax_class', $value ); } /** * Set tax_status. * * @param string $value Tax status. */ public function set_tax_status( $value ) { if ( in_array( $value, array( 'taxable', 'none' ), true ) ) { $this->set_prop( 'tax_status', $value ); } else { $this->set_prop( 'tax_status', 'taxable' ); } } /** * Set total. * * @param string $amount Fee amount (do not enter negative amounts). */ public function set_total( $amount ) { $this->set_prop( 'total', wc_format_decimal( $amount ) ); } /** * Set total tax. * * @param string $amount Amount. */ public function set_total_tax( $amount ) { $this->set_prop( 'total_tax', wc_format_decimal( $amount ) ); } /** * Set taxes. * * This is an array of tax ID keys with total amount values. * * @param array $raw_tax_data Raw tax data. */ public function set_taxes( $raw_tax_data ) { $raw_tax_data = maybe_unserialize( $raw_tax_data ); $tax_data = array( 'total' => array(), ); if ( ! empty( $raw_tax_data['total'] ) ) { $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); } $this->set_prop( 'taxes', $tax_data ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $this->set_total_tax( array_sum( $tax_data['total'] ) ); } else { $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get fee amount. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_amount( $context = 'view' ) { return $this->get_prop( 'amount', $context ); } /** * Get order item name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { $name = $this->get_prop( 'name', $context ); if ( 'view' === $context ) { return $name ? $name : __( 'Fee', 'woocommerce' ); } else { return $name; } } /** * Get order item type. * * @return string */ public function get_type() { return 'fee'; } /** * Get tax class. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_class( $context = 'view' ) { return $this->get_prop( 'tax_class', $context ); } /** * Get tax status. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_status( $context = 'view' ) { return $this->get_prop( 'tax_status', $context ); } /** * Get total fee. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /** * Get fee taxes. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_taxes( $context = 'view' ) { return $this->get_prop( 'taxes', $context ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * OffsetGet for ArrayAccess/Backwards compatibility. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { if ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } return parent::offsetGet( $offset ); } /** * OffsetSet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' ); if ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } parent::offsetSet( $offset, $value ); } /** * OffsetExists for ArrayAccess * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } includes/integrations/maxmind-geolocation/views/html-admin-options.php 0000644 00000001507 15132754524 0022422 0 ustar 00 <?php /** * Admin View: Page - Admin options. * * @package WooCommerce\Integrations */ defined( 'ABSPATH' ) || exit; ?> <table class="form-table"> <tr valign="top"> <th scope="row" class="titledesc"> <label><?php esc_html_e( 'Database File Path', 'woocommerce' ); ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php esc_html_e( 'Database File Path', 'woocommerce' ); ?></span></legend> <input class="input-text regular-input" type="text" value="<?php echo esc_attr( $this->database_service->get_database_path() ); ?>" readonly> <p class="description"><?php esc_html_e( 'The location that the MaxMind database should be stored. By default, the integration will automatically save the database here.', 'woocommerce' ); ?></p> </fieldset> </td> </tr> </table> includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php 0000644 00000021451 15132754524 0025533 0 ustar 00 <?php /** * MaxMind Geolocation Integration * * @version 3.9.0 * @package WooCommerce\Integrations */ defined( 'ABSPATH' ) || exit; require_once __DIR__ . '/class-wc-integration-maxmind-database-service.php'; /** * WC Integration MaxMind Geolocation * * @since 3.9.0 */ class WC_Integration_MaxMind_Geolocation extends WC_Integration { /** * The service responsible for interacting with the MaxMind database. * * @var WC_Integration_MaxMind_Database_Service */ private $database_service; /** * Initialize the integration. */ public function __construct() { $this->id = 'maxmind_geolocation'; $this->method_title = __( 'MaxMind Geolocation', 'woocommerce' ); $this->method_description = __( 'An integration for utilizing MaxMind to do Geolocation lookups. Please note that this integration will only do country lookups.', 'woocommerce' ); /** * Supports overriding the database service to be used. * * @since 3.9.0 * @return mixed|null The geolocation database service. */ $this->database_service = apply_filters( 'woocommerce_maxmind_geolocation_database_service', null ); if ( null === $this->database_service ) { $this->database_service = new WC_Integration_MaxMind_Database_Service( $this->get_database_prefix() ); } $this->init_form_fields(); $this->init_settings(); // Bind to the save action for the settings. add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) ); // Trigger notice if license key is missing. add_action( 'update_option_woocommerce_default_customer_address', array( $this, 'display_missing_license_key_notice' ), 1000, 2 ); /** * Allows for the automatic database update to be disabled. * * @deprecated 3.9.0 * @return bool Whether or not the database should be updated periodically. */ $bind_updater = apply_filters_deprecated( 'woocommerce_geolocation_update_database_periodically', array( true ), '3.9.0', 'woocommerce_maxmind_geolocation_update_database_periodically' ); /** * Allows for the automatic database update to be disabled. * Note that MaxMind's TOS requires that the databases be updated or removed periodically. * * @since 3.9.0 * @param bool $bind_updater Whether or not the database should be updated periodically. */ $bind_updater = apply_filters( 'woocommerce_maxmind_geolocation_update_database_periodically', $bind_updater ); // Bind to the scheduled updater action. if ( $bind_updater ) { add_action( 'woocommerce_geoip_updater', array( $this, 'update_database' ) ); } // Bind to the geolocation filter for MaxMind database lookups. add_filter( 'woocommerce_get_geolocation', array( $this, 'get_geolocation' ), 10, 2 ); } /** * Override the normal options so we can print the database file path to the admin, */ public function admin_options() { parent::admin_options(); include dirname( __FILE__ ) . '/views/html-admin-options.php'; } /** * Initializes the settings fields. */ public function init_form_fields() { $this->form_fields = array( 'license_key' => array( 'title' => __( 'MaxMind License Key', 'woocommerce' ), 'type' => 'password', 'description' => sprintf( /* translators: %1$s: Documentation URL */ __( 'The key that will be used when dealing with MaxMind Geolocation services. You can read how to generate one in <a href="%1$s">MaxMind Geolocation Integration documentation</a>.', 'woocommerce' ), 'https://docs.woocommerce.com/document/maxmind-geolocation-integration/' ), 'desc_tip' => false, 'default' => '', ), ); } /** * Get database service. * * @return WC_Integration_MaxMind_Database_Service|null */ public function get_database_service() { return $this->database_service; } /** * Checks to make sure that the license key is valid. * * @param string $key The key of the field. * @param mixed $value The value of the field. * @return mixed * @throws Exception When the license key is invalid. */ public function validate_license_key_field( $key, $value ) { // Trim whitespaces and strip slashes. $value = $this->validate_password_field( $key, $value ); // Empty license keys have no need test downloading a database. if ( empty( $value ) ) { return $value; } // Check the license key by attempting to download the Geolocation database. $tmp_database_path = $this->database_service->download_database( $value ); if ( is_wp_error( $tmp_database_path ) ) { WC_Admin_Settings::add_error( $tmp_database_path->get_error_message() ); // Throw an exception to keep from changing this value. This will prevent // users from accidentally losing their license key, which cannot // be viewed again after generating. throw new Exception( $tmp_database_path->get_error_message() ); } // We may as well put this archive to good use, now that we've downloaded one. self::update_database( $tmp_database_path ); // Remove missing license key notice. $this->remove_missing_license_key_notice(); return $value; } /** * Updates the database used for geolocation queries. * * @param string|null $new_database_path The path to the new database file. Null will fetch a new archive. */ public function update_database( $new_database_path = null ) { // Allow us to easily interact with the filesystem. require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); global $wp_filesystem; // Remove any existing archives to comply with the MaxMind TOS. $target_database_path = $this->database_service->get_database_path(); // If there's no database path, we can't store the database. if ( empty( $target_database_path ) ) { return; } if ( $wp_filesystem->exists( $target_database_path ) ) { $wp_filesystem->delete( $target_database_path ); } if ( isset( $new_database_path ) ) { $tmp_database_path = $new_database_path; } else { // We can't download a database if there's no license key configured. $license_key = $this->get_option( 'license_key' ); if ( empty( $license_key ) ) { return; } $tmp_database_path = $this->database_service->download_database( $license_key ); if ( is_wp_error( $tmp_database_path ) ) { wc_get_logger()->notice( $tmp_database_path->get_error_message(), array( 'source' => 'maxmind-geolocation' ) ); return; } } // Move the new database into position. $wp_filesystem->move( $tmp_database_path, $target_database_path, true ); $wp_filesystem->delete( dirname( $tmp_database_path ) ); } /** * Performs a geolocation lookup against the MaxMind database for the given IP address. * * @param array $data Geolocation data. * @param string $ip_address The IP address to geolocate. * @return array Geolocation including country code, state, city and postcode based on an IP address. */ public function get_geolocation( $data, $ip_address ) { // WooCommerce look for headers first, and at this moment could be just enough. if ( ! empty( $data['country'] ) ) { return $data; } if ( empty( $ip_address ) ) { return $data; } $country_code = $this->database_service->get_iso_country_code_for_ip( $ip_address ); return array( 'country' => $country_code, 'state' => '', 'city' => '', 'postcode' => '', ); } /** * Fetches the prefix for the MaxMind database file. * * @return string */ private function get_database_prefix() { $prefix = $this->get_option( 'database_prefix' ); if ( empty( $prefix ) ) { $prefix = wp_generate_password( 32, false ); $this->update_option( 'database_prefix', $prefix ); } return $prefix; } /** * Add missing license key notice. */ private function add_missing_license_key_notice() { if ( ! class_exists( 'WC_Admin_Notices' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; } WC_Admin_Notices::add_notice( 'maxmind_license_key' ); } /** * Remove missing license key notice. */ private function remove_missing_license_key_notice() { if ( ! class_exists( 'WC_Admin_Notices' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; } WC_Admin_Notices::remove_notice( 'maxmind_license_key' ); } /** * Display notice if license key is missing. * * @param mixed $old_value Option old value. * @param mixed $new_value Current value. */ public function display_missing_license_key_notice( $old_value, $new_value ) { if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) { return; } if ( ! in_array( $new_value, array( 'geolocation', 'geolocation_ajax' ), true ) ) { $this->remove_missing_license_key_notice(); return; } $license_key = $this->get_option( 'license_key' ); if ( ! empty( $license_key ) ) { return; } $this->add_missing_license_key_notice(); } } includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php 0000644 00000011667 15132754524 0026442 0 ustar 00 <?php /** * The database service class file. * * @version 3.9.0 * @package WooCommerce\Integrations */ defined( 'ABSPATH' ) || exit; /** * The service class responsible for interacting with MaxMind databases. * * @since 3.9.0 */ class WC_Integration_MaxMind_Database_Service { /** * The name of the MaxMind database to utilize. */ const DATABASE = 'GeoLite2-Country'; /** * The extension for the MaxMind database. */ const DATABASE_EXTENSION = '.mmdb'; /** * A prefix for the MaxMind database filename. * * @var string */ private $database_prefix; /** * WC_Integration_MaxMind_Database_Service constructor. * * @param string|null $database_prefix A prefix for the MaxMind database filename. */ public function __construct( $database_prefix ) { $this->database_prefix = $database_prefix; } /** * Fetches the path that the database should be stored. * * @return string The local database path. */ public function get_database_path() { $uploads_dir = wp_upload_dir(); $database_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/'; if ( ! empty( $this->database_prefix ) ) { $database_path .= $this->database_prefix . '-'; } $database_path .= self::DATABASE . self::DATABASE_EXTENSION; /** * Filter the geolocation database storage path. * * @param string $database_path The path to the database. * @param int $version Deprecated since 3.4.0. * @deprecated 3.9.0 */ $database_path = apply_filters_deprecated( 'woocommerce_geolocation_local_database_path', array( $database_path, 2 ), '3.9.0', 'woocommerce_maxmind_geolocation_database_path' ); /** * Filter the geolocation database storage path. * * @since 3.9.0 * @param string $database_path The path to the database. */ return apply_filters( 'woocommerce_maxmind_geolocation_database_path', $database_path ); } /** * Fetches the database from the MaxMind service. * * @param string $license_key The license key to be used when downloading the database. * @return string|WP_Error The path to the database file or an error if invalid. */ public function download_database( $license_key ) { $download_uri = add_query_arg( array( 'edition_id' => self::DATABASE, 'license_key' => urlencode( wc_clean( $license_key ) ), 'suffix' => 'tar.gz', ), 'https://download.maxmind.com/app/geoip_download' ); // Needed for the download_url call right below. require_once ABSPATH . 'wp-admin/includes/file.php'; $tmp_archive_path = download_url( esc_url_raw( $download_uri ) ); if ( is_wp_error( $tmp_archive_path ) ) { // Transform the error into something more informative. $error_data = $tmp_archive_path->get_error_data(); if ( isset( $error_data['code'] ) ) { switch ( $error_data['code'] ) { case 401: return new WP_Error( 'woocommerce_maxmind_geolocation_database_license_key', __( 'The MaxMind license key is invalid. If you have recently created this key, you may need to wait for it to become active.', 'woocommerce' ) ); } } return new WP_Error( 'woocommerce_maxmind_geolocation_database_download', __( 'Failed to download the MaxMind database.', 'woocommerce' ) ); } // Extract the database from the archive. try { $file = new PharData( $tmp_archive_path ); $tmp_database_path = trailingslashit( dirname( $tmp_archive_path ) ) . trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION; $file->extractTo( dirname( $tmp_archive_path ), trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION, true ); } catch ( Exception $exception ) { return new WP_Error( 'woocommerce_maxmind_geolocation_database_archive', $exception->getMessage() ); } finally { // Remove the archive since we only care about a single file in it. unlink( $tmp_archive_path ); } return $tmp_database_path; } /** * Fetches the ISO country code associated with an IP address. * * @param string $ip_address The IP address to find the country code for. * @return string The country code for the IP address, or empty if not found. */ public function get_iso_country_code_for_ip( $ip_address ) { $country_code = ''; if ( ! class_exists( 'MaxMind\Db\Reader' ) ) { wc_get_logger()->notice( __( 'Missing MaxMind Reader library!', 'woocommerce' ), array( 'source' => 'maxmind-geolocation' ) ); return $country_code; } $database_path = $this->get_database_path(); if ( ! file_exists( $database_path ) ) { return $country_code; } try { $reader = new MaxMind\Db\Reader( $database_path ); $data = $reader->get( $ip_address ); if ( isset( $data['country']['iso_code'] ) ) { $country_code = $data['country']['iso_code']; } $reader->close(); } catch ( Exception $e ) { wc_get_logger()->notice( $e->getMessage(), array( 'source' => 'maxmind-geolocation' ) ); } return $country_code; } } includes/class-wc-product-factory.php 0000644 00000007124 15132754524 0013736 0 ustar 00 <?php /** * Product Factory * * The WooCommerce product factory creating the right product object. * * @package WooCommerce\Classes * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Product factory class. */ class WC_Product_Factory { /** * Get a product. * * @param mixed $product_id WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post. * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. * @return WC_Product|bool Product object or false if the product cannot be loaded. */ public function get_product( $product_id = false, $deprecated = array() ) { $product_id = $this->get_product_id( $product_id ); if ( ! $product_id ) { return false; } $product_type = $this->get_product_type( $product_id ); // Backwards compatibility. if ( ! empty( $deprecated ) ) { wc_deprecated_argument( 'args', '3.0', 'Passing args to the product factory is deprecated. If you need to force a type, construct the product class directly.' ); if ( isset( $deprecated['product_type'] ) ) { $product_type = $this->get_classname_from_product_type( $deprecated['product_type'] ); } } $classname = $this->get_product_classname( $product_id, $product_type ); try { return new $classname( $product_id, $deprecated ); } catch ( Exception $e ) { return false; } } /** * Gets a product classname and allows filtering. Returns WC_Product_Simple if the class does not exist. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $product_type Product type. * @return string */ public static function get_product_classname( $product_id, $product_type ) { $classname = apply_filters( 'woocommerce_product_class', self::get_classname_from_product_type( $product_type ), $product_type, 'variation' === $product_type ? 'product_variation' : 'product', $product_id ); if ( ! $classname || ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } return $classname; } /** * Get the product type for a product. * * @since 3.0.0 * @param int $product_id Product ID. * @return string|false */ public static function get_product_type( $product_id ) { // Allow the overriding of the lookup in this function. Return the product type here. $override = apply_filters( 'woocommerce_product_type_query', false, $product_id ); if ( ! $override ) { return WC_Data_Store::load( 'product' )->get_product_type( $product_id ); } else { return $override; } } /** * Create a WC coding standards compliant class name e.g. WC_Product_Type_Class instead of WC_Product_type-class. * * @param string $product_type Product type. * @return string|false */ public static function get_classname_from_product_type( $product_type ) { return $product_type ? 'WC_Product_' . implode( '_', array_map( 'ucfirst', explode( '-', $product_type ) ) ) : false; } /** * Get the product ID depending on what was passed. * * @since 3.0.0 * @param WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post. * @return int|bool false on failure */ private function get_product_id( $product ) { global $post; if ( false === $product && isset( $post, $post->ID ) && 'product' === get_post_type( $post->ID ) ) { return absint( $post->ID ); } elseif ( is_numeric( $product ) ) { return $product; } elseif ( $product instanceof WC_Product ) { return $product->get_id(); } elseif ( ! empty( $product->ID ) ) { return $product->ID; } else { return false; } } } includes/class-wc-structured-data.php 0000644 00000042505 15132754524 0013726 0 ustar 00 <?php /** * Structured data's handler and generator using JSON-LD format. * * @package WooCommerce\Classes * @since 3.0.0 * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Structured data class. */ class WC_Structured_Data { /** * Stores the structured data. * * @var array $_data Array of structured data. */ private $_data = array(); /** * Constructor. */ public function __construct() { // Generate structured data. add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 ); add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 ); add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 ); add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 ); // Output structured data. add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 ); add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 ); } /** * Sets data. * * @param array $data Structured data. * @param bool $reset Unset data (default: false). * @return bool */ public function set_data( $data, $reset = false ) { if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) { return false; } if ( $reset && isset( $this->_data ) ) { unset( $this->_data ); } $this->_data[] = $data; return true; } /** * Gets data. * * @return array */ public function get_data() { return $this->_data; } /** * Structures and returns data. * * List of types available by default for specific request: * * 'product', * 'review', * 'breadcrumblist', * 'website', * 'order', * * @param array $types Structured data types. * @return array */ public function get_structured_data( $types ) { $data = array(); // Put together the values of same type of structured data. foreach ( $this->get_data() as $value ) { $data[ strtolower( $value['@type'] ) ][] = $value; } // Wrap the multiple values of each type inside a graph... Then add context to each type. foreach ( $data as $type => $value ) { $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; } // If requested types, pick them up... Finally change the associative array to an indexed one. $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); if ( ! empty( $data ) ) { if ( 1 < count( $data ) ) { $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data ); } else { $data = $data[0]; } } return $data; } /** * Get data types for pages. * * @return array */ protected function get_data_type_for_page() { $types = array(); $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; $types[] = is_shop() && is_front_page() ? 'website' : ''; $types[] = is_product() ? 'review' : ''; $types[] = 'breadcrumblist'; $types[] = 'order'; return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); } /** * Makes sure email structured data only outputs on non-plain text versions. * * @param WP_Order $order Order data. * @param bool $sent_to_admin Send to admin (default: false). * @param bool $plain_text Plain text email (default: false). */ public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { if ( $plain_text ) { return; } echo '<div style="display: none; font-size: 0; max-height: 0; line-height: 0; padding: 0; mso-hide: all;">'; $this->output_structured_data(); echo '</div>'; } /** * Sanitizes, encodes and outputs structured data. * * Hooked into `wp_footer` action hook. * Hooked into `woocommerce_email_order_details` action hook. */ public function output_structured_data() { $types = $this->get_data_type_for_page(); $data = $this->get_structured_data( $types ); if ( $data ) { echo '<script type="application/ld+json">' . wc_esc_json( wp_json_encode( $data ), true ) . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /* |-------------------------------------------------------------------------- | Generators |-------------------------------------------------------------------------- | | Methods for generating specific structured data types: | | - Product | - Review | - BreadcrumbList | - WebSite | - Order | | The generated data is stored into `$this->_data`. | See the methods above for handling `$this->_data`. | */ /** * Generates Product structured data. * * Hooked into `woocommerce_single_product_summary` action hook. * * @param WC_Product $product Product data (default: null). */ public function generate_product_data( $product = null ) { if ( ! is_object( $product ) ) { global $product; } if ( ! is_a( $product, 'WC_Product' ) ) { return; } $shop_name = get_bloginfo( 'name' ); $shop_url = home_url(); $currency = get_woocommerce_currency(); $permalink = get_permalink( $product->get_id() ); $image = wp_get_attachment_url( $product->get_image_id() ); $markup = array( '@type' => 'Product', '@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist. 'name' => wp_kses_post( $product->get_name() ), 'url' => $permalink, 'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ), ); if ( $image ) { $markup['image'] = $image; } // Declare SKU or fallback to ID. if ( $product->get_sku() ) { $markup['sku'] = $product->get_sku(); } else { $markup['sku'] = $product->get_id(); } if ( '' !== $product->get_price() ) { // Assume prices will be valid until the end of next year, unless on sale and there is an end date. $price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS ); if ( $product->is_type( 'variable' ) ) { $lowest = $product->get_variation_price( 'min', false ); $highest = $product->get_variation_price( 'max', false ); if ( $lowest === $highest ) { $markup_offer = array( '@type' => 'Offer', 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), 'priceValidUntil' => $price_valid_until, 'priceSpecification' => array( 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), 'priceCurrency' => $currency, 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', ), ); } else { $markup_offer = array( '@type' => 'AggregateOffer', 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ), 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ), 'offerCount' => count( $product->get_children() ), ); } } else { if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) { $price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ); } $markup_offer = array( '@type' => 'Offer', 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), 'priceValidUntil' => $price_valid_until, 'priceSpecification' => array( 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), 'priceCurrency' => $currency, 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', ), ); } $markup_offer += array( 'priceCurrency' => $currency, 'availability' => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ), 'url' => $permalink, 'seller' => array( '@type' => 'Organization', 'name' => $shop_name, 'url' => $shop_url, ), ); $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); } if ( $product->get_rating_count() && wc_review_ratings_enabled() ) { $markup['aggregateRating'] = array( '@type' => 'AggregateRating', 'ratingValue' => $product->get_average_rating(), 'reviewCount' => $product->get_review_count(), ); // Markup 5 most recent rating/review. $comments = get_comments( array( 'number' => 5, 'post_id' => $product->get_id(), 'status' => 'approve', 'post_status' => 'publish', 'post_type' => 'product', 'parent' => 0, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => 'rating', 'type' => 'NUMERIC', 'compare' => '>', 'value' => 0, ), ), ) ); if ( $comments ) { $markup['review'] = array(); foreach ( $comments as $comment ) { $markup['review'][] = array( '@type' => 'Review', 'reviewRating' => array( '@type' => 'Rating', 'bestRating' => '5', 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'worstRating' => '1', ), 'author' => array( '@type' => 'Person', 'name' => get_comment_author( $comment ), ), 'reviewBody' => get_comment_text( $comment ), 'datePublished' => get_comment_date( 'c', $comment ), ); } } } // Check we have required data. if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) { return; } $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); } /** * Generates Review structured data. * * Hooked into `woocommerce_review_meta` action hook. * * @param WP_Comment $comment Comment data. */ public function generate_review_data( $comment ) { $markup = array(); $markup['@type'] = 'Review'; $markup['@id'] = get_comment_link( $comment->comment_ID ); $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); $markup['description'] = get_comment_text( $comment->comment_ID ); $markup['itemReviewed'] = array( '@type' => 'Product', 'name' => get_the_title( $comment->comment_post_ID ), ); // Skip replies unless they have a rating. $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); if ( $rating ) { $markup['reviewRating'] = array( '@type' => 'Rating', 'bestRating' => '5', 'ratingValue' => $rating, 'worstRating' => '1', ); } elseif ( $comment->comment_parent ) { return; } $markup['author'] = array( '@type' => 'Person', 'name' => get_comment_author( $comment->comment_ID ), ); $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); } /** * Generates BreadcrumbList structured data. * * Hooked into `woocommerce_breadcrumb` action hook. * * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. */ public function generate_breadcrumblist_data( $breadcrumbs ) { $crumbs = $breadcrumbs->get_breadcrumb(); if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { return; } $markup = array(); $markup['@type'] = 'BreadcrumbList'; $markup['itemListElement'] = array(); foreach ( $crumbs as $key => $crumb ) { $markup['itemListElement'][ $key ] = array( '@type' => 'ListItem', 'position' => $key + 1, 'item' => array( 'name' => $crumb[0], ), ); if ( ! empty( $crumb[1] ) ) { $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url ); } } $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); } /** * Generates WebSite structured data. * * Hooked into `woocommerce_before_main_content` action hook. */ public function generate_website_data() { $markup = array(); $markup['@type'] = 'WebSite'; $markup['name'] = get_bloginfo( 'name' ); $markup['url'] = home_url(); $markup['potentialAction'] = array( '@type' => 'SearchAction', 'target' => home_url( '?s={search_term_string}&post_type=product' ), 'query-input' => 'required name=search_term_string', ); $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); } /** * Generates Order structured data. * * Hooked into `woocommerce_email_order_details` action hook. * * @param WP_Order $order Order data. * @param bool $sent_to_admin Send to admin (default: false). * @param bool $plain_text Plain text email (default: false). */ public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { return; } $shop_name = get_bloginfo( 'name' ); $shop_url = home_url(); $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url(); $order_statuses = array( 'pending' => 'https://schema.org/OrderPaymentDue', 'processing' => 'https://schema.org/OrderProcessing', 'on-hold' => 'https://schema.org/OrderProblem', 'completed' => 'https://schema.org/OrderDelivered', 'cancelled' => 'https://schema.org/OrderCancelled', 'refunded' => 'https://schema.org/OrderReturned', 'failed' => 'https://schema.org/OrderProblem', ); $markup_offers = array(); foreach ( $order->get_items() as $item ) { if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { continue; } $product = $item->get_product(); $product_exists = is_object( $product ); $is_visible = $product_exists && $product->is_visible(); $markup_offers[] = array( '@type' => 'Offer', 'price' => $order->get_line_subtotal( $item ), 'priceCurrency' => $order->get_currency(), 'priceSpecification' => array( 'price' => $order->get_line_subtotal( $item ), 'priceCurrency' => $order->get_currency(), 'eligibleQuantity' => array( '@type' => 'QuantitativeValue', 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ), ), ), 'itemOffered' => array( '@type' => 'Product', 'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ), 'sku' => $product_exists ? $product->get_sku() : '', 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '', 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(), ), 'seller' => array( '@type' => 'Organization', 'name' => $shop_name, 'url' => $shop_url, ), ); } $markup = array(); $markup['@type'] = 'Order'; $markup['url'] = $order_url; $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; $markup['orderNumber'] = $order->get_order_number(); $markup['orderDate'] = $order->get_date_created()->format( 'c' ); $markup['acceptedOffer'] = $markup_offers; $markup['discount'] = $order->get_total_discount(); $markup['discountCurrency'] = $order->get_currency(); $markup['price'] = $order->get_total(); $markup['priceCurrency'] = $order->get_currency(); $markup['priceSpecification'] = array( 'price' => $order->get_total(), 'priceCurrency' => $order->get_currency(), 'valueAddedTaxIncluded' => 'true', ); $markup['billingAddress'] = array( '@type' => 'PostalAddress', 'name' => $order->get_formatted_billing_full_name(), 'streetAddress' => $order->get_billing_address_1(), 'postalCode' => $order->get_billing_postcode(), 'addressLocality' => $order->get_billing_city(), 'addressRegion' => $order->get_billing_state(), 'addressCountry' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'telephone' => $order->get_billing_phone(), ); $markup['customer'] = array( '@type' => 'Person', 'name' => $order->get_formatted_billing_full_name(), ); $markup['merchant'] = array( '@type' => 'Organization', 'name' => $shop_name, 'url' => $shop_url, ); $markup['potentialAction'] = array( '@type' => 'ViewAction', 'name' => 'View Order', 'url' => $order_url, 'target' => $order_url, ); $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); } } includes/class-wc-shipping-rate.php 0000644 00000012466 15132754524 0013370 0 ustar 00 <?php /** * WooCommerce Shipping Rate * * Simple Class for storing rates. * * @package WooCommerce\Classes\Shipping * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Shipping rate class. */ class WC_Shipping_Rate { /** * Stores data for this rate. * * @since 3.2.0 * @var array */ protected $data = array( 'id' => '', 'method_id' => '', 'instance_id' => 0, 'label' => '', 'cost' => 0, 'taxes' => array(), ); /** * Stores meta data for this rate. * * @since 2.6.0 * @var array */ protected $meta_data = array(); /** * Constructor. * * @param string $id Shipping rate ID. * @param string $label Shipping rate label. * @param integer $cost Cost. * @param array $taxes Taxes applied to shipping rate. * @param string $method_id Shipping method ID. * @param int $instance_id Shipping instance ID. */ public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '', $instance_id = 0 ) { $this->set_id( $id ); $this->set_label( $label ); $this->set_cost( $cost ); $this->set_taxes( $taxes ); $this->set_method_id( $method_id ); $this->set_instance_id( $instance_id ); } /** * Magic methods to support direct access to props. * * @since 3.2.0 * @param string $key Key. * @return bool */ public function __isset( $key ) { return isset( $this->data[ $key ] ); } /** * Magic methods to support direct access to props. * * @since 3.2.0 * @param string $key Key. * @return mixed */ public function __get( $key ) { if ( is_callable( array( $this, "get_{$key}" ) ) ) { return $this->{"get_{$key}"}(); } elseif ( isset( $this->data[ $key ] ) ) { return $this->data[ $key ]; } else { return ''; } } /** * Magic methods to support direct access to props. * * @since 3.2.0 * @param string $key Key. * @param mixed $value Value. */ public function __set( $key, $value ) { if ( is_callable( array( $this, "set_{$key}" ) ) ) { $this->{"set_{$key}"}( $value ); } else { $this->data[ $key ] = $value; } } /** * Set ID for the rate. This is usually a combination of the method and instance IDs. * * @since 3.2.0 * @param string $id Shipping rate ID. */ public function set_id( $id ) { $this->data['id'] = (string) $id; } /** * Set shipping method ID the rate belongs to. * * @since 3.2.0 * @param string $method_id Shipping method ID. */ public function set_method_id( $method_id ) { $this->data['method_id'] = (string) $method_id; } /** * Set instance ID the rate belongs to. * * @since 3.2.0 * @param int $instance_id Instance ID. */ public function set_instance_id( $instance_id ) { $this->data['instance_id'] = absint( $instance_id ); } /** * Set rate label. * * @since 3.2.0 * @param string $label Shipping rate label. */ public function set_label( $label ) { $this->data['label'] = (string) $label; } /** * Set rate cost. * * @todo 4.0 Prevent negative value being set. #19293 * @since 3.2.0 * @param string $cost Shipping rate cost. */ public function set_cost( $cost ) { $this->data['cost'] = $cost; } /** * Set rate taxes. * * @since 3.2.0 * @param array $taxes List of taxes applied to shipping rate. */ public function set_taxes( $taxes ) { $this->data['taxes'] = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array(); } /** * Get ID for the rate. This is usually a combination of the method and instance IDs. * * @since 3.2.0 * @return string */ public function get_id() { return apply_filters( 'woocommerce_shipping_rate_id', $this->data['id'], $this ); } /** * Get shipping method ID the rate belongs to. * * @since 3.2.0 * @return string */ public function get_method_id() { return apply_filters( 'woocommerce_shipping_rate_method_id', $this->data['method_id'], $this ); } /** * Get instance ID the rate belongs to. * * @since 3.2.0 * @return int */ public function get_instance_id() { return apply_filters( 'woocommerce_shipping_rate_instance_id', $this->data['instance_id'], $this ); } /** * Get rate label. * * @return string */ public function get_label() { return apply_filters( 'woocommerce_shipping_rate_label', $this->data['label'], $this ); } /** * Get rate cost. * * @since 3.2.0 * @return string */ public function get_cost() { return apply_filters( 'woocommerce_shipping_rate_cost', $this->data['cost'], $this ); } /** * Get rate taxes. * * @since 3.2.0 * @return array */ public function get_taxes() { return apply_filters( 'woocommerce_shipping_rate_taxes', $this->data['taxes'], $this ); } /** * Get shipping tax. * * @return array */ public function get_shipping_tax() { return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); } /** * Add some meta data for this rate. * * @since 2.6.0 * @param string $key Key. * @param string $value Value. */ public function add_meta_data( $key, $value ) { $this->meta_data[ wc_clean( $key ) ] = wc_clean( $value ); } /** * Get all meta data for this rate. * * @since 2.6.0 * @return array */ public function get_meta_data() { return $this->meta_data; } } includes/class-wc-privacy-erasers.php 0000644 00000033156 15132754524 0013734 0 ustar 00 <?php /** * Personal data erasers. * * @since 3.4.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Privacy_Erasers Class. */ class WC_Privacy_Erasers { /** * Finds and erases customer data by email address. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function customer_data_eraser( $email_address, $page ) { $response = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. if ( ! $user instanceof WP_User ) { return $response; } $customer = new WC_Customer( $user->ID ); if ( ! $customer ) { return $response; } $props_to_erase = apply_filters( 'woocommerce_privacy_erase_customer_personal_data_props', array( 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), 'billing_company' => __( 'Billing Company', 'woocommerce' ), 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), 'billing_city' => __( 'Billing City', 'woocommerce' ), 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), 'billing_state' => __( 'Billing State', 'woocommerce' ), 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), 'billing_phone' => __( 'Billing Phone Number', 'woocommerce' ), 'billing_email' => __( 'Email Address', 'woocommerce' ), 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), 'shipping_city' => __( 'Shipping City', 'woocommerce' ), 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), 'shipping_state' => __( 'Shipping State', 'woocommerce' ), 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), ), $customer ); foreach ( $props_to_erase as $prop => $label ) { $erased = false; if ( is_callable( array( $customer, 'get_' . $prop ) ) && is_callable( array( $customer, 'set_' . $prop ) ) ) { $value = $customer->{"get_$prop"}( 'edit' ); if ( $value ) { $customer->{"set_$prop"}( '' ); $erased = true; } } $erased = apply_filters( 'woocommerce_privacy_erase_customer_personal_data_prop', $erased, $prop, $customer ); if ( $erased ) { /* Translators: %s Prop name. */ $response['messages'][] = sprintf( __( 'Removed customer "%s"', 'woocommerce' ), $label ); $response['items_removed'] = true; } } $customer->save(); /** * Allow extensions to remove data for this customer and adjust the response. * * @since 3.4.0 * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done. * @param WC_Order $order A customer object. */ return apply_filters( 'woocommerce_privacy_erase_personal_data_customer', $response, $customer ); } /** * Finds and erases data which could be used to identify a person from WooCommerce data assocated with an email address. * * Orders are erased in blocks of 10 to avoid timeouts. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function order_data_eraser( $email_address, $page ) { $page = (int) $page; $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) ); $response = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); $order_query = array( 'limit' => 10, 'page' => $page, 'customer' => array( $email_address ), ); if ( $user instanceof WP_User ) { $order_query['customer'][] = (int) $user->ID; } $orders = wc_get_orders( $order_query ); if ( 0 < count( $orders ) ) { foreach ( $orders as $order ) { if ( apply_filters( 'woocommerce_privacy_erase_order_personal_data', $erasure_enabled, $order ) ) { self::remove_order_personal_data( $order ); /* Translators: %s Order number. */ $response['messages'][] = sprintf( __( 'Removed personal data from order %s.', 'woocommerce' ), $order->get_order_number() ); $response['items_removed'] = true; } else { /* Translators: %s Order number. */ $response['messages'][] = sprintf( __( 'Personal data within order %s has been retained.', 'woocommerce' ), $order->get_order_number() ); $response['items_retained'] = true; } } $response['done'] = 10 > count( $orders ); } else { $response['done'] = true; } return $response; } /** * Finds and removes customer download logs by email address. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function download_data_eraser( $email_address, $page ) { $page = (int) $page; $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_download_data', 'no' ) ); $response = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); $downloads_query = array( 'limit' => -1, 'page' => $page, 'return' => 'ids', ); if ( $user instanceof WP_User ) { $downloads_query['user_id'] = (int) $user->ID; } else { $downloads_query['user_email'] = $email_address; } $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); // Revoke download permissions. if ( apply_filters( 'woocommerce_privacy_erase_download_personal_data', $erasure_enabled, $email_address ) ) { if ( $user instanceof WP_User ) { $result = $customer_download_data_store->delete_by_user_id( (int) $user->ID ); } else { $result = $customer_download_data_store->delete_by_user_email( $email_address ); } if ( $result ) { $response['messages'][] = __( 'Removed access to downloadable files.', 'woocommerce' ); $response['items_removed'] = true; } } else { $response['messages'][] = __( 'Customer download permissions have been retained.', 'woocommerce' ); $response['items_retained'] = true; } return $response; } /** * Remove personal data specific to WooCommerce from an order object. * * Note; this will hinder order processing for obvious reasons! * * @param WC_Order $order Order object. */ public static function remove_order_personal_data( $order ) { $anonymized_data = array(); /** * Allow extensions to remove their own personal data for this order first, so order data is still available. * * @since 3.4.0 * @param WC_Order $order A customer object. */ do_action( 'woocommerce_privacy_before_remove_order_personal_data', $order ); /** * Expose props and data types we'll be anonymizing. * * @since 3.4.0 * @param array $props Keys are the prop names, values are the data type we'll be passing to wp_privacy_anonymize_data(). * @param WC_Order $order A customer object. */ $props_to_remove = apply_filters( 'woocommerce_privacy_remove_order_personal_data_props', array( 'customer_ip_address' => 'ip', 'customer_user_agent' => 'text', 'billing_first_name' => 'text', 'billing_last_name' => 'text', 'billing_company' => 'text', 'billing_address_1' => 'text', 'billing_address_2' => 'text', 'billing_city' => 'text', 'billing_postcode' => 'text', 'billing_state' => 'address_state', 'billing_country' => 'address_country', 'billing_phone' => 'phone', 'billing_email' => 'email', 'shipping_first_name' => 'text', 'shipping_last_name' => 'text', 'shipping_company' => 'text', 'shipping_address_1' => 'text', 'shipping_address_2' => 'text', 'shipping_city' => 'text', 'shipping_postcode' => 'text', 'shipping_state' => 'address_state', 'shipping_country' => 'address_country', 'shipping_phone' => 'phone', 'customer_id' => 'numeric_id', 'transaction_id' => 'numeric_id', ), $order ); if ( ! empty( $props_to_remove ) && is_array( $props_to_remove ) ) { foreach ( $props_to_remove as $prop => $data_type ) { // Get the current value in edit context. $value = $order->{"get_$prop"}( 'edit' ); // If the value is empty, it does not need to be anonymized. if ( empty( $value ) || empty( $data_type ) ) { continue; } $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; /** * Expose a way to control the anonymized value of a prop via 3rd party code. * * @since 3.4.0 * @param string $anon_value Value of this prop after anonymization. * @param string $prop Name of the prop being removed. * @param string $value Current value of the data. * @param string $data_type Type of data. * @param WC_Order $order An order object. */ $anonymized_data[ $prop ] = apply_filters( 'woocommerce_privacy_remove_order_personal_data_prop_value', $anon_value, $prop, $value, $data_type, $order ); } } // Set all new props and persist the new data to the database. $order->set_props( $anonymized_data ); // Remove meta data. $meta_to_remove = apply_filters( 'woocommerce_privacy_remove_order_personal_data_meta', array( 'Payer first name' => 'text', 'Payer last name' => 'text', 'Payer PayPal address' => 'email', 'Transaction ID' => 'numeric_id', ) ); if ( ! empty( $meta_to_remove ) && is_array( $meta_to_remove ) ) { foreach ( $meta_to_remove as $meta_key => $data_type ) { $value = $order->get_meta( $meta_key ); // If the value is empty, it does not need to be anonymized. if ( empty( $value ) || empty( $data_type ) ) { continue; } $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; /** * Expose a way to control the anonymized value of a value via 3rd party code. * * @since 3.4.0 * @param string $anon_value Value of this data after anonymization. * @param string $prop meta_key key being removed. * @param string $value Current value of the data. * @param string $data_type Type of data. * @param WC_Order $order An order object. */ $anon_value = apply_filters( 'woocommerce_privacy_remove_order_personal_data_meta_value', $anon_value, $meta_key, $value, $data_type, $order ); if ( $anon_value ) { $order->update_meta_data( $meta_key, $anon_value ); } else { $order->delete_meta_data( $meta_key ); } } } $order->update_meta_data( '_anonymized', 'yes' ); $order->save(); // Delete order notes which can contain PII. $notes = wc_get_order_notes( array( 'order_id' => $order->get_id(), ) ); foreach ( $notes as $note ) { wc_delete_order_note( $note->id ); } // Add note that this event occured. $order->add_order_note( __( 'Personal data removed.', 'woocommerce' ) ); /** * Allow extensions to remove their own personal data for this order. * * @since 3.4.0 * @param WC_Order $order A customer object. */ do_action( 'woocommerce_privacy_remove_order_personal_data', $order ); } /** * Finds and erases customer tokens by email address. * * @since 3.4.0 * @param string $email_address The user email address. * @param int $page Page. * @return array An array of personal data in name value pairs */ public static function customer_tokens_eraser( $email_address, $page ) { $response = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. if ( ! $user instanceof WP_User ) { return $response; } $tokens = WC_Payment_Tokens::get_tokens( array( 'user_id' => $user->ID, ) ); if ( empty( $tokens ) ) { return $response; } foreach ( $tokens as $token ) { WC_Payment_Tokens::delete( $token->get_id() ); /* Translators: %s Prop name. */ $response['messages'][] = sprintf( __( 'Removed payment token "%d"', 'woocommerce' ), $token->get_id() ); $response['items_removed'] = true; } /** * Allow extensions to remove data for tokens and adjust the response. * * @since 3.4.0 * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done. * @param array $tokens Array of tokens. */ return apply_filters( 'woocommerce_privacy_erase_personal_data_tokens', $response, $tokens ); } } includes/class-wc-product-attribute.php 0000644 00000015575 15132754524 0014303 0 ustar 00 <?php /** * Represents a product attribute * * Attributes can be global (taxonomy based) or local to the product itself. * Uses ArrayAccess to be BW compatible with previous ways of reading attributes. * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Product attribute class. */ class WC_Product_Attribute implements ArrayAccess { /** * Data array. * * @since 3.0.0 * @var array */ protected $data = array( 'id' => 0, 'name' => '', 'options' => array(), 'position' => 0, 'visible' => false, 'variation' => false, ); /** * Return if this attribute is a taxonomy. * * @return boolean */ public function is_taxonomy() { return 0 < $this->get_id(); } /** * Get taxonomy name if applicable. * * @return string */ public function get_taxonomy() { return $this->is_taxonomy() ? $this->get_name() : ''; } /** * Get taxonomy object. * * @return array|null */ public function get_taxonomy_object() { global $wc_product_attributes; return $this->is_taxonomy() ? $wc_product_attributes[ $this->get_name() ] : null; } /** * Gets terms from the stored options. * * @return array|null */ public function get_terms() { if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { return null; } $terms = array(); foreach ( $this->get_options() as $option ) { if ( is_int( $option ) ) { $term = get_term_by( 'id', $option, $this->get_name() ); } else { // Term names get escaped in WP. See sanitize_term_field. $term = get_term_by( 'name', $option, $this->get_name() ); if ( ! $term || is_wp_error( $term ) ) { $new_term = wp_insert_term( $option, $this->get_name() ); $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); } } if ( $term && ! is_wp_error( $term ) ) { $terms[] = $term; } } return $terms; } /** * Gets slugs from the stored options, or just the string if text based. * * @return array */ public function get_slugs() { if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { return $this->get_options(); } $terms = array(); foreach ( $this->get_options() as $option ) { if ( is_int( $option ) ) { $term = get_term_by( 'id', $option, $this->get_name() ); } else { $term = get_term_by( 'name', $option, $this->get_name() ); if ( ! $term || is_wp_error( $term ) ) { $new_term = wp_insert_term( $option, $this->get_name() ); $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); } } if ( $term && ! is_wp_error( $term ) ) { $terms[] = $term->slug; } } return $terms; } /** * Returns all data for this object. * * @return array */ public function get_data() { return array_merge( $this->data, array( 'is_visible' => $this->get_visible() ? 1 : 0, 'is_variation' => $this->get_variation() ? 1 : 0, 'is_taxonomy' => $this->is_taxonomy() ? 1 : 0, 'value' => $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ), ) ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set ID (this is the attribute ID). * * @param int $value Attribute ID. */ public function set_id( $value ) { $this->data['id'] = absint( $value ); } /** * Set name (this is the attribute name or taxonomy). * * @param int $value Attribute name. */ public function set_name( $value ) { $this->data['name'] = $value; } /** * Set options. * * @param array $value Attribute options. */ public function set_options( $value ) { $this->data['options'] = $value; } /** * Set position. * * @param int $value Attribute position. */ public function set_position( $value ) { $this->data['position'] = absint( $value ); } /** * Set if visible. * * @param bool $value If is visible on Product's additional info tab. */ public function set_visible( $value ) { $this->data['visible'] = wc_string_to_bool( $value ); } /** * Set if variation. * * @param bool $value If is used for variations. */ public function set_variation( $value ) { $this->data['variation'] = wc_string_to_bool( $value ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get the ID. * * @return int */ public function get_id() { return $this->data['id']; } /** * Get name. * * @return string */ public function get_name() { return $this->data['name']; } /** * Get options. * * @return array */ public function get_options() { return $this->data['options']; } /** * Get position. * * @return int */ public function get_position() { return $this->data['position']; } /** * Get if visible. * * @return bool */ public function get_visible() { return $this->data['visible']; } /** * Get if variation. * * @return bool */ public function get_variation() { return $this->data['variation']; } /* |-------------------------------------------------------------------------- | ArrayAccess/Backwards compatibility. |-------------------------------------------------------------------------- */ /** * OffsetGet. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { switch ( $offset ) { case 'is_variation': return $this->get_variation() ? 1 : 0; case 'is_visible': return $this->get_visible() ? 1 : 0; case 'is_taxonomy': return $this->is_taxonomy() ? 1 : 0; case 'value': return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ); default: if ( is_callable( array( $this, "get_$offset" ) ) ) { return $this->{"get_$offset"}(); } break; } return ''; } /** * OffsetSet. * * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { switch ( $offset ) { case 'is_variation': $this->set_variation( $value ); break; case 'is_visible': $this->set_visible( $value ); break; case 'value': $this->set_options( $value ); break; default: if ( is_callable( array( $this, "set_$offset" ) ) ) { return $this->{"set_$offset"}( $value ); } break; } } /** * OffsetUnset. * * @param string $offset Offset. */ public function offsetUnset( $offset ) {} /** * OffsetExists. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true ); } } includes/wc-attribute-functions.php 0000644 00000051130 15132754524 0013513 0 ustar 00 <?php /** * WooCommerce Attribute Functions * * @package WooCommerce\Functions * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; /** * Gets text attributes from a string. * * @since 2.4 * @param string $raw_attributes Raw attributes. * @return array */ function wc_get_text_attributes( $raw_attributes ) { return array_filter( array_map( 'trim', explode( WC_DELIMITER, html_entity_decode( $raw_attributes, ENT_QUOTES, get_bloginfo( 'charset' ) ) ) ), 'wc_get_text_attributes_filter_callback' ); } /** * See if an attribute is actually valid. * * @since 3.0.0 * @param string $value Value. * @return bool */ function wc_get_text_attributes_filter_callback( $value ) { return '' !== $value; } /** * Implode an array of attributes using WC_DELIMITER. * * @since 3.0.0 * @param array $attributes Attributes list. * @return string */ function wc_implode_text_attributes( $attributes ) { return implode( ' ' . WC_DELIMITER . ' ', $attributes ); } /** * Get attribute taxonomies. * * @return array of objects, @since 3.6.0 these are also indexed by ID. */ function wc_get_attribute_taxonomies() { $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); $cache_key = $prefix . 'attributes'; $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); if ( false !== $cache_value ) { return $cache_value; } $raw_attribute_taxonomies = get_transient( 'wc_attribute_taxonomies' ); if ( false === $raw_attribute_taxonomies ) { global $wpdb; $raw_attribute_taxonomies = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC;" ); set_transient( 'wc_attribute_taxonomies', $raw_attribute_taxonomies ); } /** * Filter attribute taxonomies. * * @param array $attribute_taxonomies Results of the DB query. Each taxonomy is an object. */ $raw_attribute_taxonomies = (array) array_filter( apply_filters( 'woocommerce_attribute_taxonomies', $raw_attribute_taxonomies ) ); // Index by ID for easer lookups. $attribute_taxonomies = array(); foreach ( $raw_attribute_taxonomies as $result ) { $attribute_taxonomies[ 'id:' . $result->attribute_id ] = $result; } wp_cache_set( $cache_key, $attribute_taxonomies, 'woocommerce-attributes' ); return $attribute_taxonomies; } /** * Get (cached) attribute taxonomy ID and name pairs. * * @since 3.6.0 * @return array */ function wc_get_attribute_taxonomy_ids() { $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); $cache_key = $prefix . 'ids'; $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); if ( $cache_value ) { return $cache_value; } $taxonomy_ids = array_map( 'absint', wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id', 'attribute_name' ) ); wp_cache_set( $cache_key, $taxonomy_ids, 'woocommerce-attributes' ); return $taxonomy_ids; } /** * Get (cached) attribute taxonomy label and name pairs. * * @since 3.6.0 * @return array */ function wc_get_attribute_taxonomy_labels() { $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); $cache_key = $prefix . 'labels'; $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); if ( $cache_value ) { return $cache_value; } $taxonomy_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); wp_cache_set( $cache_key, $taxonomy_labels, 'woocommerce-attributes' ); return $taxonomy_labels; } /** * Get a product attribute name. * * @param string $attribute_name Attribute name. * @return string */ function wc_attribute_taxonomy_name( $attribute_name ) { return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : ''; } /** * Get the attribute name used when storing values in post meta. * * @since 2.6.0 * @param string $attribute_name Attribute name. * @return string */ function wc_variation_attribute_name( $attribute_name ) { return 'attribute_' . sanitize_title( $attribute_name ); } /** * Get a product attribute name by ID. * * @since 2.4.0 * @param int $attribute_id Attribute ID. * @return string Return an empty string if attribute doesn't exist. */ function wc_attribute_taxonomy_name_by_id( $attribute_id ) { $taxonomy_ids = wc_get_attribute_taxonomy_ids(); $attribute_name = (string) array_search( $attribute_id, $taxonomy_ids, true ); return wc_attribute_taxonomy_name( $attribute_name ); } /** * Get a product attribute ID by name. * * @since 2.6.0 * @param string $name Attribute name. * @return int */ function wc_attribute_taxonomy_id_by_name( $name ) { $name = wc_attribute_taxonomy_slug( $name ); $taxonomy_ids = wc_get_attribute_taxonomy_ids(); return isset( $taxonomy_ids[ $name ] ) ? $taxonomy_ids[ $name ] : 0; } /** * Get a product attributes label. * * @param string $name Attribute name. * @param WC_Product $product Product data. * @return string */ function wc_attribute_label( $name, $product = '' ) { if ( taxonomy_is_product_attribute( $name ) ) { $slug = wc_attribute_taxonomy_slug( $name ); $all_labels = wc_get_attribute_taxonomy_labels(); $label = isset( $all_labels[ $slug ] ) ? $all_labels[ $slug ] : $slug; } elseif ( $product ) { if ( $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); } $attributes = array(); if ( false !== $product ) { $attributes = $product->get_attributes(); } // Attempt to get label from product, as entered by the user. if ( $attributes && isset( $attributes[ sanitize_title( $name ) ] ) ) { $label = $attributes[ sanitize_title( $name ) ]->get_name(); } else { $label = $name; } } else { $label = $name; } return apply_filters( 'woocommerce_attribute_label', $label, $name, $product ); } /** * Get a product attributes orderby setting. * * @param string $name Attribute name. * @return string */ function wc_attribute_orderby( $name ) { $name = wc_attribute_taxonomy_slug( $name ); $id = wc_attribute_taxonomy_id_by_name( $name ); $taxonomies = wc_get_attribute_taxonomies(); return apply_filters( 'woocommerce_attribute_orderby', isset( $taxonomies[ 'id:' . $id ] ) ? $taxonomies[ 'id:' . $id ]->attribute_orderby : 'menu_order', $name ); } /** * Get an array of product attribute taxonomies. * * @return array */ function wc_get_attribute_taxonomy_names() { $taxonomy_names = array(); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $tax ) { $taxonomy_names[] = wc_attribute_taxonomy_name( $tax->attribute_name ); } } return $taxonomy_names; } /** * Get attribute types. * * @since 2.4.0 * @return array */ function wc_get_attribute_types() { return (array) apply_filters( 'product_attributes_type_selector', array( 'select' => __( 'Select', 'woocommerce' ), ) ); } /** * Check if there are custom attribute types. * * @since 3.3.2 * @return bool True if there are custom types, otherwise false. */ function wc_has_custom_attribute_types() { $types = wc_get_attribute_types(); return 1 < count( $types ) || ! array_key_exists( 'select', $types ); } /** * Get attribute type label. * * @since 3.0.0 * @param string $type Attribute type slug. * @return string */ function wc_get_attribute_type_label( $type ) { $types = wc_get_attribute_types(); return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' ); } /** * Check if attribute name is reserved. * https://codex.wordpress.org/Function_Reference/register_taxonomy#Reserved_Terms. * * @since 2.4.0 * @param string $attribute_name Attribute name. * @return bool */ function wc_check_if_attribute_name_is_reserved( $attribute_name ) { // Forbidden attribute names. $reserved_terms = array( 'attachment', 'attachment_id', 'author', 'author_name', 'calendar', 'cat', 'category', 'category__and', 'category__in', 'category__not_in', 'category_name', 'comments_per_page', 'comments_popup', 'cpage', 'day', 'debug', 'error', 'exact', 'feed', 'hour', 'link_category', 'm', 'minute', 'monthnum', 'more', 'name', 'nav_menu', 'nopaging', 'offset', 'order', 'orderby', 'p', 'page', 'page_id', 'paged', 'pagename', 'pb', 'perm', 'post', 'post__in', 'post__not_in', 'post_format', 'post_mime_type', 'post_status', 'post_tag', 'post_type', 'posts', 'posts_per_archive_page', 'posts_per_page', 'preview', 'robots', 's', 'search', 'second', 'sentence', 'showposts', 'static', 'subpost', 'subpost_id', 'tag', 'tag__and', 'tag__in', 'tag__not_in', 'tag_id', 'tag_slug__and', 'tag_slug__in', 'taxonomy', 'tb', 'term', 'type', 'w', 'withcomments', 'withoutcomments', 'year', ); return in_array( $attribute_name, $reserved_terms, true ); } /** * Callback for array filter to get visible only. * * @since 3.0.0 * @param WC_Product_Attribute $attribute Attribute data. * @return bool */ function wc_attributes_array_filter_visible( $attribute ) { return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_visible() && ( ! $attribute->is_taxonomy() || taxonomy_exists( $attribute->get_name() ) ); } /** * Callback for array filter to get variation attributes only. * * @since 3.0.0 * @param WC_Product_Attribute $attribute Attribute data. * @return bool */ function wc_attributes_array_filter_variation( $attribute ) { return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_variation(); } /** * Check if an attribute is included in the attributes area of a variation name. * * @since 3.0.2 * @param string $attribute Attribute value to check for. * @param string $name Product name to check in. * @return bool */ function wc_is_attribute_in_product_name( $attribute, $name ) { $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) ); return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name ); } /** * Callback for array filter to get default attributes. Will allow for '0' string values, but regard all other * class PHP FALSE equivalents normally. * * @since 3.1.0 * @param mixed $attribute Attribute being considered for exclusion from parent array. * @return bool */ function wc_array_filter_default_attributes( $attribute ) { return is_scalar( $attribute ) && ( ! empty( $attribute ) || '0' === $attribute ); } /** * Get attribute data by ID. * * @since 3.2.0 * @param int $id Attribute ID. * @return stdClass|null */ function wc_get_attribute( $id ) { $attributes = wc_get_attribute_taxonomies(); if ( ! isset( $attributes[ 'id:' . $id ] ) ) { return null; } $data = $attributes[ 'id:' . $id ]; $attribute = new stdClass(); $attribute->id = (int) $data->attribute_id; $attribute->name = $data->attribute_label; $attribute->slug = wc_attribute_taxonomy_name( $data->attribute_name ); $attribute->type = $data->attribute_type; $attribute->order_by = $data->attribute_orderby; $attribute->has_archives = (bool) $data->attribute_public; return $attribute; } /** * Create attribute. * * @since 3.2.0 * @param array $args Attribute arguments { * Array of attribute parameters. * * @type int $id Unique identifier, used to update an attribute. * @type string $name Attribute name. Always required. * @type string $slug Attribute alphanumeric identifier. * @type string $type Type of attribute. * Core by default accepts: 'select' and 'text'. * Default to 'select'. * @type string $order_by Sort order. * Accepts: 'menu_order', 'name', 'name_num' and 'id'. * Default to 'menu_order'. * @type bool $has_archives Enable or disable attribute archives. False by default. * } * @return int|WP_Error */ function wc_create_attribute( $args ) { global $wpdb; $args = wp_unslash( $args ); $id = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0; $format = array( '%s', '%s', '%s', '%s', '%d' ); // Name is required. if ( empty( $args['name'] ) ) { return new WP_Error( 'missing_attribute_name', __( 'Please, provide an attribute name.', 'woocommerce' ), array( 'status' => 400 ) ); } // Set the attribute slug. if ( empty( $args['slug'] ) ) { $slug = wc_sanitize_taxonomy_name( $args['name'] ); } else { $slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) ); } // Validate slug. if ( strlen( $slug ) >= 28 ) { /* translators: %s: attribute slug */ return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { /* translators: %s: attribute slug */ return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) || ( isset( $args['old_slug'] ) && $args['old_slug'] !== $slug && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) ) { /* translators: %s: attribute slug */ return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } // Validate type. if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) { $args['type'] = 'select'; } // Validate order by. if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) { $args['order_by'] = 'menu_order'; } $data = array( 'attribute_label' => $args['name'], 'attribute_name' => $slug, 'attribute_type' => $args['type'], 'attribute_orderby' => $args['order_by'], 'attribute_public' => isset( $args['has_archives'] ) ? (int) $args['has_archives'] : 0, ); // Create or update. if ( 0 === $id ) { $results = $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $data, $format ); if ( is_wp_error( $results ) ) { return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) ); } $id = $wpdb->insert_id; /** * Attribute added. * * @param int $id Added attribute ID. * @param array $data Attribute data. */ do_action( 'woocommerce_attribute_added', $id, $data ); } else { $results = $wpdb->update( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $data, array( 'attribute_id' => $id ), $format, array( '%d' ) ); if ( false === $results ) { return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); } // Set old slug to check for database changes. $old_slug = ! empty( $args['old_slug'] ) ? wc_sanitize_taxonomy_name( $args['old_slug'] ) : $slug; /** * Attribute updated. * * @param int $id Added attribute ID. * @param array $data Attribute data. * @param string $old_slug Attribute old name. */ do_action( 'woocommerce_attribute_updated', $id, $data, $old_slug ); if ( $old_slug !== $slug ) { // Update taxonomies in the wp term taxonomy table. $wpdb->update( $wpdb->term_taxonomy, array( 'taxonomy' => wc_attribute_taxonomy_name( $data['attribute_name'] ) ), array( 'taxonomy' => 'pa_' . $old_slug ) ); // Update taxonomy ordering term meta. $wpdb->update( $wpdb->termmeta, array( 'meta_key' => 'order_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. array( 'meta_key' => 'order_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. ); // Update product attributes which use this taxonomy. $old_taxonomy_name = 'pa_' . $old_slug; $new_taxonomy_name = 'pa_' . $data['attribute_name']; $old_attribute_key = sanitize_title( $old_taxonomy_name ); // @see WC_Product::set_attributes(). $new_attribute_key = sanitize_title( $new_taxonomy_name ); // @see WC_Product::set_attributes(). $metadatas = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND meta_value LIKE %s", '%' . $wpdb->esc_like( $old_taxonomy_name ) . '%' ), ARRAY_A ); foreach ( $metadatas as $metadata ) { $product_id = $metadata['post_id']; $unserialized_data = maybe_unserialize( $metadata['meta_value'] ); if ( ! $unserialized_data || ! is_array( $unserialized_data ) || ! isset( $unserialized_data[ $old_attribute_key ] ) ) { continue; } $unserialized_data[ $new_attribute_key ] = $unserialized_data[ $old_attribute_key ]; unset( $unserialized_data[ $old_attribute_key ] ); $unserialized_data[ $new_attribute_key ]['name'] = $new_taxonomy_name; update_post_meta( $product_id, '_product_attributes', wp_slash( $unserialized_data ) ); } // Update variations which use this taxonomy. $wpdb->update( $wpdb->postmeta, array( 'meta_key' => 'attribute_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. array( 'meta_key' => 'attribute_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. ); } } // Clear cache and flush rewrite rules. wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return $id; } /** * Update an attribute. * * For available args see wc_create_attribute(). * * @since 3.2.0 * @param int $id Attribute ID. * @param array $args Attribute arguments. * @return int|WP_Error */ function wc_update_attribute( $id, $args ) { global $wpdb; $attribute = wc_get_attribute( $id ); $args['id'] = $attribute ? $attribute->id : 0; if ( $args['id'] && empty( $args['name'] ) ) { $args['name'] = $attribute->name; } $args['old_slug'] = $wpdb->get_var( $wpdb->prepare( " SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $args['id'] ) ); return wc_create_attribute( $args ); } /** * Delete attribute by ID. * * @since 3.2.0 * @param int $id Attribute ID. * @return bool */ function wc_delete_attribute( $id ) { global $wpdb; $name = $wpdb->get_var( $wpdb->prepare( " SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); $taxonomy = wc_attribute_taxonomy_name( $name ); /** * Before deleting an attribute. * * @param int $id Attribute ID. * @param string $name Attribute name. * @param string $taxonomy Attribute taxonomy name. */ do_action( 'woocommerce_before_attribute_delete', $id, $name, $taxonomy ); if ( $name && $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $id ) ) ) { if ( taxonomy_exists( $taxonomy ) ) { $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); foreach ( $terms as $term ) { wp_delete_term( $term->term_id, $taxonomy ); } } /** * After deleting an attribute. * * @param int $id Attribute ID. * @param string $name Attribute name. * @param string $taxonomy Attribute taxonomy name. */ do_action( 'woocommerce_attribute_deleted', $id, $name, $taxonomy ); wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return true; } return false; } /** * Get an unprefixed product attribute name. * * @since 3.6.0 * * @param string $attribute_name Attribute name. * @return string */ function wc_attribute_taxonomy_slug( $attribute_name ) { $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); $cache_key = $prefix . 'slug-' . $attribute_name; $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); if ( $cache_value ) { return $cache_value; } $attribute_name = wc_sanitize_taxonomy_name( $attribute_name ); $attribute_slug = 0 === strpos( $attribute_name, 'pa_' ) ? substr( $attribute_name, 3 ) : $attribute_name; wp_cache_set( $cache_key, $attribute_slug, 'woocommerce-attributes' ); return $attribute_slug; } includes/wc-template-functions.php 0000644 00000345431 15132754524 0013335 0 ustar 00 <?php /** * WooCommerce Template * * Functions for the templating system. * * @package WooCommerce\Functions * @version 2.5.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Handle redirects before content is output - hooked into template_redirect so is_page works. */ function wc_template_redirect() { global $wp_query, $wp; // phpcs:disable WordPress.Security.NonceVerification.Recommended // When default permalinks are enabled, redirect shop page to post type archive url. if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) { wp_safe_redirect( get_post_type_archive_link( 'product' ) ); exit; } // phpcs:enable WordPress.Security.NonceVerification.Recommended // When on the checkout with an empty cart, redirect to cart page. if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { wc_add_notice( __( 'Checkout is not available whilst your cart is empty.', 'woocommerce' ), 'notice' ); wp_safe_redirect( wc_get_cart_url() ); exit; } // Logout. if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { wp_safe_redirect( str_replace( '&', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) ); exit; } // Redirect to the correct logout endpoint. if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) { wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) ); exit; } // Trigger 404 if trying to access an endpoint on wrong page. if ( is_wc_endpoint_url() && ! is_account_page() && ! is_checkout() && apply_filters( 'woocommerce_account_endpoint_page_not_found', true ) ) { $wp_query->set_404(); status_header( 404 ); include get_query_template( '404' ); exit; } // Redirect to the product page if we have a single product. if ( is_search() && is_post_type_archive( 'product' ) && apply_filters( 'woocommerce_redirect_single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) { $product = wc_get_product( $wp_query->post ); if ( $product && $product->is_visible() ) { wp_safe_redirect( get_permalink( $product->get_id() ), 302 ); exit; } } // Ensure gateways and shipping methods are loaded early. if ( is_add_payment_method_page() || is_checkout() ) { // Buffer the checkout page. ob_start(); // Ensure gateways and shipping methods are loaded early. WC()->payment_gateways(); WC()->shipping(); } } add_action( 'template_redirect', 'wc_template_redirect' ); /** * When loading sensitive checkout or account pages, send a HTTP header to limit rendering of pages to same origin iframes for security reasons. * * Can be disabled with: remove_action( 'template_redirect', 'wc_send_frame_options_header' ); * * @since 2.3.10 */ function wc_send_frame_options_header() { if ( ( is_checkout() || is_account_page() ) && ! is_customize_preview() ) { send_frame_options_header(); } } add_action( 'template_redirect', 'wc_send_frame_options_header' ); /** * No index our endpoints. * Prevent indexing pages like order-received. * * @since 2.5.3 */ function wc_prevent_endpoint_indexing() { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { @header( 'X-Robots-Tag: noindex' ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged } add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' ); /** * Remove adjacent_posts_rel_link_wp_head - pointless for products. * * @since 3.0.0 */ function wc_prevent_adjacent_posts_rel_link_wp_head() { if ( is_singular( 'product' ) ) { remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 ); } } add_action( 'template_redirect', 'wc_prevent_adjacent_posts_rel_link_wp_head' ); /** * Show the gallery if JS is disabled. * * @since 3.0.6 */ function wc_gallery_noscript() { ?> <noscript><style>.woocommerce-product-gallery{ opacity: 1 !important; }</style></noscript> <?php } add_action( 'wp_head', 'wc_gallery_noscript' ); /** * When the_post is called, put product data into a global. * * @param mixed $post Post Object. * @return WC_Product */ function wc_setup_product_data( $post ) { unset( $GLOBALS['product'] ); if ( is_int( $post ) ) { $post = get_post( $post ); } if ( empty( $post->post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { return; } $GLOBALS['product'] = wc_get_product( $post ); return $GLOBALS['product']; } add_action( 'the_post', 'wc_setup_product_data' ); /** * Sets up the woocommerce_loop global from the passed args or from the main query. * * @since 3.3.0 * @param array $args Args to pass into the global. */ function wc_setup_loop( $args = array() ) { $default_args = array( 'loop' => 0, 'columns' => wc_get_default_products_per_row(), 'name' => '', 'is_shortcode' => false, 'is_paginated' => true, 'is_search' => false, 'is_filtered' => false, 'total' => 0, 'total_pages' => 0, 'per_page' => 0, 'current_page' => 1, ); // If this is a main WC query, use global args as defaults. if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) { $default_args = array_merge( $default_args, array( 'is_search' => $GLOBALS['wp_query']->is_search(), 'is_filtered' => is_filtered(), 'total' => $GLOBALS['wp_query']->found_posts, 'total_pages' => $GLOBALS['wp_query']->max_num_pages, 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ), 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ), ) ); } // Merge any existing values. if ( isset( $GLOBALS['woocommerce_loop'] ) ) { $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] ); } $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args ); } add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' ); /** * Resets the woocommerce_loop global. * * @since 3.3.0 */ function wc_reset_loop() { unset( $GLOBALS['woocommerce_loop'] ); } add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 ); /** * Gets a property from the woocommerce_loop global. * * @since 3.3.0 * @param string $prop Prop to get. * @param string $default Default if the prop does not exist. * @return mixed */ function wc_get_loop_prop( $prop, $default = '' ) { wc_setup_loop(); // Ensure shop loop is setup. return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default; } /** * Sets a property in the woocommerce_loop global. * * @since 3.3.0 * @param string $prop Prop to set. * @param string $value Value to set. */ function wc_set_loop_prop( $prop, $value = '' ) { if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) { wc_setup_loop(); } $GLOBALS['woocommerce_loop'][ $prop ] = $value; } /** * Set the current visbility for a product in the woocommerce_loop global. * * @since 4.4.0 * @param int $product_id Product it to cache visbiility for. * @param bool $value The poduct visibility value to cache. */ function wc_set_loop_product_visibility( $product_id, $value ) { wc_set_loop_prop( "product_visibility_$product_id", $value ); } /** * Gets the cached current visibility for a product from the woocommerce_loop global. * * @since 4.4.0 * @param int $product_id Product id to get the cached visibility for. * * @return bool|null The cached product visibility, or null if on visibility has been cached for that product. */ function wc_get_loop_product_visibility( $product_id ) { return wc_get_loop_prop( "product_visibility_$product_id", null ); } /** * Should the WooCommerce loop be displayed? * * This will return true if we have posts (products) or if we have subcats to display. * * @since 3.4.0 * @return bool */ function woocommerce_product_loop() { return have_posts() || 'products' !== woocommerce_get_loop_display_mode(); } /** * Output generator tag to aid debugging. * * @param string $gen Generator. * @param string $type Type. * @return string */ function wc_generator_tag( $gen, $type ) { $version = Constants::get_constant( 'WC_VERSION' ); switch ( $type ) { case 'html': $gen .= "\n" . '<meta name="generator" content="WooCommerce ' . esc_attr( $version ) . '">'; break; case 'xhtml': $gen .= "\n" . '<meta name="generator" content="WooCommerce ' . esc_attr( $version ) . '" />'; break; } return $gen; } /** * Add body classes for WC pages. * * @param array $classes Body Classes. * @return array */ function wc_body_class( $classes ) { $classes = (array) $classes; if ( is_shop() ) { $classes[] = 'woocommerce-shop'; } if ( is_woocommerce() ) { $classes[] = 'woocommerce'; $classes[] = 'woocommerce-page'; } elseif ( is_checkout() ) { $classes[] = 'woocommerce-checkout'; $classes[] = 'woocommerce-page'; } elseif ( is_cart() ) { $classes[] = 'woocommerce-cart'; $classes[] = 'woocommerce-page'; } elseif ( is_account_page() ) { $classes[] = 'woocommerce-account'; $classes[] = 'woocommerce-page'; } if ( is_store_notice_showing() ) { $classes[] = 'woocommerce-demo-store'; } foreach ( WC()->query->get_query_vars() as $key => $value ) { if ( is_wc_endpoint_url( $key ) ) { $classes[] = 'woocommerce-' . sanitize_html_class( $key ); } } $classes[] = 'woocommerce-no-js'; add_action( 'wp_footer', 'wc_no_js' ); return array_unique( $classes ); } /** * NO JS handling. * * @since 3.4.0 */ function wc_no_js() { ?> <script type="text/javascript"> (function () { var c = document.body.className; c = c.replace(/woocommerce-no-js/, 'woocommerce-js'); document.body.className = c; })(); </script> <?php } /** * Display the classes for the product cat div. * * @since 2.4.0 * @param string|array $class One or more classes to add to the class list. * @param object $category object Optional. */ function wc_product_cat_class( $class = '', $category = null ) { // Separates classes with a single space, collates classes for post DIV. echo 'class="' . esc_attr( join( ' ', wc_get_product_cat_class( $class, $category ) ) ) . '"'; } /** * Get the default columns setting - this is how many products will be shown per row in loops. * * @since 3.3.0 * @return int */ function wc_get_default_products_per_row() { $columns = get_option( 'woocommerce_catalog_columns', 4 ); $product_grid = wc_get_theme_support( 'product_grid' ); $min_columns = isset( $product_grid['min_columns'] ) ? absint( $product_grid['min_columns'] ) : 0; $max_columns = isset( $product_grid['max_columns'] ) ? absint( $product_grid['max_columns'] ) : 0; if ( $min_columns && $columns < $min_columns ) { $columns = $min_columns; update_option( 'woocommerce_catalog_columns', $columns ); } elseif ( $max_columns && $columns > $max_columns ) { $columns = $max_columns; update_option( 'woocommerce_catalog_columns', $columns ); } if ( has_filter( 'loop_shop_columns' ) ) { // Legacy filter handling. $columns = apply_filters( 'loop_shop_columns', $columns ); } $columns = absint( $columns ); return max( 1, $columns ); } /** * Get the default rows setting - this is how many product rows will be shown in loops. * * @since 3.3.0 * @return int */ function wc_get_default_product_rows_per_page() { $rows = absint( get_option( 'woocommerce_catalog_rows', 4 ) ); $product_grid = wc_get_theme_support( 'product_grid' ); $min_rows = isset( $product_grid['min_rows'] ) ? absint( $product_grid['min_rows'] ) : 0; $max_rows = isset( $product_grid['max_rows'] ) ? absint( $product_grid['max_rows'] ) : 0; if ( $min_rows && $rows < $min_rows ) { $rows = $min_rows; update_option( 'woocommerce_catalog_rows', $rows ); } elseif ( $max_rows && $rows > $max_rows ) { $rows = $max_rows; update_option( 'woocommerce_catalog_rows', $rows ); } return $rows; } /** * Reset the product grid settings when a new theme is activated. * * @since 3.3.0 */ function wc_reset_product_grid_settings() { $product_grid = wc_get_theme_support( 'product_grid' ); if ( ! empty( $product_grid['default_rows'] ) ) { update_option( 'woocommerce_catalog_rows', absint( $product_grid['default_rows'] ) ); } if ( ! empty( $product_grid['default_columns'] ) ) { update_option( 'woocommerce_catalog_columns', absint( $product_grid['default_columns'] ) ); } wp_cache_flush(); // Flush any caches which could impact settings or templates. } add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' ); /** * Get classname for woocommerce loops. * * @since 2.6.0 * @return string */ function wc_get_loop_class() { $loop_index = wc_get_loop_prop( 'loop', 0 ); $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) ); $loop_index ++; wc_set_loop_prop( 'loop', $loop_index ); if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) { return 'first'; } if ( 0 === $loop_index % $columns ) { return 'last'; } return ''; } /** * Get the classes for the product cat div. * * @since 2.4.0 * * @param string|array $class One or more classes to add to the class list. * @param object $category object Optional. * * @return array */ function wc_get_product_cat_class( $class = '', $category = null ) { $classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) ); $classes[] = 'product-category'; $classes[] = 'product'; $classes[] = wc_get_loop_class(); $classes = apply_filters( 'product_cat_class', $classes, $class, $category ); return array_unique( array_filter( $classes ) ); } /** * Adds extra post classes for products via the WordPress post_class hook, if used. * * Note: For performance reasons we instead recommend using wc_product_class/wc_get_product_class instead. * * @since 2.1.0 * @param array $classes Current classes. * @param string|array $class Additional class. * @param int $post_id Post ID. * @return array */ function wc_product_post_class( $classes, $class = '', $post_id = 0 ) { if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ), true ) ) { return $classes; } $product = wc_get_product( $post_id ); if ( ! $product ) { return $classes; } $classes[] = 'product'; $classes[] = wc_get_loop_class(); $classes[] = $product->get_stock_status(); if ( $product->is_on_sale() ) { $classes[] = 'sale'; } if ( $product->is_featured() ) { $classes[] = 'featured'; } if ( $product->is_downloadable() ) { $classes[] = 'downloadable'; } if ( $product->is_virtual() ) { $classes[] = 'virtual'; } if ( $product->is_sold_individually() ) { $classes[] = 'sold-individually'; } if ( $product->is_taxable() ) { $classes[] = 'taxable'; } if ( $product->is_shipping_taxable() ) { $classes[] = 'shipping-taxable'; } if ( $product->is_purchasable() ) { $classes[] = 'purchasable'; } if ( $product->get_type() ) { $classes[] = 'product-type-' . $product->get_type(); } if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { $classes[] = 'has-default-attributes'; } $key = array_search( 'hentry', $classes, true ); if ( false !== $key ) { unset( $classes[ $key ] ); } return $classes; } /** * Get product taxonomy HTML classes. * * @since 3.4.0 * @param array $term_ids Array of terms IDs or objects. * @param string $taxonomy Taxonomy. * @return array */ function wc_get_product_taxonomy_class( $term_ids, $taxonomy ) { $classes = array(); foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( empty( $term->slug ) ) { continue; } $term_class = sanitize_html_class( $term->slug, $term->term_id ); if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { $term_class = $term->term_id; } // 'post_tag' uses the 'tag' prefix for backward compatibility. if ( 'post_tag' === $taxonomy ) { $classes[] = 'tag-' . $term_class; } else { $classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id ); } } return $classes; } /** * Retrieves the classes for the post div as an array. * * This method was modified from WordPress's get_post_class() to allow the removal of taxonomies * (for performance reasons). Previously wc_product_post_class was hooked into post_class. @since 3.6.0 * * @since 3.4.0 * @param string|array $class One or more classes to add to the class list. * @param int|WP_Post|WC_Product $product Product ID or product object. * @return array */ function wc_get_product_class( $class = '', $product = null ) { if ( is_null( $product ) && ! empty( $GLOBALS['product'] ) ) { // Product was null so pull from global. $product = $GLOBALS['product']; } if ( $product && ! is_a( $product, 'WC_Product' ) ) { // Make sure we have a valid product, or set to false. $product = wc_get_product( $product ); } if ( $class ) { if ( ! is_array( $class ) ) { $class = preg_split( '#\s+#', $class ); } } else { $class = array(); } $post_classes = array_map( 'esc_attr', $class ); if ( ! $product ) { return $post_classes; } // Run through the post_class hook so 3rd parties using this previously can still append classes. // Note, to change classes you will need to use the newer woocommerce_post_class filter. // @internal This removes the wc_product_post_class filter so classes are not duplicated. $filtered = has_filter( 'post_class', 'wc_product_post_class' ); if ( $filtered ) { remove_filter( 'post_class', 'wc_product_post_class', 20 ); } $post_classes = apply_filters( 'post_class', $post_classes, $class, $product->get_id() ); if ( $filtered ) { add_filter( 'post_class', 'wc_product_post_class', 20, 3 ); } $classes = array_merge( $post_classes, array( 'product', 'type-product', 'post-' . $product->get_id(), 'status-' . $product->get_status(), wc_get_loop_class(), $product->get_stock_status(), ), wc_get_product_taxonomy_class( $product->get_category_ids(), 'product_cat' ), wc_get_product_taxonomy_class( $product->get_tag_ids(), 'product_tag' ) ); if ( $product->get_image_id() ) { $classes[] = 'has-post-thumbnail'; } if ( $product->get_post_password() ) { $classes[] = post_password_required( $product->get_id() ) ? 'post-password-required' : 'post-password-protected'; } if ( $product->is_on_sale() ) { $classes[] = 'sale'; } if ( $product->is_featured() ) { $classes[] = 'featured'; } if ( $product->is_downloadable() ) { $classes[] = 'downloadable'; } if ( $product->is_virtual() ) { $classes[] = 'virtual'; } if ( $product->is_sold_individually() ) { $classes[] = 'sold-individually'; } if ( $product->is_taxable() ) { $classes[] = 'taxable'; } if ( $product->is_shipping_taxable() ) { $classes[] = 'shipping-taxable'; } if ( $product->is_purchasable() ) { $classes[] = 'purchasable'; } if ( $product->get_type() ) { $classes[] = 'product-type-' . $product->get_type(); } if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { $classes[] = 'has-default-attributes'; } // Include attributes and any extra taxonomies only if enabled via the hook - this is a performance issue. if ( apply_filters( 'woocommerce_get_product_class_include_taxonomies', false ) ) { $taxonomies = get_taxonomies( array( 'public' => true ) ); $type = 'variation' === $product->get_type() ? 'product_variation' : 'product'; foreach ( (array) $taxonomies as $taxonomy ) { if ( is_object_in_taxonomy( $type, $taxonomy ) && ! in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) { $classes = array_merge( $classes, wc_get_product_taxonomy_class( (array) get_the_terms( $product->get_id(), $taxonomy ), $taxonomy ) ); } } } /** * WooCommerce Post Class filter. * * @since 3.6.2 * @param array $classes Array of CSS classes. * @param WC_Product $product Product object. */ $classes = apply_filters( 'woocommerce_post_class', $classes, $product ); return array_map( 'esc_attr', array_unique( array_filter( $classes ) ) ); } /** * Display the classes for the product div. * * @since 3.4.0 * @param string|array $class One or more classes to add to the class list. * @param int|WP_Post|WC_Product $product_id Product ID or product object. */ function wc_product_class( $class = '', $product_id = null ) { echo 'class="' . esc_attr( implode( ' ', wc_get_product_class( $class, $product_id ) ) ) . '"'; } /** * Outputs hidden form inputs for each query string variable. * * @since 3.0.0 * @param string|array $values Name value pairs, or a URL to parse. * @param array $exclude Keys to exclude. * @param string $current_key Current key we are outputting. * @param bool $return Whether to return. * @return string */ function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) { if ( is_null( $values ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $values = $_GET; } elseif ( is_string( $values ) ) { $url_parts = wp_parse_url( $values ); $values = array(); if ( ! empty( $url_parts['query'] ) ) { // This is to preserve full-stops, pluses and spaces in the query string when ran through parse_str. $replace_chars = array( '.' => '{dot}', '+' => '{plus}', ); $query_string = str_replace( array_keys( $replace_chars ), array_values( $replace_chars ), $url_parts['query'] ); // Parse the string. parse_str( $query_string, $parsed_query_string ); // Convert the full-stops, pluses and spaces back and add to values array. foreach ( $parsed_query_string as $key => $value ) { $new_key = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key ); $new_value = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value ); $values[ $new_key ] = $new_value; } } } $html = ''; foreach ( $values as $key => $value ) { if ( in_array( $key, $exclude, true ) ) { continue; } if ( $current_key ) { $key = $current_key . '[' . $key . ']'; } if ( is_array( $value ) ) { $html .= wc_query_string_form_fields( $value, $exclude, $key, true ); } else { $html .= '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( wp_unslash( $value ) ) . '" />'; } } if ( $return ) { return $html; } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get the terms and conditons page ID. * * @since 3.4.0 * @return int */ function wc_terms_and_conditions_page_id() { $page_id = wc_get_page_id( 'terms' ); return apply_filters( 'woocommerce_terms_and_conditions_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); } /** * Get the privacy policy page ID. * * @since 3.4.0 * @return int */ function wc_privacy_policy_page_id() { $page_id = get_option( 'wp_page_for_privacy_policy', 0 ); return apply_filters( 'woocommerce_privacy_policy_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); } /** * See if the checkbox is enabled or not based on the existance of the terms page and checkbox text. * * @since 3.4.0 * @return bool */ function wc_terms_and_conditions_checkbox_enabled() { $page_id = wc_terms_and_conditions_page_id(); $page = $page_id ? get_post( $page_id ) : false; return $page && wc_get_terms_and_conditions_checkbox_text(); } /** * Get the terms and conditons checkbox text, if set. * * @since 3.4.0 * @return string */ function wc_get_terms_and_conditions_checkbox_text() { /* translators: %s terms and conditions page name and link */ return trim( apply_filters( 'woocommerce_get_terms_and_conditions_checkbox_text', get_option( 'woocommerce_checkout_terms_and_conditions_checkbox_text', sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ) ) ) ); } /** * Get the privacy policy text, if set. * * @since 3.4.0 * @param string $type Type of policy to load. Valid values include registration and checkout. * @return string */ function wc_get_privacy_policy_text( $type = '' ) { $text = ''; switch ( $type ) { case 'checkout': /* translators: %s privacy policy page name and link */ $text = get_option( 'woocommerce_checkout_privacy_policy_text', sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); break; case 'registration': /* translators: %s privacy policy page name and link */ $text = get_option( 'woocommerce_registration_privacy_policy_text', sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); break; } return trim( apply_filters( 'woocommerce_get_privacy_policy_text', $text, $type ) ); } /** * Output t&c checkbox text. * * @since 3.4.0 */ function wc_terms_and_conditions_checkbox_text() { $text = wc_get_terms_and_conditions_checkbox_text(); if ( ! $text ) { return; } echo wp_kses_post( wc_replace_policy_page_link_placeholders( $text ) ); } /** * Output t&c page's content (if set). The page can be set from checkout settings. * * @since 3.4.0 */ function wc_terms_and_conditions_page_content() { $terms_page_id = wc_terms_and_conditions_page_id(); if ( ! $terms_page_id ) { return; } $page = get_post( $terms_page_id ); if ( $page && 'publish' === $page->post_status && $page->post_content && ! has_shortcode( $page->post_content, 'woocommerce_checkout' ) ) { echo '<div class="woocommerce-terms-and-conditions" style="display: none; max-height: 200px; overflow: auto;">' . wc_format_content( wp_kses_post( $page->post_content ) ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Render privacy policy text on the checkout. * * @since 3.4.0 */ function wc_checkout_privacy_policy_text() { echo '<div class="woocommerce-privacy-policy-text">'; wc_privacy_policy_text( 'checkout' ); echo '</div>'; } /** * Render privacy policy text on the register forms. * * @since 3.4.0 */ function wc_registration_privacy_policy_text() { echo '<div class="woocommerce-privacy-policy-text">'; wc_privacy_policy_text( 'registration' ); echo '</div>'; } /** * Output privacy policy text. This is custom text which can be added via the customizer/privacy settings section. * * Loads the relevant policy for the current page unless a specific policy text is required. * * @since 3.4.0 * @param string $type Type of policy to load. Valid values include registration and checkout. */ function wc_privacy_policy_text( $type = 'checkout' ) { if ( ! wc_privacy_policy_page_id() ) { return; } echo wp_kses_post( wpautop( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( $type ) ) ) ); } /** * Replaces placeholders with links to WooCommerce policy pages. * * @since 3.4.0 * @param string $text Text to find/replace within. * @return string */ function wc_replace_policy_page_link_placeholders( $text ) { $privacy_page_id = wc_privacy_policy_page_id(); $terms_page_id = wc_terms_and_conditions_page_id(); $privacy_link = $privacy_page_id ? '<a href="' . esc_url( get_permalink( $privacy_page_id ) ) . '" class="woocommerce-privacy-policy-link" target="_blank">' . __( 'privacy policy', 'woocommerce' ) . '</a>' : __( 'privacy policy', 'woocommerce' ); $terms_link = $terms_page_id ? '<a href="' . esc_url( get_permalink( $terms_page_id ) ) . '" class="woocommerce-terms-and-conditions-link" target="_blank">' . __( 'terms and conditions', 'woocommerce' ) . '</a>' : __( 'terms and conditions', 'woocommerce' ); $find_replace = array( '[terms]' => $terms_link, '[privacy_policy]' => $privacy_link, ); return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $text ); } /** * Template pages */ if ( ! function_exists( 'woocommerce_content' ) ) { /** * Output WooCommerce content. * * This function is only used in the optional 'woocommerce.php' template. * which people can add to their themes to add basic woocommerce support. * without hooks or modifying core templates. */ function woocommerce_content() { if ( is_singular( 'product' ) ) { while ( have_posts() ) : the_post(); wc_get_template_part( 'content', 'single-product' ); endwhile; } else { ?> <?php if ( apply_filters( 'woocommerce_show_page_title', true ) ) : ?> <h1 class="page-title"><?php woocommerce_page_title(); ?></h1> <?php endif; ?> <?php do_action( 'woocommerce_archive_description' ); ?> <?php if ( woocommerce_product_loop() ) : ?> <?php do_action( 'woocommerce_before_shop_loop' ); ?> <?php woocommerce_product_loop_start(); ?> <?php if ( wc_get_loop_prop( 'total' ) ) : ?> <?php while ( have_posts() ) : ?> <?php the_post(); ?> <?php wc_get_template_part( 'content', 'product' ); ?> <?php endwhile; ?> <?php endif; ?> <?php woocommerce_product_loop_end(); ?> <?php do_action( 'woocommerce_after_shop_loop' ); ?> <?php else : do_action( 'woocommerce_no_products_found' ); endif; } } } /** * Global */ if ( ! function_exists( 'woocommerce_output_content_wrapper' ) ) { /** * Output the start of the page wrapper. */ function woocommerce_output_content_wrapper() { wc_get_template( 'global/wrapper-start.php' ); } } if ( ! function_exists( 'woocommerce_output_content_wrapper_end' ) ) { /** * Output the end of the page wrapper. */ function woocommerce_output_content_wrapper_end() { wc_get_template( 'global/wrapper-end.php' ); } } if ( ! function_exists( 'woocommerce_get_sidebar' ) ) { /** * Get the shop sidebar template. */ function woocommerce_get_sidebar() { wc_get_template( 'global/sidebar.php' ); } } if ( ! function_exists( 'woocommerce_demo_store' ) ) { /** * Adds a demo store banner to the site if enabled. */ function woocommerce_demo_store() { if ( ! is_store_notice_showing() ) { return; } $notice = get_option( 'woocommerce_demo_store_notice' ); if ( empty( $notice ) ) { $notice = __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ); } $notice_id = md5( $notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice ); } } /** * Loop */ if ( ! function_exists( 'woocommerce_page_title' ) ) { /** * Page Title function. * * @param bool $echo Should echo title. * @return string */ function woocommerce_page_title( $echo = true ) { if ( is_search() ) { /* translators: %s: search query */ $page_title = sprintf( __( 'Search results: “%s”', 'woocommerce' ), get_search_query() ); if ( get_query_var( 'paged' ) ) { /* translators: %s: page number */ $page_title .= sprintf( __( ' – Page %s', 'woocommerce' ), get_query_var( 'paged' ) ); } } elseif ( is_tax() ) { $page_title = single_term_title( '', false ); } else { $shop_page_id = wc_get_page_id( 'shop' ); $page_title = get_the_title( $shop_page_id ); } $page_title = apply_filters( 'woocommerce_page_title', $page_title ); if ( $echo ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $page_title; } else { return $page_title; } } } if ( ! function_exists( 'woocommerce_product_loop_start' ) ) { /** * Output the start of a product loop. By default this is a UL. * * @param bool $echo Should echo?. * @return string */ function woocommerce_product_loop_start( $echo = true ) { ob_start(); wc_set_loop_prop( 'loop', 0 ); wc_get_template( 'loop/loop-start.php' ); $loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() ); if ( $echo ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $loop_start; } else { return $loop_start; } } } if ( ! function_exists( 'woocommerce_product_loop_end' ) ) { /** * Output the end of a product loop. By default this is a UL. * * @param bool $echo Should echo?. * @return string */ function woocommerce_product_loop_end( $echo = true ) { ob_start(); wc_get_template( 'loop/loop-end.php' ); $loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() ); if ( $echo ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $loop_end; } else { return $loop_end; } } } if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) { /** * Show the product title in the product loop. By default this is an H2. */ function woocommerce_template_loop_product_title() { echo '<h2 class="' . esc_attr( apply_filters( 'woocommerce_product_loop_title_classes', 'woocommerce-loop-product__title' ) ) . '">' . get_the_title() . '</h2>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) { /** * Show the subcategory title in the product loop. * * @param object $category Category object. */ function woocommerce_template_loop_category_title( $category ) { ?> <h2 class="woocommerce-loop-category__title"> <?php echo esc_html( $category->name ); if ( $category->count > 0 ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category ); } ?> </h2> <?php } } if ( ! function_exists( 'woocommerce_template_loop_product_link_open' ) ) { /** * Insert the opening anchor tag for products in the loop. */ function woocommerce_template_loop_product_link_open() { global $product; $link = apply_filters( 'woocommerce_loop_product_link', get_the_permalink(), $product ); echo '<a href="' . esc_url( $link ) . '" class="woocommerce-LoopProduct-link woocommerce-loop-product__link">'; } } if ( ! function_exists( 'woocommerce_template_loop_product_link_close' ) ) { /** * Insert the closing anchor tag for products in the loop. */ function woocommerce_template_loop_product_link_close() { echo '</a>'; } } if ( ! function_exists( 'woocommerce_template_loop_category_link_open' ) ) { /** * Insert the opening anchor tag for categories in the loop. * * @param int|object|string $category Category ID, Object or String. */ function woocommerce_template_loop_category_link_open( $category ) { echo '<a href="' . esc_url( get_term_link( $category, 'product_cat' ) ) . '">'; } } if ( ! function_exists( 'woocommerce_template_loop_category_link_close' ) ) { /** * Insert the closing anchor tag for categories in the loop. */ function woocommerce_template_loop_category_link_close() { echo '</a>'; } } if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) { /** * Show an archive description on taxonomy archives. */ function woocommerce_taxonomy_archive_description() { if ( is_product_taxonomy() && 0 === absint( get_query_var( 'paged' ) ) ) { $term = get_queried_object(); if ( $term && ! empty( $term->description ) ) { echo '<div class="term-description">' . wc_format_content( wp_kses_post( $term->description ) ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } } } if ( ! function_exists( 'woocommerce_product_archive_description' ) ) { /** * Show a shop page description on product archives. */ function woocommerce_product_archive_description() { // Don't display the description on search results page. if ( is_search() ) { return; } if ( is_post_type_archive( 'product' ) && in_array( absint( get_query_var( 'paged' ) ), array( 0, 1 ), true ) ) { $shop_page = get_post( wc_get_page_id( 'shop' ) ); if ( $shop_page ) { $allowed_html = wp_kses_allowed_html( 'post' ); // This is needed for the search product block to work. $allowed_html = array_merge( $allowed_html, array( 'form' => array( 'action' => true, 'accept' => true, 'accept-charset' => true, 'enctype' => true, 'method' => true, 'name' => true, 'target' => true, ), 'input' => array( 'type' => true, 'id' => true, 'class' => true, 'placeholder' => true, 'name' => true, 'value' => true, ), 'button' => array( 'type' => true, 'class' => true, 'label' => true, ), 'svg' => array( 'hidden' => true, 'role' => true, 'focusable' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, ), 'path' => array( 'd' => true, ), ) ); $description = wc_format_content( wp_kses( $shop_page->post_content, $allowed_html ) ); if ( $description ) { echo '<div class="page-description">' . $description . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } } } } if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { /** * Get the add to cart template for the loop. * * @param array $args Arguments. */ function woocommerce_template_loop_add_to_cart( $args = array() ) { global $product; if ( $product ) { $defaults = array( 'quantity' => 1, 'class' => implode( ' ', array_filter( array( 'button', 'product_type_' . $product->get_type(), $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '', ) ) ), 'attributes' => array( 'data-product_id' => $product->get_id(), 'data-product_sku' => $product->get_sku(), 'aria-label' => $product->add_to_cart_description(), 'rel' => 'nofollow', ), ); $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); if ( isset( $args['attributes']['aria-label'] ) ) { $args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] ); } wc_get_template( 'loop/add-to-cart.php', $args ); } } } if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) { /** * Get the product thumbnail for the loop. */ function woocommerce_template_loop_product_thumbnail() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo woocommerce_get_product_thumbnail(); } } if ( ! function_exists( 'woocommerce_template_loop_price' ) ) { /** * Get the product price for the loop. */ function woocommerce_template_loop_price() { wc_get_template( 'loop/price.php' ); } } if ( ! function_exists( 'woocommerce_template_loop_rating' ) ) { /** * Display the average rating in the loop. */ function woocommerce_template_loop_rating() { wc_get_template( 'loop/rating.php' ); } } if ( ! function_exists( 'woocommerce_show_product_loop_sale_flash' ) ) { /** * Get the sale flash for the loop. */ function woocommerce_show_product_loop_sale_flash() { wc_get_template( 'loop/sale-flash.php' ); } } if ( ! function_exists( 'woocommerce_get_product_thumbnail' ) ) { /** * Get the product thumbnail, or the placeholder if not set. * * @param string $size (default: 'woocommerce_thumbnail'). * @param int $deprecated1 Deprecated since WooCommerce 2.0 (default: 0). * @param int $deprecated2 Deprecated since WooCommerce 2.0 (default: 0). * @return string */ function woocommerce_get_product_thumbnail( $size = 'woocommerce_thumbnail', $deprecated1 = 0, $deprecated2 = 0 ) { global $product; $image_size = apply_filters( 'single_product_archive_thumbnail_size', $size ); return $product ? $product->get_image( $image_size ) : ''; } } if ( ! function_exists( 'woocommerce_result_count' ) ) { /** * Output the result count text (Showing x - x of x results). */ function woocommerce_result_count() { if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { return; } $args = array( 'total' => wc_get_loop_prop( 'total' ), 'per_page' => wc_get_loop_prop( 'per_page' ), 'current' => wc_get_loop_prop( 'current_page' ), ); wc_get_template( 'loop/result-count.php', $args ); } } if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) { /** * Output the product sorting options. */ function woocommerce_catalog_ordering() { if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { return; } $show_default_orderby = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); $catalog_orderby_options = apply_filters( 'woocommerce_catalog_orderby', array( 'menu_order' => __( 'Default sorting', 'woocommerce' ), 'popularity' => __( 'Sort by popularity', 'woocommerce' ), 'rating' => __( 'Sort by average rating', 'woocommerce' ), 'date' => __( 'Sort by latest', 'woocommerce' ), 'price' => __( 'Sort by price: low to high', 'woocommerce' ), 'price-desc' => __( 'Sort by price: high to low', 'woocommerce' ), ) ); $default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( wc_get_loop_prop( 'is_search' ) ) { $catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options ); unset( $catalog_orderby_options['menu_order'] ); } if ( ! $show_default_orderby ) { unset( $catalog_orderby_options['menu_order'] ); } if ( ! wc_review_ratings_enabled() ) { unset( $catalog_orderby_options['rating'] ); } if ( ! array_key_exists( $orderby, $catalog_orderby_options ) ) { $orderby = current( array_keys( $catalog_orderby_options ) ); } wc_get_template( 'loop/orderby.php', array( 'catalog_orderby_options' => $catalog_orderby_options, 'orderby' => $orderby, 'show_default_orderby' => $show_default_orderby, ) ); } } if ( ! function_exists( 'woocommerce_pagination' ) ) { /** * Output the pagination. */ function woocommerce_pagination() { if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { return; } $args = array( 'total' => wc_get_loop_prop( 'total_pages' ), 'current' => wc_get_loop_prop( 'current_page' ), 'base' => esc_url_raw( add_query_arg( 'product-page', '%#%', false ) ), 'format' => '?product-page=%#%', ); if ( ! wc_get_loop_prop( 'is_shortcode' ) ) { $args['format'] = ''; $args['base'] = esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) ); } wc_get_template( 'loop/pagination.php', $args ); } } /** * Single Product */ if ( ! function_exists( 'woocommerce_show_product_images' ) ) { /** * Output the product image before the single product summary. */ function woocommerce_show_product_images() { wc_get_template( 'single-product/product-image.php' ); } } if ( ! function_exists( 'woocommerce_show_product_thumbnails' ) ) { /** * Output the product thumbnails. */ function woocommerce_show_product_thumbnails() { wc_get_template( 'single-product/product-thumbnails.php' ); } } /** * Get HTML for a gallery image. * * Hooks: woocommerce_gallery_thumbnail_size, woocommerce_gallery_image_size and woocommerce_gallery_full_size accept name based image sizes, or an array of width/height values. * * @since 3.3.2 * @param int $attachment_id Attachment ID. * @param bool $main_image Is this the main image or a thumbnail?. * @return string */ function wc_get_gallery_image_html( $attachment_id, $main_image = false ) { $flexslider = (bool) apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ); $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); $thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); $image_size = apply_filters( 'woocommerce_gallery_image_size', $flexslider || $main_image ? 'woocommerce_single' : $thumbnail_size ); $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); $thumbnail_src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); $full_src = wp_get_attachment_image_src( $attachment_id, $full_size ); $alt_text = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ); $image = wp_get_attachment_image( $attachment_id, $image_size, false, apply_filters( 'woocommerce_gallery_image_html_attachment_image_params', array( 'title' => _wp_specialchars( get_post_field( 'post_title', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), 'data-caption' => _wp_specialchars( get_post_field( 'post_excerpt', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), 'data-src' => esc_url( $full_src[0] ), 'data-large_image' => esc_url( $full_src[0] ), 'data-large_image_width' => esc_attr( $full_src[1] ), 'data-large_image_height' => esc_attr( $full_src[2] ), 'class' => esc_attr( $main_image ? 'wp-post-image' : '' ), ), $attachment_id, $image_size, $main_image ) ); return '<div data-thumb="' . esc_url( $thumbnail_src[0] ) . '" data-thumb-alt="' . esc_attr( $alt_text ) . '" class="woocommerce-product-gallery__image"><a href="' . esc_url( $full_src[0] ) . '">' . $image . '</a></div>'; } if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) { /** * Output the product tabs. */ function woocommerce_output_product_data_tabs() { wc_get_template( 'single-product/tabs/tabs.php' ); } } if ( ! function_exists( 'woocommerce_template_single_title' ) ) { /** * Output the product title. */ function woocommerce_template_single_title() { wc_get_template( 'single-product/title.php' ); } } if ( ! function_exists( 'woocommerce_template_single_rating' ) ) { /** * Output the product rating. */ function woocommerce_template_single_rating() { if ( post_type_supports( 'product', 'comments' ) ) { wc_get_template( 'single-product/rating.php' ); } } } if ( ! function_exists( 'woocommerce_template_single_price' ) ) { /** * Output the product price. */ function woocommerce_template_single_price() { wc_get_template( 'single-product/price.php' ); } } if ( ! function_exists( 'woocommerce_template_single_excerpt' ) ) { /** * Output the product short description (excerpt). */ function woocommerce_template_single_excerpt() { wc_get_template( 'single-product/short-description.php' ); } } if ( ! function_exists( 'woocommerce_template_single_meta' ) ) { /** * Output the product meta. */ function woocommerce_template_single_meta() { wc_get_template( 'single-product/meta.php' ); } } if ( ! function_exists( 'woocommerce_template_single_sharing' ) ) { /** * Output the product sharing. */ function woocommerce_template_single_sharing() { wc_get_template( 'single-product/share.php' ); } } if ( ! function_exists( 'woocommerce_show_product_sale_flash' ) ) { /** * Output the product sale flash. */ function woocommerce_show_product_sale_flash() { wc_get_template( 'single-product/sale-flash.php' ); } } if ( ! function_exists( 'woocommerce_template_single_add_to_cart' ) ) { /** * Trigger the single product add to cart action. */ function woocommerce_template_single_add_to_cart() { global $product; do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); } } if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) { /** * Output the simple product add to cart area. */ function woocommerce_simple_add_to_cart() { wc_get_template( 'single-product/add-to-cart/simple.php' ); } } if ( ! function_exists( 'woocommerce_grouped_add_to_cart' ) ) { /** * Output the grouped product add to cart area. */ function woocommerce_grouped_add_to_cart() { global $product; $products = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' ); if ( $products ) { wc_get_template( 'single-product/add-to-cart/grouped.php', array( 'grouped_product' => $product, 'grouped_products' => $products, 'quantites_required' => false, ) ); } } } if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { /** * Output the variable product add to cart area. */ function woocommerce_variable_add_to_cart() { global $product; // Enqueue variation scripts. wp_enqueue_script( 'wc-add-to-cart-variation' ); // Get Available variations? $get_variations = count( $product->get_children() ) <= apply_filters( 'woocommerce_ajax_variation_threshold', 30, $product ); // Load the template. wc_get_template( 'single-product/add-to-cart/variable.php', array( 'available_variations' => $get_variations ? $product->get_available_variations() : false, 'attributes' => $product->get_variation_attributes(), 'selected_attributes' => $product->get_default_attributes(), ) ); } } if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) { /** * Output the external product add to cart area. */ function woocommerce_external_add_to_cart() { global $product; if ( ! $product->add_to_cart_url() ) { return; } wc_get_template( 'single-product/add-to-cart/external.php', array( 'product_url' => $product->add_to_cart_url(), 'button_text' => $product->single_add_to_cart_text(), ) ); } } if ( ! function_exists( 'woocommerce_quantity_input' ) ) { /** * Output the quantity input for add to cart forms. * * @param array $args Args for the input. * @param WC_Product|null $product Product. * @param boolean $echo Whether to return or echo|string. * * @return string */ function woocommerce_quantity_input( $args = array(), $product = null, $echo = true ) { if ( is_null( $product ) ) { $product = $GLOBALS['product']; } $defaults = array( 'input_id' => uniqid( 'quantity_' ), 'input_name' => 'quantity', 'input_value' => '1', 'classes' => apply_filters( 'woocommerce_quantity_input_classes', array( 'input-text', 'qty', 'text' ), $product ), 'max_value' => apply_filters( 'woocommerce_quantity_input_max', -1, $product ), 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $product ), 'step' => apply_filters( 'woocommerce_quantity_input_step', 1, $product ), 'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', has_filter( 'woocommerce_stock_amount', 'intval' ) ? '[0-9]*' : '' ), 'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'numeric' : '' ), 'product_name' => $product ? $product->get_title() : '', 'placeholder' => apply_filters( 'woocommerce_quantity_input_placeholder', '', $product ), ); $args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product ); // Apply sanity to min/max args - min cannot be lower than 0. $args['min_value'] = max( $args['min_value'], 0 ); $args['max_value'] = 0 < $args['max_value'] ? $args['max_value'] : ''; // Max cannot be lower than min if defined. if ( '' !== $args['max_value'] && $args['max_value'] < $args['min_value'] ) { $args['max_value'] = $args['min_value']; } ob_start(); wc_get_template( 'global/quantity-input.php', $args ); if ( $echo ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ob_get_clean(); } else { return ob_get_clean(); } } } if ( ! function_exists( 'woocommerce_product_description_tab' ) ) { /** * Output the description tab content. */ function woocommerce_product_description_tab() { wc_get_template( 'single-product/tabs/description.php' ); } } if ( ! function_exists( 'woocommerce_product_additional_information_tab' ) ) { /** * Output the attributes tab content. */ function woocommerce_product_additional_information_tab() { wc_get_template( 'single-product/tabs/additional-information.php' ); } } if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { /** * Add default product tabs to product pages. * * @param array $tabs Array of tabs. * @return array */ function woocommerce_default_product_tabs( $tabs = array() ) { global $product, $post; // Description tab - shows product content. if ( $post->post_content ) { $tabs['description'] = array( 'title' => __( 'Description', 'woocommerce' ), 'priority' => 10, 'callback' => 'woocommerce_product_description_tab', ); } // Additional information tab - shows attributes. if ( $product && ( $product->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ) ) ) { $tabs['additional_information'] = array( 'title' => __( 'Additional information', 'woocommerce' ), 'priority' => 20, 'callback' => 'woocommerce_product_additional_information_tab', ); } // Reviews tab - shows comments. if ( comments_open() ) { $tabs['reviews'] = array( /* translators: %s: reviews count */ 'title' => sprintf( __( 'Reviews (%d)', 'woocommerce' ), $product->get_review_count() ), 'priority' => 30, 'callback' => 'comments_template', ); } return $tabs; } } if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) { /** * Sort tabs by priority. * * @param array $tabs Array of tabs. * @return array */ function woocommerce_sort_product_tabs( $tabs = array() ) { // Make sure the $tabs parameter is an array. if ( ! is_array( $tabs ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' ); $tabs = array(); } // Re-order tabs by priority. if ( ! function_exists( '_sort_priority_callback' ) ) { /** * Sort Priority Callback Function * * @param array $a Comparison A. * @param array $b Comparison B. * @return bool */ function _sort_priority_callback( $a, $b ) { if ( ! isset( $a['priority'], $b['priority'] ) || $a['priority'] === $b['priority'] ) { return 0; } return ( $a['priority'] < $b['priority'] ) ? -1 : 1; } } uasort( $tabs, '_sort_priority_callback' ); return $tabs; } } if ( ! function_exists( 'woocommerce_comments' ) ) { /** * Output the Review comments template. * * @param WP_Comment $comment Comment object. * @param array $args Arguments. * @param int $depth Depth. */ function woocommerce_comments( $comment, $args, $depth ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $GLOBALS['comment'] = $comment; wc_get_template( 'single-product/review.php', array( 'comment' => $comment, 'args' => $args, 'depth' => $depth, ) ); } } if ( ! function_exists( 'woocommerce_review_display_gravatar' ) ) { /** * Display the review authors gravatar * * @param array $comment WP_Comment. * @return void */ function woocommerce_review_display_gravatar( $comment ) { echo get_avatar( $comment, apply_filters( 'woocommerce_review_gravatar_size', '60' ), '' ); } } if ( ! function_exists( 'woocommerce_review_display_rating' ) ) { /** * Display the reviewers star rating * * @return void */ function woocommerce_review_display_rating() { if ( post_type_supports( 'product', 'comments' ) ) { wc_get_template( 'single-product/review-rating.php' ); } } } if ( ! function_exists( 'woocommerce_review_display_meta' ) ) { /** * Display the review authors meta (name, verified owner, review date) * * @return void */ function woocommerce_review_display_meta() { wc_get_template( 'single-product/review-meta.php' ); } } if ( ! function_exists( 'woocommerce_review_display_comment_text' ) ) { /** * Display the review content. */ function woocommerce_review_display_comment_text() { echo '<div class="description">'; comment_text(); echo '</div>'; } } if ( ! function_exists( 'woocommerce_output_related_products' ) ) { /** * Output the related products. */ function woocommerce_output_related_products() { $args = array( 'posts_per_page' => 4, 'columns' => 4, 'orderby' => 'rand', // @codingStandardsIgnoreLine. ); woocommerce_related_products( apply_filters( 'woocommerce_output_related_products_args', $args ) ); } } if ( ! function_exists( 'woocommerce_related_products' ) ) { /** * Output the related products. * * @param array $args Provided arguments. */ function woocommerce_related_products( $args = array() ) { global $product; if ( ! $product ) { return; } $defaults = array( 'posts_per_page' => 2, 'columns' => 2, 'orderby' => 'rand', // @codingStandardsIgnoreLine. 'order' => 'desc', ); $args = wp_parse_args( $args, $defaults ); // Get visible related products then sort them at random. $args['related_products'] = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $args['posts_per_page'], $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' ); // Handle orderby. $args['related_products'] = wc_products_array_orderby( $args['related_products'], $args['orderby'], $args['order'] ); // Set global loop values. wc_set_loop_prop( 'name', 'related' ); wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_related_products_columns', $args['columns'] ) ); wc_get_template( 'single-product/related.php', $args ); } } if ( ! function_exists( 'woocommerce_upsell_display' ) ) { /** * Output product up sells. * * @param int $limit (default: -1). * @param int $columns (default: 4). * @param string $orderby Supported values - rand, title, ID, date, modified, menu_order, price. * @param string $order Sort direction. */ function woocommerce_upsell_display( $limit = '-1', $columns = 4, $orderby = 'rand', $order = 'desc' ) { global $product; if ( ! $product ) { return; } // Handle the legacy filter which controlled posts per page etc. $args = apply_filters( 'woocommerce_upsell_display_args', array( 'posts_per_page' => $limit, 'orderby' => $orderby, 'order' => $order, 'columns' => $columns, ) ); wc_set_loop_prop( 'name', 'up-sells' ); wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_upsells_columns', isset( $args['columns'] ) ? $args['columns'] : $columns ) ); $orderby = apply_filters( 'woocommerce_upsells_orderby', isset( $args['orderby'] ) ? $args['orderby'] : $orderby ); $order = apply_filters( 'woocommerce_upsells_order', isset( $args['order'] ) ? $args['order'] : $order ); $limit = apply_filters( 'woocommerce_upsells_total', isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : $limit ); // Get visible upsells then sort them at random, then limit result set. $upsells = wc_products_array_orderby( array_filter( array_map( 'wc_get_product', $product->get_upsell_ids() ), 'wc_products_array_filter_visible' ), $orderby, $order ); $upsells = $limit > 0 ? array_slice( $upsells, 0, $limit ) : $upsells; wc_get_template( 'single-product/up-sells.php', array( 'upsells' => $upsells, // Not used now, but used in previous version of up-sells.php. 'posts_per_page' => $limit, 'orderby' => $orderby, 'columns' => $columns, ) ); } } /** Cart */ if ( ! function_exists( 'woocommerce_shipping_calculator' ) ) { /** * Output the cart shipping calculator. * * @param string $button_text Text for the shipping calculation toggle. */ function woocommerce_shipping_calculator( $button_text = '' ) { if ( 'no' === get_option( 'woocommerce_enable_shipping_calc' ) || ! WC()->cart->needs_shipping() ) { return; } wp_enqueue_script( 'wc-country-select' ); wc_get_template( 'cart/shipping-calculator.php', array( 'button_text' => $button_text, ) ); } } if ( ! function_exists( 'woocommerce_cart_totals' ) ) { /** * Output the cart totals. */ function woocommerce_cart_totals() { if ( is_checkout() ) { return; } wc_get_template( 'cart/cart-totals.php' ); } } if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) { /** * Output the cart cross-sells. * * @param int $limit (default: 2). * @param int $columns (default: 2). * @param string $orderby (default: 'rand'). * @param string $order (default: 'desc'). */ function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) { if ( is_checkout() ) { return; } // Get visible cross sells then sort them at random. $cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' ); wc_set_loop_prop( 'name', 'cross-sells' ); wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_cross_sells_columns', $columns ) ); // Handle orderby and limit results. $orderby = apply_filters( 'woocommerce_cross_sells_orderby', $orderby ); $order = apply_filters( 'woocommerce_cross_sells_order', $order ); $cross_sells = wc_products_array_orderby( $cross_sells, $orderby, $order ); $limit = apply_filters( 'woocommerce_cross_sells_total', $limit ); $cross_sells = $limit > 0 ? array_slice( $cross_sells, 0, $limit ) : $cross_sells; wc_get_template( 'cart/cross-sells.php', array( 'cross_sells' => $cross_sells, // Not used now, but used in previous version of up-sells.php. 'posts_per_page' => $limit, 'orderby' => $orderby, 'columns' => $columns, ) ); } } if ( ! function_exists( 'woocommerce_button_proceed_to_checkout' ) ) { /** * Output the proceed to checkout button. */ function woocommerce_button_proceed_to_checkout() { wc_get_template( 'cart/proceed-to-checkout-button.php' ); } } if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) { /** * Output the view cart button. */ function woocommerce_widget_shopping_cart_button_view_cart() { echo '<a href="' . esc_url( wc_get_cart_url() ) . '" class="button wc-forward">' . esc_html__( 'View cart', 'woocommerce' ) . '</a>'; } } if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) ) { /** * Output the proceed to checkout button. */ function woocommerce_widget_shopping_cart_proceed_to_checkout() { echo '<a href="' . esc_url( wc_get_checkout_url() ) . '" class="button checkout wc-forward">' . esc_html__( 'Checkout', 'woocommerce' ) . '</a>'; } } if ( ! function_exists( 'woocommerce_widget_shopping_cart_subtotal' ) ) { /** * Output to view cart subtotal. * * @since 3.7.0 */ function woocommerce_widget_shopping_cart_subtotal() { echo '<strong>' . esc_html__( 'Subtotal:', 'woocommerce' ) . '</strong> ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** Mini-Cart */ if ( ! function_exists( 'woocommerce_mini_cart' ) ) { /** * Output the Mini-cart - used by cart widget. * * @param array $args Arguments. */ function woocommerce_mini_cart( $args = array() ) { $defaults = array( 'list_class' => '', ); $args = wp_parse_args( $args, $defaults ); wc_get_template( 'cart/mini-cart.php', $args ); } } /** Login */ if ( ! function_exists( 'woocommerce_login_form' ) ) { /** * Output the WooCommerce Login Form. * * @param array $args Arguments. */ function woocommerce_login_form( $args = array() ) { $defaults = array( 'message' => '', 'redirect' => '', 'hidden' => false, ); $args = wp_parse_args( $args, $defaults ); wc_get_template( 'global/form-login.php', $args ); } } if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) { /** * Output the WooCommerce Checkout Login Form. */ function woocommerce_checkout_login_form() { wc_get_template( 'checkout/form-login.php', array( 'checkout' => WC()->checkout(), ) ); } } if ( ! function_exists( 'woocommerce_breadcrumb' ) ) { /** * Output the WooCommerce Breadcrumb. * * @param array $args Arguments. */ function woocommerce_breadcrumb( $args = array() ) { $args = wp_parse_args( $args, apply_filters( 'woocommerce_breadcrumb_defaults', array( 'delimiter' => ' / ', 'wrap_before' => '<nav class="woocommerce-breadcrumb">', 'wrap_after' => '</nav>', 'before' => '', 'after' => '', 'home' => _x( 'Home', 'breadcrumb', 'woocommerce' ), ) ) ); $breadcrumbs = new WC_Breadcrumb(); if ( ! empty( $args['home'] ) ) { $breadcrumbs->add_crumb( $args['home'], apply_filters( 'woocommerce_breadcrumb_home_url', home_url() ) ); } $args['breadcrumb'] = $breadcrumbs->generate(); /** * WooCommerce Breadcrumb hook * * @hooked WC_Structured_Data::generate_breadcrumblist_data() - 10 */ do_action( 'woocommerce_breadcrumb', $breadcrumbs, $args ); wc_get_template( 'global/breadcrumb.php', $args ); } } if ( ! function_exists( 'woocommerce_order_review' ) ) { /** * Output the Order review table for the checkout. * * @param bool $deprecated Deprecated param. */ function woocommerce_order_review( $deprecated = false ) { wc_get_template( 'checkout/review-order.php', array( 'checkout' => WC()->checkout(), ) ); } } if ( ! function_exists( 'woocommerce_checkout_payment' ) ) { /** * Output the Payment Methods on the checkout. */ function woocommerce_checkout_payment() { if ( WC()->cart->needs_payment() ) { $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); WC()->payment_gateways()->set_current_gateway( $available_gateways ); } else { $available_gateways = array(); } wc_get_template( 'checkout/payment.php', array( 'checkout' => WC()->checkout(), 'available_gateways' => $available_gateways, 'order_button_text' => apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ), ) ); } } if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) { /** * Output the Coupon form for the checkout. */ function woocommerce_checkout_coupon_form() { if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) { wc_get_template( 'checkout/form-coupon.php', array( 'checkout' => WC()->checkout(), ) ); } } } if ( ! function_exists( 'woocommerce_products_will_display' ) ) { /** * Check if we will be showing products or not (and not sub-categories only). * * @return bool */ function woocommerce_products_will_display() { $display_type = woocommerce_get_loop_display_mode(); return 0 < wc_get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type; } } if ( ! function_exists( 'woocommerce_get_loop_display_mode' ) ) { /** * See what is going to display in the loop. * * @since 3.3.0 * @return string Either products, subcategories, or both, based on current page. */ function woocommerce_get_loop_display_mode() { // Only return products when filtering things. if ( wc_get_loop_prop( 'is_search' ) || wc_get_loop_prop( 'is_filtered' ) ) { return 'products'; } $parent_id = 0; $display_type = ''; if ( is_shop() ) { $display_type = get_option( 'woocommerce_shop_page_display', '' ); } elseif ( is_product_category() ) { $parent_id = get_queried_object_id(); $display_type = get_term_meta( $parent_id, 'display_type', true ); $display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type; } if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) { return 'products'; } // Ensure valid value. if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) { $display_type = 'products'; } // If we're showing categories, ensure we actually have something to show. if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) { $subcategories = woocommerce_get_product_subcategories( $parent_id ); if ( empty( $subcategories ) ) { $display_type = 'products'; } } return $display_type; } } if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) { /** * Maybe display categories before, or instead of, a product loop. * * @since 3.3.0 * @param string $loop_html HTML. * @return string */ function woocommerce_maybe_show_product_subcategories( $loop_html = '' ) { if ( wc_get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) { return $loop_html; } $display_type = woocommerce_get_loop_display_mode(); // If displaying categories, append to the loop. if ( 'subcategories' === $display_type || 'both' === $display_type ) { ob_start(); woocommerce_output_product_categories( array( 'parent_id' => is_product_category() ? get_queried_object_id() : 0, ) ); $loop_html .= ob_get_clean(); if ( 'subcategories' === $display_type ) { wc_set_loop_prop( 'total', 0 ); // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. global $wp_query; if ( $wp_query->is_main_query() ) { $wp_query->post_count = 0; $wp_query->max_num_pages = 0; } } } return $loop_html; } } if ( ! function_exists( 'woocommerce_product_subcategories' ) ) { /** * This is a legacy function which used to check if we needed to display subcats and then output them. It was called by templates. * * From 3.3 onwards this is all handled via hooks and the woocommerce_maybe_show_product_subcategories function. * * Since some templates have not updated compatibility, to avoid showing incorrect categories this function has been deprecated and will * return nothing. Replace usage with woocommerce_output_product_categories to render the category list manually. * * This is a legacy function which also checks if things should display. * Themes no longer need to call these functions. It's all done via hooks. * * @deprecated 3.3.1 @todo Add a notice in a future version. * @param array $args Arguments. * @return null|boolean */ function woocommerce_product_subcategories( $args = array() ) { $defaults = array( 'before' => '', 'after' => '', 'force_display' => false, ); $args = wp_parse_args( $args, $defaults ); if ( $args['force_display'] ) { // We can still render if display is forced. woocommerce_output_product_categories( array( 'before' => $args['before'], 'after' => $args['after'], 'parent_id' => is_product_category() ? get_queried_object_id() : 0, ) ); return true; } else { // Output nothing. woocommerce_maybe_show_product_subcategories will handle the output of cats. $display_type = woocommerce_get_loop_display_mode(); if ( 'subcategories' === $display_type ) { // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. global $wp_query; if ( $wp_query->is_main_query() ) { $wp_query->post_count = 0; $wp_query->max_num_pages = 0; } } return 'subcategories' === $display_type || 'both' === $display_type; } } } if ( ! function_exists( 'woocommerce_output_product_categories' ) ) { /** * Display product sub categories as thumbnails. * * This is a replacement for woocommerce_product_subcategories which also does some logic * based on the loop. This function however just outputs when called. * * @since 3.3.1 * @param array $args Arguments. * @return boolean */ function woocommerce_output_product_categories( $args = array() ) { $args = wp_parse_args( $args, array( 'before' => apply_filters( 'woocommerce_before_output_product_categories', '' ), 'after' => apply_filters( 'woocommerce_after_output_product_categories', '' ), 'parent_id' => 0, ) ); $product_categories = woocommerce_get_product_subcategories( $args['parent_id'] ); if ( ! $product_categories ) { return false; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $args['before']; foreach ( $product_categories as $category ) { wc_get_template( 'content-product_cat.php', array( 'category' => $category, ) ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $args['after']; return true; } } if ( ! function_exists( 'woocommerce_get_product_subcategories' ) ) { /** * Get (and cache) product subcategories. * * @param int $parent_id Get subcategories of this ID. * @return array */ function woocommerce_get_product_subcategories( $parent_id = 0 ) { $parent_id = absint( $parent_id ); $cache_key = apply_filters( 'woocommerce_get_product_subcategories_cache_key', 'product-category-hierarchy-' . $parent_id, $parent_id ); $product_categories = $cache_key ? wp_cache_get( $cache_key, 'product_cat' ) : false; if ( false === $product_categories ) { // NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( https://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work. $product_categories = get_categories( apply_filters( 'woocommerce_product_subcategories_args', array( 'parent' => $parent_id, 'hide_empty' => 0, 'hierarchical' => 1, 'taxonomy' => 'product_cat', 'pad_counts' => 1, ) ) ); if ( $cache_key ) { wp_cache_set( $cache_key, $product_categories, 'product_cat' ); } } if ( apply_filters( 'woocommerce_product_subcategories_hide_empty', true ) ) { $product_categories = wp_list_filter( $product_categories, array( 'count' => 0 ), 'NOT' ); } return $product_categories; } } if ( ! function_exists( 'woocommerce_subcategory_thumbnail' ) ) { /** * Show subcategory thumbnails. * * @param mixed $category Category. */ function woocommerce_subcategory_thumbnail( $category ) { $small_thumbnail_size = apply_filters( 'subcategory_archive_thumbnail_size', 'woocommerce_thumbnail' ); $dimensions = wc_get_image_size( $small_thumbnail_size ); $thumbnail_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); if ( $thumbnail_id ) { $image = wp_get_attachment_image_src( $thumbnail_id, $small_thumbnail_size ); $image = $image[0]; $image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $small_thumbnail_size ) : false; $image_sizes = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $small_thumbnail_size ) : false; } else { $image = wc_placeholder_img_src(); $image_srcset = false; $image_sizes = false; } if ( $image ) { // Prevent esc_url from breaking spaces in urls for image embeds. // Ref: https://core.trac.wordpress.org/ticket/23605. $image = str_replace( ' ', '%20', $image ); // Add responsive image markup if available. if ( $image_srcset && $image_sizes ) { echo '<img src="' . esc_url( $image ) . '" alt="' . esc_attr( $category->name ) . '" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" srcset="' . esc_attr( $image_srcset ) . '" sizes="' . esc_attr( $image_sizes ) . '" />'; } else { echo '<img src="' . esc_url( $image ) . '" alt="' . esc_attr( $category->name ) . '" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" />'; } } } } if ( ! function_exists( 'woocommerce_order_details_table' ) ) { /** * Displays order details in a table. * * @param mixed $order_id Order ID. */ function woocommerce_order_details_table( $order_id ) { if ( ! $order_id ) { return; } wc_get_template( 'order/order-details.php', array( 'order_id' => $order_id, ) ); } } if ( ! function_exists( 'woocommerce_order_downloads_table' ) ) { /** * Displays order downloads in a table. * * @since 3.2.0 * @param array $downloads Downloads. */ function woocommerce_order_downloads_table( $downloads ) { if ( ! $downloads ) { return; } wc_get_template( 'order/order-downloads.php', array( 'downloads' => $downloads, ) ); } } if ( ! function_exists( 'woocommerce_order_again_button' ) ) { /** * Display an 'order again' button on the view order page. * * @param object $order Order. */ function woocommerce_order_again_button( $order ) { if ( ! $order || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! is_user_logged_in() ) { return; } wc_get_template( 'order/order-again.php', array( 'order' => $order, 'order_again_url' => wp_nonce_url( add_query_arg( 'order_again', $order->get_id(), wc_get_cart_url() ), 'woocommerce-order_again' ), ) ); } } /** Forms */ if ( ! function_exists( 'woocommerce_form_field' ) ) { /** * Outputs a checkout/address form field. * * @param string $key Key. * @param mixed $args Arguments. * @param string $value (default: null). * @return string */ function woocommerce_form_field( $key, $args, $value = null ) { $defaults = array( 'type' => 'text', 'label' => '', 'description' => '', 'placeholder' => '', 'maxlength' => false, 'required' => false, 'autocomplete' => false, 'id' => $key, 'class' => array(), 'label_class' => array(), 'input_class' => array(), 'return' => false, 'options' => array(), 'custom_attributes' => array(), 'validate' => array(), 'default' => '', 'autofocus' => '', 'priority' => '', ); $args = wp_parse_args( $args, $defaults ); $args = apply_filters( 'woocommerce_form_field_args', $args, $key, $value ); if ( $args['required'] ) { $args['class'][] = 'validate-required'; $required = ' <abbr class="required" title="' . esc_attr__( 'required', 'woocommerce' ) . '">*</abbr>'; } else { $required = ' <span class="optional">(' . esc_html__( 'optional', 'woocommerce' ) . ')</span>'; } if ( is_string( $args['label_class'] ) ) { $args['label_class'] = array( $args['label_class'] ); } if ( is_null( $value ) ) { $value = $args['default']; } // Custom attribute handling. $custom_attributes = array(); $args['custom_attributes'] = array_filter( (array) $args['custom_attributes'], 'strlen' ); if ( $args['maxlength'] ) { $args['custom_attributes']['maxlength'] = absint( $args['maxlength'] ); } if ( ! empty( $args['autocomplete'] ) ) { $args['custom_attributes']['autocomplete'] = $args['autocomplete']; } if ( true === $args['autofocus'] ) { $args['custom_attributes']['autofocus'] = 'autofocus'; } if ( $args['description'] ) { $args['custom_attributes']['aria-describedby'] = $args['id'] . '-description'; } if ( ! empty( $args['custom_attributes'] ) && is_array( $args['custom_attributes'] ) ) { foreach ( $args['custom_attributes'] as $attribute => $attribute_value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; } } if ( ! empty( $args['validate'] ) ) { foreach ( $args['validate'] as $validate ) { $args['class'][] = 'validate-' . $validate; } } $field = ''; $label_id = $args['id']; $sort = $args['priority'] ? $args['priority'] : ''; $field_container = '<p class="form-row %1$s" id="%2$s" data-priority="' . esc_attr( $sort ) . '">%3$s</p>'; switch ( $args['type'] ) { case 'country': $countries = 'shipping_country' === $key ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries(); if ( 1 === count( $countries ) ) { $field .= '<strong>' . current( array_values( $countries ) ) . '</strong>'; $field .= '<input type="hidden" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="' . current( array_keys( $countries ) ) . '" ' . implode( ' ', $custom_attributes ) . ' class="country_to_state" readonly="readonly" />'; } else { $data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : ''; $field = '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="country_to_state country_select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ? $args['placeholder'] : esc_attr__( 'Select a country / region…', 'woocommerce' ) ) . '" ' . $data_label . '><option value="">' . esc_html__( 'Select a country / region…', 'woocommerce' ) . '</option>'; foreach ( $countries as $ckey => $cvalue ) { $field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>'; } $field .= '</select>'; $field .= '<noscript><button type="submit" name="woocommerce_checkout_update_totals" value="' . esc_attr__( 'Update country / region', 'woocommerce' ) . '">' . esc_html__( 'Update country / region', 'woocommerce' ) . '</button></noscript>'; } break; case 'state': /* Get country this state field is representing */ $for_country = isset( $args['country'] ) ? $args['country'] : WC()->checkout->get_value( 'billing_state' === $key ? 'billing_country' : 'shipping_country' ); $states = WC()->countries->get_states( $for_country ); if ( is_array( $states ) && empty( $states ) ) { $field_container = '<p class="form-row %1$s" id="%2$s" style="display: none">%3$s</p>'; $field .= '<input type="hidden" class="hidden" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="" ' . implode( ' ', $custom_attributes ) . ' placeholder="' . esc_attr( $args['placeholder'] ) . '" readonly="readonly" data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '"/>'; } elseif ( ! is_null( $for_country ) && is_array( $states ) ) { $data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : ''; $field .= '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="state_select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ? $args['placeholder'] : esc_html__( 'Select an option…', 'woocommerce' ) ) . '" data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . $data_label . '> <option value="">' . esc_html__( 'Select an option…', 'woocommerce' ) . '</option>'; foreach ( $states as $ckey => $cvalue ) { $field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>'; } $field .= '</select>'; } else { $field .= '<input type="text" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $args['placeholder'] ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" ' . implode( ' ', $custom_attributes ) . ' data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '"/>'; } break; case 'textarea': $field .= '<textarea name="' . esc_attr( $key ) . '" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" id="' . esc_attr( $args['id'] ) . '" placeholder="' . esc_attr( $args['placeholder'] ) . '" ' . ( empty( $args['custom_attributes']['rows'] ) ? ' rows="2"' : '' ) . ( empty( $args['custom_attributes']['cols'] ) ? ' cols="5"' : '' ) . implode( ' ', $custom_attributes ) . '>' . esc_textarea( $value ) . '</textarea>'; break; case 'checkbox': $field = '<label class="checkbox ' . implode( ' ', $args['label_class'] ) . '" ' . implode( ' ', $custom_attributes ) . '> <input type="' . esc_attr( $args['type'] ) . '" class="input-checkbox ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="1" ' . checked( $value, 1, false ) . ' /> ' . $args['label'] . $required . '</label>'; break; case 'text': case 'password': case 'datetime': case 'datetime-local': case 'date': case 'month': case 'time': case 'week': case 'number': case 'email': case 'url': case 'tel': $field .= '<input type="' . esc_attr( $args['type'] ) . '" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" placeholder="' . esc_attr( $args['placeholder'] ) . '" value="' . esc_attr( $value ) . '" ' . implode( ' ', $custom_attributes ) . ' />'; break; case 'hidden': $field .= '<input type="' . esc_attr( $args['type'] ) . '" class="input-hidden ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $value ) . '" ' . implode( ' ', $custom_attributes ) . ' />'; break; case 'select': $field = ''; $options = ''; if ( ! empty( $args['options'] ) ) { foreach ( $args['options'] as $option_key => $option_text ) { if ( '' === $option_key ) { // If we have a blank option, select2 needs a placeholder. if ( empty( $args['placeholder'] ) ) { $args['placeholder'] = $option_text ? $option_text : __( 'Choose an option', 'woocommerce' ); } $custom_attributes[] = 'data-allow_clear="true"'; } $options .= '<option value="' . esc_attr( $option_key ) . '" ' . selected( $value, $option_key, false ) . '>' . esc_html( $option_text ) . '</option>'; } $field .= '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ) . '"> ' . $options . ' </select>'; } break; case 'radio': $label_id .= '_' . current( array_keys( $args['options'] ) ); if ( ! empty( $args['options'] ) ) { foreach ( $args['options'] as $option_key => $option_text ) { $field .= '<input type="radio" class="input-radio ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" value="' . esc_attr( $option_key ) . '" name="' . esc_attr( $key ) . '" ' . implode( ' ', $custom_attributes ) . ' id="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '"' . checked( $value, $option_key, false ) . ' />'; $field .= '<label for="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '" class="radio ' . implode( ' ', $args['label_class'] ) . '">' . esc_html( $option_text ) . '</label>'; } } break; } if ( ! empty( $field ) ) { $field_html = ''; if ( $args['label'] && 'checkbox' !== $args['type'] ) { $field_html .= '<label for="' . esc_attr( $label_id ) . '" class="' . esc_attr( implode( ' ', $args['label_class'] ) ) . '">' . wp_kses_post( $args['label'] ) . $required . '</label>'; } $field_html .= '<span class="woocommerce-input-wrapper">' . $field; if ( $args['description'] ) { $field_html .= '<span class="description" id="' . esc_attr( $args['id'] ) . '-description" aria-hidden="true">' . wp_kses_post( $args['description'] ) . '</span>'; } $field_html .= '</span>'; $container_class = esc_attr( implode( ' ', $args['class'] ) ); $container_id = esc_attr( $args['id'] ) . '_field'; $field = sprintf( $field_container, $container_class, $container_id, $field_html ); } /** * Filter by type. */ $field = apply_filters( 'woocommerce_form_field_' . $args['type'], $field, $key, $args, $value ); /** * General filter on form fields. * * @since 3.4.0 */ $field = apply_filters( 'woocommerce_form_field', $field, $key, $args, $value ); if ( $args['return'] ) { return $field; } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $field; } } } if ( ! function_exists( 'get_product_search_form' ) ) { /** * Display product search form. * * Will first attempt to locate the product-searchform.php file in either the child or. * the parent, then load it. If it doesn't exist, then the default search form. * will be displayed. * * The default searchform uses html5. * * @param bool $echo (default: true). * @return string */ function get_product_search_form( $echo = true ) { global $product_search_form_index; ob_start(); if ( empty( $product_search_form_index ) ) { $product_search_form_index = 0; } do_action( 'pre_get_product_search_form' ); wc_get_template( 'product-searchform.php', array( 'index' => $product_search_form_index++, ) ); $form = apply_filters( 'get_product_search_form', ob_get_clean() ); if ( ! $echo ) { return $form; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $form; } } if ( ! function_exists( 'woocommerce_output_auth_header' ) ) { /** * Output the Auth header. */ function woocommerce_output_auth_header() { wc_get_template( 'auth/header.php' ); } } if ( ! function_exists( 'woocommerce_output_auth_footer' ) ) { /** * Output the Auth footer. */ function woocommerce_output_auth_footer() { wc_get_template( 'auth/footer.php' ); } } if ( ! function_exists( 'woocommerce_single_variation' ) ) { /** * Output placeholders for the single variation. */ function woocommerce_single_variation() { echo '<div class="woocommerce-variation single_variation"></div>'; } } if ( ! function_exists( 'woocommerce_single_variation_add_to_cart_button' ) ) { /** * Output the add to cart button for variations. */ function woocommerce_single_variation_add_to_cart_button() { wc_get_template( 'single-product/add-to-cart/variation-add-to-cart-button.php' ); } } if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) { /** * Output a list of variation attributes for use in the cart forms. * * @param array $args Arguments. * @since 2.4.0 */ function wc_dropdown_variation_attribute_options( $args = array() ) { $args = wp_parse_args( apply_filters( 'woocommerce_dropdown_variation_attribute_options_args', $args ), array( 'options' => false, 'attribute' => false, 'product' => false, 'selected' => false, 'name' => '', 'id' => '', 'class' => '', 'show_option_none' => __( 'Choose an option', 'woocommerce' ), ) ); // Get selected value. if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) { $selected_key = 'attribute_' . sanitize_title( $args['attribute'] ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } $options = $args['options']; $product = $args['product']; $attribute = $args['attribute']; $name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute ); $id = $args['id'] ? $args['id'] : sanitize_title( $attribute ); $class = $args['class']; $show_option_none = (bool) $args['show_option_none']; $show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __( 'Choose an option', 'woocommerce' ); // We'll do our best to hide the placeholder, but we'll need to show something when resetting options. if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) { $attributes = $product->get_variation_attributes(); $options = $attributes[ $attribute ]; } $html = '<select id="' . esc_attr( $id ) . '" class="' . esc_attr( $class ) . '" name="' . esc_attr( $name ) . '" data-attribute_name="attribute_' . esc_attr( sanitize_title( $attribute ) ) . '" data-show_option_none="' . ( $show_option_none ? 'yes' : 'no' ) . '">'; $html .= '<option value="">' . esc_html( $show_option_none_text ) . '</option>'; if ( ! empty( $options ) ) { if ( $product && taxonomy_exists( $attribute ) ) { // Get terms if this is a taxonomy - ordered. We need the names too. $terms = wc_get_product_terms( $product->get_id(), $attribute, array( 'fields' => 'all', ) ); foreach ( $terms as $term ) { if ( in_array( $term->slug, $options, true ) ) { $html .= '<option value="' . esc_attr( $term->slug ) . '" ' . selected( sanitize_title( $args['selected'] ), $term->slug, false ) . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $term->name, $term, $attribute, $product ) ) . '</option>'; } } } else { foreach ( $options as $option ) { // This handles < 2.4.0 bw compatibility where text attributes were not sanitized. $selected = sanitize_title( $args['selected'] ) === $args['selected'] ? selected( $args['selected'], sanitize_title( $option ), false ) : selected( $args['selected'], $option, false ); $html .= '<option value="' . esc_attr( $option ) . '" ' . $selected . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute, $product ) ) . '</option>'; } } } $html .= '</select>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args ); } } if ( ! function_exists( 'woocommerce_account_content' ) ) { /** * My Account content output. */ function woocommerce_account_content() { global $wp; if ( ! empty( $wp->query_vars ) ) { foreach ( $wp->query_vars as $key => $value ) { // Ignore pagename param. if ( 'pagename' === $key ) { continue; } if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); return; } } } // No endpoint found? Default to dashboard. wc_get_template( 'myaccount/dashboard.php', array( 'current_user' => get_user_by( 'id', get_current_user_id() ), ) ); } } if ( ! function_exists( 'woocommerce_account_navigation' ) ) { /** * My Account navigation template. */ function woocommerce_account_navigation() { wc_get_template( 'myaccount/navigation.php' ); } } if ( ! function_exists( 'woocommerce_account_orders' ) ) { /** * My Account > Orders template. * * @param int $current_page Current page number. */ function woocommerce_account_orders( $current_page ) { $current_page = empty( $current_page ) ? 1 : absint( $current_page ); $customer_orders = wc_get_orders( apply_filters( 'woocommerce_my_account_my_orders_query', array( 'customer' => get_current_user_id(), 'page' => $current_page, 'paginate' => true, ) ) ); wc_get_template( 'myaccount/orders.php', array( 'current_page' => absint( $current_page ), 'customer_orders' => $customer_orders, 'has_orders' => 0 < $customer_orders->total, ) ); } } if ( ! function_exists( 'woocommerce_account_view_order' ) ) { /** * My Account > View order template. * * @param int $order_id Order ID. */ function woocommerce_account_view_order( $order_id ) { WC_Shortcode_My_Account::view_order( absint( $order_id ) ); } } if ( ! function_exists( 'woocommerce_account_downloads' ) ) { /** * My Account > Downloads template. */ function woocommerce_account_downloads() { wc_get_template( 'myaccount/downloads.php' ); } } if ( ! function_exists( 'woocommerce_account_edit_address' ) ) { /** * My Account > Edit address template. * * @param string $type Address type. */ function woocommerce_account_edit_address( $type ) { $type = wc_edit_address_i18n( sanitize_title( $type ), true ); WC_Shortcode_My_Account::edit_address( $type ); } } if ( ! function_exists( 'woocommerce_account_payment_methods' ) ) { /** * My Account > Downloads template. */ function woocommerce_account_payment_methods() { wc_get_template( 'myaccount/payment-methods.php' ); } } if ( ! function_exists( 'woocommerce_account_add_payment_method' ) ) { /** * My Account > Add payment method template. */ function woocommerce_account_add_payment_method() { WC_Shortcode_My_Account::add_payment_method(); } } if ( ! function_exists( 'woocommerce_account_edit_account' ) ) { /** * My Account > Edit account template. */ function woocommerce_account_edit_account() { WC_Shortcode_My_Account::edit_account(); } } if ( ! function_exists( 'wc_no_products_found' ) ) { /** * Handles the loop when no products were found/no product exist. */ function wc_no_products_found() { wc_get_template( 'loop/no-products-found.php' ); } } if ( ! function_exists( 'wc_get_email_order_items' ) ) { /** * Get HTML for the order items to be shown in emails. * * @param WC_Order $order Order object. * @param array $args Arguments. * * @since 3.0.0 * @return string */ function wc_get_email_order_items( $order, $args = array() ) { ob_start(); $defaults = array( 'show_sku' => false, 'show_image' => false, 'image_size' => array( 32, 32 ), 'plain_text' => false, 'sent_to_admin' => false, ); $args = wp_parse_args( $args, $defaults ); $template = $args['plain_text'] ? 'emails/plain/email-order-items.php' : 'emails/email-order-items.php'; wc_get_template( $template, apply_filters( 'woocommerce_email_order_items_args', array( 'order' => $order, 'items' => $order->get_items(), 'show_download_links' => $order->is_download_permitted() && ! $args['sent_to_admin'], 'show_sku' => $args['show_sku'], 'show_purchase_note' => $order->is_paid() && ! $args['sent_to_admin'], 'show_image' => $args['show_image'], 'image_size' => $args['image_size'], 'plain_text' => $args['plain_text'], 'sent_to_admin' => $args['sent_to_admin'], ) ) ); return apply_filters( 'woocommerce_email_order_items_table', ob_get_clean(), $order ); } } if ( ! function_exists( 'wc_display_item_meta' ) ) { /** * Display item meta data. * * @since 3.0.0 * @param WC_Order_Item $item Order Item. * @param array $args Arguments. * @return string|void */ function wc_display_item_meta( $item, $args = array() ) { $strings = array(); $html = ''; $args = wp_parse_args( $args, array( 'before' => '<ul class="wc-item-meta"><li>', 'after' => '</li></ul>', 'separator' => '</li><li>', 'echo' => true, 'autop' => false, 'label_before' => '<strong class="wc-item-meta-label">', 'label_after' => ':</strong> ', ) ); foreach ( $item->get_formatted_meta_data() as $meta_id => $meta ) { $value = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) ); $strings[] = $args['label_before'] . wp_kses_post( $meta->display_key ) . $args['label_after'] . $value; } if ( $strings ) { $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; } $html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args ); if ( $args['echo'] ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $html; } else { return $html; } } } if ( ! function_exists( 'wc_display_item_downloads' ) ) { /** * Display item download links. * * @since 3.0.0 * @param WC_Order_Item $item Order Item. * @param array $args Arguments. * @return string|void */ function wc_display_item_downloads( $item, $args = array() ) { $strings = array(); $html = ''; $args = wp_parse_args( $args, array( 'before' => '<ul class ="wc-item-downloads"><li>', 'after' => '</li></ul>', 'separator' => '</li><li>', 'echo' => true, 'show_url' => false, ) ); $downloads = is_object( $item ) && $item->is_type( 'line_item' ) ? $item->get_item_downloads() : array(); if ( $downloads ) { $i = 0; foreach ( $downloads as $file ) { $i ++; if ( $args['show_url'] ) { $strings[] = '<strong class="wc-item-download-label">' . esc_html( $file['name'] ) . ':</strong> ' . esc_html( $file['download_url'] ); } else { /* translators: %d: downloads count */ $prefix = count( $downloads ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' ); $strings[] = '<strong class="wc-item-download-label">' . $prefix . ':</strong> <a href="' . esc_url( $file['download_url'] ) . '" target="_blank">' . esc_html( $file['name'] ) . '</a>'; } } } if ( $strings ) { $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; } $html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args ); if ( $args['echo'] ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $html; } else { return $html; } } } if ( ! function_exists( 'woocommerce_photoswipe' ) ) { /** * Get the shop sidebar template. */ function woocommerce_photoswipe() { if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { wc_get_template( 'single-product/photoswipe.php' ); } } } /** * Outputs a list of product attributes for a product. * * @since 3.0.0 * @param WC_Product $product Product Object. */ function wc_display_product_attributes( $product ) { $product_attributes = array(); // Display weight and dimensions before attribute list. $display_dimensions = apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ); if ( $display_dimensions && $product->has_weight() ) { $product_attributes['weight'] = array( 'label' => __( 'Weight', 'woocommerce' ), 'value' => wc_format_weight( $product->get_weight() ), ); } if ( $display_dimensions && $product->has_dimensions() ) { $product_attributes['dimensions'] = array( 'label' => __( 'Dimensions', 'woocommerce' ), 'value' => wc_format_dimensions( $product->get_dimensions( false ) ), ); } // Add product attributes to list. $attributes = array_filter( $product->get_attributes(), 'wc_attributes_array_filter_visible' ); foreach ( $attributes as $attribute ) { $values = array(); if ( $attribute->is_taxonomy() ) { $attribute_taxonomy = $attribute->get_taxonomy_object(); $attribute_values = wc_get_product_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'all' ) ); foreach ( $attribute_values as $attribute_value ) { $value_name = esc_html( $attribute_value->name ); if ( $attribute_taxonomy->attribute_public ) { $values[] = '<a href="' . esc_url( get_term_link( $attribute_value->term_id, $attribute->get_name() ) ) . '" rel="tag">' . $value_name . '</a>'; } else { $values[] = $value_name; } } } else { $values = $attribute->get_options(); foreach ( $values as &$value ) { $value = make_clickable( esc_html( $value ) ); } } $product_attributes[ 'attribute_' . sanitize_title_with_dashes( $attribute->get_name() ) ] = array( 'label' => wc_attribute_label( $attribute->get_name() ), 'value' => apply_filters( 'woocommerce_attribute', wpautop( wptexturize( implode( ', ', $values ) ) ), $attribute, $values ), ); } /** * Hook: woocommerce_display_product_attributes. * * @since 3.6.0. * @param array $product_attributes Array of atributes to display; label, value. * @param WC_Product $product Showing attributes for this product. */ $product_attributes = apply_filters( 'woocommerce_display_product_attributes', $product_attributes, $product ); wc_get_template( 'single-product/product-attributes.php', array( 'product_attributes' => $product_attributes, // Legacy params. 'product' => $product, 'attributes' => $attributes, 'display_dimensions' => $display_dimensions, ) ); } /** * Get HTML to show product stock. * * @since 3.0.0 * @param WC_Product $product Product Object. * @return string */ function wc_get_stock_html( $product ) { $html = ''; $availability = $product->get_availability(); if ( ! empty( $availability['availability'] ) ) { ob_start(); wc_get_template( 'single-product/stock.php', array( 'product' => $product, 'class' => $availability['class'], 'availability' => $availability['availability'], ) ); $html = ob_get_clean(); } if ( has_filter( 'woocommerce_stock_html' ) ) { wc_deprecated_function( 'The woocommerce_stock_html filter', '', 'woocommerce_get_stock_html' ); $html = apply_filters( 'woocommerce_stock_html', $html, $availability['availability'], $product ); } return apply_filters( 'woocommerce_get_stock_html', $html, $product ); } /** * Get HTML for ratings. * * @since 3.0.0 * @param float $rating Rating being shown. * @param int $count Total number of ratings. * @return string */ function wc_get_rating_html( $rating, $count = 0 ) { $html = ''; if ( 0 < $rating ) { /* translators: %s: rating */ $label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ); $html = '<div class="star-rating" role="img" aria-label="' . esc_attr( $label ) . '">' . wc_get_star_rating_html( $rating, $count ) . '</div>'; } return apply_filters( 'woocommerce_product_get_rating_html', $html, $rating, $count ); } /** * Get HTML for star rating. * * @since 3.1.0 * @param float $rating Rating being shown. * @param int $count Total number of ratings. * @return string */ function wc_get_star_rating_html( $rating, $count = 0 ) { $html = '<span style="width:' . ( ( $rating / 5 ) * 100 ) . '%">'; if ( 0 < $count ) { /* translators: 1: rating 2: rating count */ $html .= sprintf( _n( 'Rated %1$s out of 5 based on %2$s customer rating', 'Rated %1$s out of 5 based on %2$s customer ratings', $count, 'woocommerce' ), '<strong class="rating">' . esc_html( $rating ) . '</strong>', '<span class="rating">' . esc_html( $count ) . '</span>' ); } else { /* translators: %s: rating */ $html .= sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), '<strong class="rating">' . esc_html( $rating ) . '</strong>' ); } $html .= '</span>'; return apply_filters( 'woocommerce_get_star_rating_html', $html, $rating, $count ); } /** * Returns a 'from' prefix if you want to show where prices start at. * * @since 3.0.0 * @return string */ function wc_get_price_html_from_text() { return apply_filters( 'woocommerce_get_price_html_from_text', '<span class="from">' . _x( 'From:', 'min_price', 'woocommerce' ) . ' </span>' ); } /** * Get logout endpoint. * * @since 2.6.9 * * @param string $redirect Redirect URL. * * @return string */ function wc_logout_url( $redirect = '' ) { $redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ); if ( get_option( 'woocommerce_logout_endpoint' ) ) { return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' ); } return wp_logout_url( $redirect ); } /** * Show notice if cart is empty. * * @since 3.1.0 */ function wc_empty_cart_message() { echo '<p class="cart-empty woocommerce-info">' . wp_kses_post( apply_filters( 'wc_empty_cart_message', __( 'Your cart is currently empty.', 'woocommerce' ) ) ) . '</p>'; } /** * Disable search engines indexing core, dynamic, cart/checkout pages. * * @todo Deprecated this function after dropping support for WP 5.6. * @since 3.2.0 */ function wc_page_noindex() { // wp_no_robots is deprecated since WP 5.7. if ( function_exists( 'wp_robots_no_robots' ) ) { return; } if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { wp_no_robots(); } } add_action( 'wp_head', 'wc_page_noindex' ); /** * Disable search engines indexing core, dynamic, cart/checkout pages. * Uses "wp_robots" filter introduced in WP 5.7. * * @since 5.0.0 * @param array $robots Associative array of robots directives. * @return array Filtered robots directives. */ function wc_page_no_robots( $robots ) { if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { return wp_robots_no_robots( $robots ); } return $robots; } add_filter( 'wp_robots', 'wc_page_no_robots' ); /** * Get a slug identifying the current theme. * * @since 3.3.0 * @return string */ function wc_get_theme_slug_for_templates() { return apply_filters( 'woocommerce_theme_slug_for_templates', get_option( 'template' ) ); } /** * Gets and formats a list of cart item data + variations for display on the frontend. * * @since 3.3.0 * @param array $cart_item Cart item object. * @param bool $flat Should the data be returned flat or in a list. * @return string */ function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) { $item_data = array(); // Variation values are shown only if they are not found in the title as of 3.0. // This is because variation titles display the attributes. if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) { foreach ( $cart_item['variation'] as $name => $value ) { $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); if ( taxonomy_exists( $taxonomy ) ) { // If this is a term slug, get the term's nice name. $term = get_term_by( 'slug', $value, $taxonomy ); if ( ! is_wp_error( $term ) && $term && $term->name ) { $value = $term->name; } $label = wc_attribute_label( $taxonomy ); } else { // If this is a custom option slug, get the options name. $value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $cart_item['data'] ); $label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] ); } // Check the nicename against the title. if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) { continue; } $item_data[] = array( 'key' => $label, 'value' => $value, ); } } // Filter item data to allow 3rd parties to add more to the array. $item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item ); // Format item data ready to display. foreach ( $item_data as $key => $data ) { // Set hidden to true to not display meta on cart. if ( ! empty( $data['hidden'] ) ) { unset( $item_data[ $key ] ); continue; } $item_data[ $key ]['key'] = ! empty( $data['key'] ) ? $data['key'] : $data['name']; $item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value']; } // Output flat or in list format. if ( count( $item_data ) > 0 ) { ob_start(); if ( $flat ) { foreach ( $item_data as $data ) { echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n"; } } else { wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); } return ob_get_clean(); } return ''; } /** * Gets the url to remove an item from the cart. * * @since 3.3.0 * @param string $cart_item_key contains the id of the cart item. * @return string url to page */ function wc_get_cart_remove_url( $cart_item_key ) { $cart_page_url = wc_get_cart_url(); return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ); } /** * Gets the url to re-add an item into the cart. * * @since 3.3.0 * @param string $cart_item_key Cart item key to undo. * @return string url to page */ function wc_get_cart_undo_url( $cart_item_key ) { $cart_page_url = wc_get_cart_url(); $query_args = array( 'undo_item' => $cart_item_key, ); return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key ); } /** * Outputs all queued notices on WC pages. * * @since 3.5.0 */ function woocommerce_output_all_notices() { echo '<div class="woocommerce-notices-wrapper">'; wc_print_notices(); echo '</div>'; } /** * Products RSS Feed. * * @deprecated 2.6 */ function wc_products_rss_feed() { wc_deprecated_function( 'wc_products_rss_feed', '2.6' ); } if ( ! function_exists( 'woocommerce_reset_loop' ) ) { /** * Reset the loop's index and columns when we're done outputting a product loop. * * @deprecated 3.3 */ function woocommerce_reset_loop() { wc_reset_loop(); } } if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) { /** * Output the reviews tab content. * * @deprecated 2.4.0 Unused. */ function woocommerce_product_reviews_tab() { wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' ); } } /** * Display pay buttons HTML. * * @since 3.9.0 */ function wc_get_pay_buttons() { $supported_gateways = array(); $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); foreach ( $available_gateways as $gateway ) { if ( $gateway->supports( 'pay_button' ) ) { $supported_gateways[] = $gateway->get_pay_button_id(); } } if ( ! $supported_gateways ) { return; } echo '<div class="woocommerce-pay-buttons">'; foreach ( $supported_gateways as $pay_button_id ) { echo sprintf( '<div class="woocommerce-pay-button__%1$s %1$s" id="%1$s"></div>', esc_attr( $pay_button_id ) ); } echo '</div>'; } // phpcs:enable Generic.Commenting.Todo.TaskFound includes/wc-user-functions.php 0000644 00000065152 15132754524 0012477 0 ustar 00 <?php /** * WooCommerce Customer Functions * * Functions for customers. * * @package WooCommerce\Functions * @version 2.2.0 */ defined( 'ABSPATH' ) || exit; /** * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from seeing the admin bar. * * Note: get_option( 'woocommerce_lock_down_admin', true ) is a deprecated option here for backwards compatibility. Defaults to true. * * @param bool $show_admin_bar If should display admin bar. * @return bool */ function wc_disable_admin_bar( $show_admin_bar ) { if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_woocommerce' ) ) ) { $show_admin_bar = false; } return $show_admin_bar; } add_filter( 'show_admin_bar', 'wc_disable_admin_bar', 10, 1 ); // phpcs:ignore WordPress.VIP.AdminBarRemoval.RemovalDetected if ( ! function_exists( 'wc_create_new_customer' ) ) { /** * Create a new customer. * * @param string $email Customer email. * @param string $username Customer username. * @param string $password Customer password. * @param array $args List of arguments to pass to `wp_insert_user()`. * @return int|WP_Error Returns WP_Error on failure, Int (user ID) on success. */ function wc_create_new_customer( $email, $username = '', $password = '', $args = array() ) { if ( empty( $email ) || ! is_email( $email ) ) { return new WP_Error( 'registration-error-invalid-email', __( 'Please provide a valid email address.', 'woocommerce' ) ); } if ( email_exists( $email ) ) { return new WP_Error( 'registration-error-email-exists', apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ), $email ) ); } if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) { $username = wc_create_new_customer_username( $email, $args ); } $username = sanitize_user( $username ); if ( empty( $username ) || ! validate_username( $username ) ) { return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) ); } if ( username_exists( $username ) ) { return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) ); } // Handle password creation. $password_generated = false; if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) { $password = wp_generate_password(); $password_generated = true; } if ( empty( $password ) ) { return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) ); } // Use WP_Error to handle registration errors. $errors = new WP_Error(); do_action( 'woocommerce_register_post', $username, $email, $errors ); $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email ); if ( $errors->get_error_code() ) { return $errors; } $new_customer_data = apply_filters( 'woocommerce_new_customer_data', array_merge( $args, array( 'user_login' => $username, 'user_pass' => $password, 'user_email' => $email, 'role' => 'customer', ) ) ); $customer_id = wp_insert_user( $new_customer_data ); if ( is_wp_error( $customer_id ) ) { return $customer_id; } do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); return $customer_id; } } /** * Create a unique username for a new customer. * * @since 3.6.0 * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. * @return string Generated username. */ function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) { $username_parts = array(); if ( isset( $new_user_args['first_name'] ) ) { $username_parts[] = sanitize_user( $new_user_args['first_name'], true ); } if ( isset( $new_user_args['last_name'] ) ) { $username_parts[] = sanitize_user( $new_user_args['last_name'], true ); } // Remove empty parts. $username_parts = array_filter( $username_parts ); // If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email. if ( empty( $username_parts ) ) { $email_parts = explode( '@', $email ); $email_username = $email_parts[0]; // Exclude common prefixes. if ( in_array( $email_username, array( 'sales', 'hello', 'mail', 'contact', 'info', ), true ) ) { // Get the domain part. $email_username = $email_parts[1]; } $username_parts[] = sanitize_user( $email_username, true ); } $username = wc_strtolower( implode( '.', $username_parts ) ); if ( $suffix ) { $username .= $suffix; } /** * WordPress 4.4 - filters the list of blocked usernames. * * @since 3.7.0 * @param array $usernames Array of blocked usernames. */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); // Stop illegal logins and generate a new random username. if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { $new_args = array(); /** * Filter generated customer username. * * @since 3.7.0 * @param string $username Generated username. * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. */ $new_args['first_name'] = apply_filters( 'woocommerce_generated_customer_username', 'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ), $email, $new_user_args, $suffix ); return wc_create_new_customer_username( $email, $new_args, $suffix ); } if ( username_exists( $username ) ) { // Generate something unique to append to the username in case of a conflict with another user. $suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 ); return wc_create_new_customer_username( $email, $new_user_args, $suffix ); } /** * Filter new customer username. * * @since 3.7.0 * @param string $username Customer username. * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. */ return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix ); } /** * Login a customer (set auth cookie and set global user object). * * @param int $customer_id Customer ID. */ function wc_set_customer_auth_cookie( $customer_id ) { wp_set_current_user( $customer_id ); wp_set_auth_cookie( $customer_id, true ); // Update session. WC()->session->init_session_cookie(); } /** * Get past orders (by email) and update them. * * @param int $customer_id Customer ID. * @return int */ function wc_update_new_customer_past_orders( $customer_id ) { $linked = 0; $complete = 0; $customer = get_user_by( 'id', absint( $customer_id ) ); $customer_orders = wc_get_orders( array( 'limit' => -1, 'customer' => array( array( 0, $customer->user_email ) ), 'return' => 'ids', ) ); if ( ! empty( $customer_orders ) ) { foreach ( $customer_orders as $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { continue; } $order->set_customer_id( $customer->ID ); $order->save(); if ( $order->has_downloadable_item() ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->delete_by_order_id( $order->get_id() ); wc_downloadable_product_permissions( $order->get_id(), true ); } do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer ); if ( get_post_status( $order_id ) === 'wc-completed' ) { $complete++; } $linked++; } } if ( $complete ) { update_user_meta( $customer_id, 'paying_customer', 1 ); update_user_meta( $customer_id, '_order_count', '' ); update_user_meta( $customer_id, '_money_spent', '' ); delete_user_meta( $customer_id, '_last_order' ); } return $linked; } /** * Order payment completed - This is a paying customer. * * @param int $order_id Order ID. */ function wc_paying_customer( $order_id ) { $order = wc_get_order( $order_id ); $customer_id = $order->get_customer_id(); if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) { $customer = new WC_Customer( $customer_id ); if ( ! $customer->get_is_paying_customer() ) { $customer->set_is_paying_customer( true ); $customer->save(); } } } add_action( 'woocommerce_payment_complete', 'wc_paying_customer' ); add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' ); /** * Checks if a user (by email or ID or both) has bought an item. * * @param string $customer_email Customer email to check. * @param int $user_id User ID to check. * @param int $product_id Product ID to check. * @return bool */ function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { global $wpdb; $result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id ); if ( null !== $result ) { return $result; } $transient_name = 'wc_customer_bought_product_' . md5( $customer_email . $user_id ); $transient_version = WC_Cache_Helper::get_transient_version( 'orders' ); $transient_value = get_transient( $transient_name ); if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { $result = $transient_value['value']; } else { $customer_data = array( $user_id ); if ( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( isset( $user->user_email ) ) { $customer_data[] = $user->user_email; } } if ( is_email( $customer_email ) ) { $customer_data[] = $customer_email; } $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); if ( count( $customer_data ) === 0 ) { return false; } $result = $wpdb->get_col( " SELECT im.meta_value FROM {$wpdb->posts} AS p INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) AND pm.meta_key IN ( '_billing_email', '_customer_user' ) AND im.meta_key IN ( '_product_id', '_variation_id' ) AND im.meta_value != 0 AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) " ); // WPCS: unprepared SQL ok. $result = array_map( 'absint', $result ); $transient_value = array( 'version' => $transient_version, 'value' => $result, ); set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); } return in_array( absint( $product_id ), $result, true ); } /** * Checks if the current user has a role. * * @param string $role The role. * @return bool */ function wc_current_user_has_role( $role ) { return wc_user_has_role( wp_get_current_user(), $role ); } /** * Checks if a user has a role. * * @param int|\WP_User $user The user. * @param string $role The role. * @return bool */ function wc_user_has_role( $user, $role ) { if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } return in_array( $role, $user->roles, true ); } /** * Checks if a user has a certain capability. * * @param array $allcaps All capabilities. * @param array $caps Capabilities. * @param array $args Arguments. * * @return array The filtered array of all capabilities. */ function wc_customer_has_capability( $allcaps, $caps, $args ) { if ( isset( $caps[0] ) ) { switch ( $caps[0] ) { case 'view_order': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['view_order'] = true; } break; case 'pay_for_order': $user_id = intval( $args[1] ); $order_id = isset( $args[2] ) ? $args[2] : null; // When no order ID, we assume it's a new order // and thus, customer can pay for it. if ( ! $order_id ) { $allcaps['pay_for_order'] = true; break; } $order = wc_get_order( $order_id ); if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) { $allcaps['pay_for_order'] = true; } break; case 'order_again': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['order_again'] = true; } break; case 'cancel_order': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['cancel_order'] = true; } break; case 'download_file': $user_id = intval( $args[1] ); $download = $args[2]; if ( $download && $user_id === $download->get_user_id() ) { $allcaps['download_file'] = true; } break; } } return $allcaps; } add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 ); /** * Safe way of allowing shop managers restricted capabilities that will remove * access to the capabilities if WooCommerce is deactivated. * * @since 3.5.4 * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values * represent whether the user has that capability. * @param string[] $caps Required primitive capabilities for the requested capability. * @param array $args Arguments that accompany the requested capability check. * @param WP_User $user The user object. * @return bool[] */ function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) { if ( wc_user_has_role( $user, 'shop_manager' ) ) { // @see wc_modify_map_meta_cap, which limits editing to customers. $allcaps['edit_users'] = true; } return $allcaps; } add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 ); /** * Modify the list of editable roles to prevent non-admin adding admin users. * * @param array $roles Roles. * @return array */ function wc_modify_editable_roles( $roles ) { if ( is_multisite() && is_super_admin() ) { return $roles; } if ( ! wc_current_user_has_role( 'administrator' ) ) { unset( $roles['administrator'] ); if ( wc_current_user_has_role( 'shop_manager' ) ) { $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) ); } } return $roles; } add_filter( 'editable_roles', 'wc_modify_editable_roles' ); /** * Modify capabilities to prevent non-admin users editing admin users. * * $args[0] will be the user being edited in this case. * * @param array $caps Array of caps. * @param string $cap Name of the cap we are checking. * @param int $user_id ID of the user being checked against. * @param array $args Arguments. * @return array */ function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) { if ( is_multisite() && is_super_admin() ) { return $caps; } switch ( $cap ) { case 'edit_user': case 'remove_user': case 'promote_user': case 'delete_user': if ( ! isset( $args[0] ) || $args[0] === $user_id ) { break; } else { if ( ! wc_current_user_has_role( 'administrator' ) ) { if ( wc_user_has_role( $args[0], 'administrator' ) ) { $caps[] = 'do_not_allow'; } elseif ( wc_current_user_has_role( 'shop_manager' ) ) { // Shop managers can only edit customer info. $userdata = get_userdata( $args[0] ); $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) { $caps[] = 'do_not_allow'; } } } } break; } return $caps; } add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 ); /** * Get customer download permissions from the database. * * @param int $customer_id Customer/User ID. * @return array */ function wc_get_customer_download_permissions( $customer_id ) { $data_store = WC_Data_Store::load( 'customer-download' ); return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); } /** * Get customer available downloads. * * @param int $customer_id Customer/User ID. * @return array */ function wc_get_customer_available_downloads( $customer_id ) { $downloads = array(); $_product = null; $order = null; $file_number = 0; // Get results from valid orders only. $results = wc_get_customer_download_permissions( $customer_id ); if ( $results ) { foreach ( $results as $result ) { $order_id = intval( $result->order_id ); if ( ! $order || $order->get_id() !== $order_id ) { // New order. $order = wc_get_order( $order_id ); $_product = null; } // Make sure the order exists for this download. if ( ! $order ) { continue; } // Check if downloads are permitted. if ( ! $order->is_download_permitted() ) { continue; } $product_id = intval( $result->product_id ); if ( ! $_product || $_product->get_id() !== $product_id ) { // New product. $file_number = 0; $_product = wc_get_product( $product_id ); } // Check product exists and has the file. if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) { continue; } $download_file = $_product->get_file( $result->download_id ); // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. $download_name = apply_filters( 'woocommerce_downloadable_product_name', $download_file['name'], $_product, $result->download_id, $file_number ); $downloads[] = array( 'download_url' => add_query_arg( array( 'download_file' => $product_id, 'order' => $result->order_key, 'email' => rawurlencode( $result->user_email ), 'key' => $result->download_id, ), home_url( '/' ) ), 'download_id' => $result->download_id, 'product_id' => $_product->get_id(), 'product_name' => $_product->get_name(), 'product_url' => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0. 'download_name' => $download_name, 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key(), 'downloads_remaining' => $result->downloads_remaining, 'access_expires' => $result->access_expires, 'file' => array( 'name' => $download_file->get_name(), 'file' => $download_file->get_file(), ), ); $file_number++; } } return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id ); } /** * Get total spent by customer. * * @param int $user_id User ID. * @return string */ function wc_get_customer_total_spent( $user_id ) { $customer = new WC_Customer( $user_id ); return $customer->get_total_spent(); } /** * Get total orders by customer. * * @param int $user_id User ID. * @return int */ function wc_get_customer_order_count( $user_id ) { $customer = new WC_Customer( $user_id ); return $customer->get_order_count(); } /** * Reset _customer_user on orders when a user is deleted. * * @param int $user_id User ID. */ function wc_reset_order_customer_id_on_deleted_user( $user_id ) { global $wpdb; $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 0, ), array( 'meta_key' => '_customer_user', 'meta_value' => $user_id, ) ); // WPCS: slow query ok. } add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' ); /** * Get review verification status. * * @param int $comment_id Comment ID. * @return bool */ function wc_review_is_from_verified_owner( $comment_id ) { $verified = get_comment_meta( $comment_id, 'verified', true ); return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified; } /** * Disable author archives for customers. * * @since 2.5.0 */ function wc_disable_author_archives_for_customers() { global $author; if ( is_author() ) { $user = get_user_by( 'id', $author ); if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) { wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); exit; } } } add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' ); /** * Hooks into the `profile_update` hook to set the user last updated timestamp. * * @since 2.6.0 * @param int $user_id The user that was updated. * @param array $old The profile fields pre-change. */ function wc_update_profile_last_update_time( $user_id, $old ) { wc_set_user_last_update_time( $user_id ); } add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 ); /** * Hooks into the update user meta function to set the user last updated timestamp. * * @since 2.6.0 * @param int $meta_id ID of the meta object that was changed. * @param int $user_id The user that was updated. * @param string $meta_key Name of the meta key that was changed. * @param string $_meta_value Value of the meta that was changed. */ function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) { $keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); $update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false; $update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time; $update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time; if ( $update_time ) { wc_set_user_last_update_time( $user_id ); } } add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 ); /** * Sets a user's "last update" time to the current timestamp. * * @since 2.6.0 * @param int $user_id The user to set a timestamp for. */ function wc_set_user_last_update_time( $user_id ) { update_user_meta( $user_id, 'last_update', gmdate( 'U' ) ); } /** * Get customer saved payment methods list. * * @since 2.6.0 * @param int $customer_id Customer ID. * @return array */ function wc_get_customer_saved_methods_list( $customer_id ) { return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); } /** * Get info about customer's last order. * * @since 2.6.0 * @param int $customer_id Customer ID. * @return WC_Order|bool Order object if successful or false. */ function wc_get_customer_last_order( $customer_id ) { $customer = new WC_Customer( $customer_id ); return $customer->get_last_order(); } /** * Add support for searching by display_name. * * @since 3.2.0 * @param array $search_columns Column names. * @return array */ function wc_user_search_columns( $search_columns ) { $search_columns[] = 'display_name'; return $search_columns; } add_filter( 'user_search_columns', 'wc_user_search_columns' ); /** * When a user is deleted in WordPress, delete corresponding WooCommerce data. * * @param int $user_id User ID being deleted. */ function wc_delete_user_data( $user_id ) { global $wpdb; // Clean up sessions. $wpdb->delete( $wpdb->prefix . 'woocommerce_sessions', array( 'session_key' => $user_id, ) ); // Revoke API keys. $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'user_id' => $user_id, ) ); // Clean up payment tokens. $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id ); foreach ( $payment_tokens as $payment_token ) { $payment_token->delete(); } } add_action( 'delete_user', 'wc_delete_user_data' ); /** * Store user agents. Used for tracker. * * @since 3.0.0 * @param string $user_login User login. * @param int|object $user User. */ function wc_maybe_store_user_agent( $user_login, $user ) { if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) { $admin_user_agents = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); $admin_user_agents[] = wc_get_user_agent(); update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ) ); } } add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 ); /** * Update logic triggered on login. * * @since 3.4.0 * @param string $user_login User login. * @param object $user User. */ function wc_user_logged_in( $user_login, $user ) { wc_update_user_last_active( $user->ID ); update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 ); } add_action( 'wp_login', 'wc_user_logged_in', 10, 2 ); /** * Update when the user was last active. * * @since 3.4.0 */ function wc_current_user_is_active() { if ( ! is_user_logged_in() ) { return; } wc_update_user_last_active( get_current_user_id() ); } add_action( 'wp', 'wc_current_user_is_active', 10 ); /** * Set the user last active timestamp to now. * * @since 3.4.0 * @param int $user_id User ID to mark active. */ function wc_update_user_last_active( $user_id ) { if ( ! $user_id ) { return; } update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) ); } /** * Translate WC roles using the woocommerce textdomain. * * @since 3.7.0 * @param string $translation Translated text. * @param string $text Text to translate. * @param string $context Context information for the translators. * @param string $domain Text domain. Unique identifier for retrieving translated strings. * @return string */ function wc_translate_user_roles( $translation, $text, $context, $domain ) { // translate_user_role() only accepts a second parameter starting in WP 5.2. if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { return $translation; } if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) { return translate_user_role( $text, 'woocommerce' ); } return $translation; } add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 ); includes/class-wc-tracker.php 0000644 00000056516 15132754524 0012255 0 ustar 00 <?php /** * WooCommerce Tracker * * The WooCommerce tracker class adds functionality to track WooCommerce usage based on if the customer opted in. * No personal information is tracked, only general WooCommerce settings, general product, order and user counts and admin email for discount code. * * @class WC_Tracker * @since 2.3.0 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WooCommerce Tracker Class */ class WC_Tracker { /** * URL to the WooThemes Tracker API endpoint. * * @var string */ private static $api_url = 'https://tracking.woocommerce.com/v1/'; /** * Hook into cron event. */ public static function init() { add_action( 'woocommerce_tracker_send_event', array( __CLASS__, 'send_tracking_data' ) ); } /** * Decide whether to send tracking data or not. * * @param boolean $override Should override?. */ public static function send_tracking_data( $override = false ) { // Don't trigger this on AJAX Requests. if ( Constants::is_true( 'DOING_AJAX' ) ) { return; } if ( ! apply_filters( 'woocommerce_tracker_send_override', $override ) ) { // Send a maximum of once per week by default. $last_send = self::get_last_send_time(); if ( $last_send && $last_send > apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { return; } } else { // Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links. $last_send = self::get_last_send_time(); if ( $last_send && $last_send > strtotime( '-1 hours' ) ) { return; } } // Update time first before sending to ensure it is set. update_option( 'woocommerce_tracker_last_send', time() ); $params = self::get_tracking_data(); wp_safe_remote_post( self::$api_url, array( 'method' => 'POST', 'timeout' => 45, 'redirection' => 5, 'httpversion' => '1.0', 'blocking' => false, 'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ), 'body' => wp_json_encode( $params ), 'cookies' => array(), ) ); } /** * Get the last time tracking data was sent. * * @return int|bool */ private static function get_last_send_time() { return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) ); } /** * Test whether this site is a staging site according to the Jetpack criteria. * * With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated. * \Automattic\Jetpack\Status::is_staging_site is the replacement. * However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method, * so with those, code still needs to use the previous check as a fallback. * * @return bool */ private static function is_jetpack_staging_site() { if ( class_exists( '\Automattic\Jetpack\Status' ) ) { // Preferred way of checking with Jetpack 8.1+. $jp_status = new \Automattic\Jetpack\Status(); if ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) { return $jp_status->is_staging_site(); } } return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() ); } /** * Get all the tracking data. * * @return array */ public static function get_tracking_data() { $data = array(); // General site info. $data['url'] = home_url(); $data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) ); $data['theme'] = self::get_theme_info(); // WordPress Info. $data['wp'] = self::get_wordpress_info(); // Server Info. $data['server'] = self::get_server_info(); // Plugin info. $all_plugins = self::get_all_plugins(); $data['active_plugins'] = $all_plugins['active_plugins']; $data['inactive_plugins'] = $all_plugins['inactive_plugins']; // Jetpack & WooCommerce Connect. $data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none'; $data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no'; $data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no'; $data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no'; $data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no'; $data['helper_connected'] = self::get_helper_connected(); // Store count info. $data['users'] = self::get_user_counts(); $data['products'] = self::get_product_counts(); $data['orders'] = self::get_orders(); $data['reviews'] = self::get_review_counts(); $data['categories'] = self::get_category_counts(); // Payment gateway info. $data['gateways'] = self::get_active_payment_gateways(); // Shipping method info. $data['shipping_methods'] = self::get_active_shipping_methods(); // Get all WooCommerce options info. $data['settings'] = self::get_all_woocommerce_options_values(); // Template overrides. $data['template_overrides'] = self::get_all_template_overrides(); // Cart & checkout tech (blocks or shortcodes). $data['cart_checkout'] = self::get_cart_checkout_info(); // WooCommerce Admin info. $data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no'; // Mobile info. $data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage(); return apply_filters( 'woocommerce_tracker_data', $data ); } /** * Get the current theme info, theme name and version. * * @return array */ public static function get_theme_info() { $theme_data = wp_get_theme(); $theme_child_theme = wc_bool_to_string( is_child_theme() ); $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); return array( 'name' => $theme_data->Name, // @phpcs:ignore 'version' => $theme_data->Version, // @phpcs:ignore 'child_theme' => $theme_child_theme, 'wc_support' => $theme_wc_support, ); } /** * Get WordPress related data. * * @return array */ private static function get_wordpress_info() { $wp_data = array(); $memory = wc_let_to_num( WP_MEMORY_LIMIT ); if ( function_exists( 'memory_get_usage' ) ) { $system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) ); $memory = max( $memory, $system_memory ); } // WordPress 5.5+ environment type specification. // 'production' is the default in WP, thus using it as a default here, too. $environment_type = 'production'; if ( function_exists( 'wp_get_environment_type' ) ) { $environment_type = wp_get_environment_type(); } $wp_data['memory_limit'] = size_format( $memory ); $wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No'; $wp_data['locale'] = get_locale(); $wp_data['version'] = get_bloginfo( 'version' ); $wp_data['multisite'] = is_multisite() ? 'Yes' : 'No'; $wp_data['env_type'] = $environment_type; return $wp_data; } /** * Get server related info. * * @return array */ private static function get_server_info() { $server_data = array(); if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { $server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore } if ( function_exists( 'phpversion' ) ) { $server_data['php_version'] = phpversion(); } if ( function_exists( 'ini_get' ) ) { $server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) ); $server_data['php_time_limt'] = ini_get( 'max_execution_time' ); $server_data['php_max_input_vars'] = ini_get( 'max_input_vars' ); $server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No'; } $database_version = wc_get_server_database_version(); $server_data['mysql_version'] = $database_version['number']; $server_data['php_max_upload_size'] = size_format( wp_max_upload_size() ); $server_data['php_default_timezone'] = date_default_timezone_get(); $server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No'; $server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No'; $server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No'; return $server_data; } /** * Get all plugins grouped into activated or not. * * @return array */ private static function get_all_plugins() { // Ensure get_plugins function is loaded. if ( ! function_exists( 'get_plugins' ) ) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $active_plugins_keys = get_option( 'active_plugins', array() ); $active_plugins = array(); foreach ( $plugins as $k => $v ) { // Take care of formatting the data how we want it. $formatted = array(); $formatted['name'] = strip_tags( $v['Name'] ); if ( isset( $v['Version'] ) ) { $formatted['version'] = strip_tags( $v['Version'] ); } if ( isset( $v['Author'] ) ) { $formatted['author'] = strip_tags( $v['Author'] ); } if ( isset( $v['Network'] ) ) { $formatted['network'] = strip_tags( $v['Network'] ); } if ( isset( $v['PluginURI'] ) ) { $formatted['plugin_uri'] = strip_tags( $v['PluginURI'] ); } if ( in_array( $k, $active_plugins_keys ) ) { // Remove active plugins from list so we can show active and inactive separately. unset( $plugins[ $k ] ); $active_plugins[ $k ] = $formatted; } else { $plugins[ $k ] = $formatted; } } return array( 'active_plugins' => $active_plugins, 'inactive_plugins' => $plugins, ); } /** * Check to see if the helper is connected to woocommerce.com * * @return string */ private static function get_helper_connected() { if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) { $authenticated = WC_Helper_Options::get( 'auth' ); } else { $authenticated = ''; } return ( ! empty( $authenticated ) ) ? 'yes' : 'no'; } /** * Get user totals based on user role. * * @return array */ private static function get_user_counts() { $user_count = array(); $user_count_data = count_users(); $user_count['total'] = $user_count_data['total_users']; // Get user count based on user role. foreach ( $user_count_data['avail_roles'] as $role => $count ) { $user_count[ $role ] = $count; } return $user_count; } /** * Get product totals based on product type. * * @return array */ public static function get_product_counts() { $product_count = array(); $product_count_data = wp_count_posts( 'product' ); $product_count['total'] = $product_count_data->publish; $product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); foreach ( $product_statuses as $product_status ) { $product_count[ $product_status->name ] = $product_status->count; } return $product_count; } /** * Get order counts. * * @return array */ private static function get_order_counts() { $order_count = array(); $order_count_data = wp_count_posts( 'shop_order' ); foreach ( wc_get_order_statuses() as $status_slug => $status_name ) { $order_count[ $status_slug ] = $order_count_data->{ $status_slug }; } return $order_count; } /** * Combine all order data. * * @return array */ private static function get_orders() { $order_dates = self::get_order_dates(); $order_counts = self::get_order_counts(); $order_totals = self::get_order_totals(); $order_gateways = self::get_orders_by_gateway(); return array_merge( $order_dates, $order_counts, $order_totals, $order_gateways ); } /** * Get order totals. * * @since 5.4.0 * @return array */ private static function get_order_totals() { global $wpdb; $gross_total = $wpdb->get_var( " SELECT SUM( order_meta.meta_value ) AS 'gross_total' FROM {$wpdb->prefix}posts AS orders LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID WHERE order_meta.meta_key = '_order_total' AND orders.post_status in ( 'wc-completed', 'wc-refunded' ) GROUP BY order_meta.meta_key " ); if ( is_null( $gross_total ) ) { $gross_total = 0; } $processing_gross_total = $wpdb->get_var( " SELECT SUM( order_meta.meta_value ) AS 'gross_total' FROM {$wpdb->prefix}posts AS orders LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID WHERE order_meta.meta_key = '_order_total' AND orders.post_status = 'wc-processing' GROUP BY order_meta.meta_key " ); if ( is_null( $processing_gross_total ) ) { $processing_gross_total = 0; } return array( 'gross' => $gross_total, 'processing_gross' => $processing_gross_total, ); } /** * Get last order date. * * @return string */ private static function get_order_dates() { global $wpdb; $min_max = $wpdb->get_row( " SELECT MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last' FROM {$wpdb->prefix}posts WHERE post_type = 'shop_order' AND post_status = 'wc-completed' ", ARRAY_A ); if ( is_null( $min_max ) ) { $min_max = array( 'first' => '-', 'last' => '-', ); } $processing_min_max = $wpdb->get_row( " SELECT MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last' FROM {$wpdb->prefix}posts WHERE post_type = 'shop_order' AND post_status = 'wc-processing' ", ARRAY_A ); if ( is_null( $processing_min_max ) ) { $processing_min_max = array( 'processing_first' => '-', 'processing_last' => '-', ); } return array_merge( $min_max, $processing_min_max ); } /** * Get order details by gateway. * * @return array */ private static function get_orders_by_gateway() { global $wpdb; $orders_by_gateway = $wpdb->get_results( " SELECT gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts FROM ( SELECT orders.id AS order_id, MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway, MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total, MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency FROM {$wpdb->prefix}posts orders LEFT JOIN {$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id WHERE orders.post_type = 'shop_order' AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' ) AND meta_key in( '_payment_method','_order_total','_order_currency') GROUP BY orders.id ) order_gateways GROUP BY gateway, currency " ); $orders_by_gateway_currency = array(); foreach ( $orders_by_gateway as $orders_details ) { $gateway = 'gateway_' . $orders_details->gateway; $currency = $orders_details->currency; $count = $gateway . '_' . $currency . '_count'; $total = $gateway . '_' . $currency . '_total'; $orders_by_gateway_currency[ $count ] = $orders_details->counts; $orders_by_gateway_currency[ $total ] = $orders_details->totals; } return $orders_by_gateway_currency; } /** * Get review counts for different statuses. * * @return array */ private static function get_review_counts() { global $wpdb; $review_count = array( 'total' => 0 ); $status_map = array( '0' => 'pending', '1' => 'approved', 'trash' => 'trash', 'spam' => 'spam', ); $counts = $wpdb->get_results( " SELECT comment_approved, COUNT(*) AS num_reviews FROM {$wpdb->comments} WHERE comment_type = 'review' GROUP BY comment_approved ", ARRAY_A ); if ( ! $counts ) { return $review_count; } foreach ( $counts as $count ) { $status = $count['comment_approved']; if ( array_key_exists( $status, $status_map ) ) { $review_count[ $status_map[ $status ] ] = $count['num_reviews']; } $review_count['total'] += $count['num_reviews']; } return $review_count; } /** * Get the number of product categories. * * @return int */ private static function get_category_counts() { return wp_count_terms( 'product_cat' ); } /** * Get a list of all active payment gateways. * * @return array */ private static function get_active_payment_gateways() { $active_gateways = array(); $gateways = WC()->payment_gateways->payment_gateways(); foreach ( $gateways as $id => $gateway ) { if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) { $active_gateways[ $id ] = array( 'title' => $gateway->title, 'supports' => $gateway->supports, ); } } return $active_gateways; } /** * Get a list of all active shipping methods. * * @return array */ private static function get_active_shipping_methods() { $active_methods = array(); $shipping_methods = WC()->shipping()->get_shipping_methods(); foreach ( $shipping_methods as $id => $shipping_method ) { if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) { $active_methods[ $id ] = array( 'title' => $shipping_method->title, 'tax_status' => $shipping_method->tax_status, ); } } return $active_methods; } /** * Get all options starting with woocommerce_ prefix. * * @return array */ private static function get_all_woocommerce_options_values() { return array( 'version' => WC()->version, 'currency' => get_woocommerce_currency(), 'base_location' => WC()->countries->get_base_country(), 'base_state' => WC()->countries->get_base_state(), 'base_postcode' => WC()->countries->get_base_postcode(), 'selling_locations' => WC()->countries->get_allowed_countries(), 'api_enabled' => get_option( 'woocommerce_api_enabled' ), 'weight_unit' => get_option( 'woocommerce_weight_unit' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), 'download_method' => get_option( 'woocommerce_file_download_method' ), 'download_require_login' => get_option( 'woocommerce_downloads_require_login' ), 'calc_taxes' => get_option( 'woocommerce_calc_taxes' ), 'coupons_enabled' => get_option( 'woocommerce_enable_coupons' ), 'guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), 'checkout_login_reminder' => get_option( 'woocommerce_enable_checkout_login_reminder' ), 'secure_checkout' => get_option( 'woocommerce_force_ssl_checkout' ), 'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ), 'enable_myaccount_registration' => get_option( 'woocommerce_enable_myaccount_registration' ), 'registration_generate_username' => get_option( 'woocommerce_registration_generate_username' ), 'registration_generate_password' => get_option( 'woocommerce_registration_generate_password' ), ); } /** * Look for any template override and return filenames. * * @return array */ private static function get_all_template_overrides() { $override_data = array(); $template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) ); $scanned_files = array(); require_once WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php'; foreach ( $template_paths as $plugin_name => $template_path ) { $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); } foreach ( $scanned_files as $plugin_name => $files ) { foreach ( $files as $file ) { if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . $file; } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { $theme_file = get_template_directory() . '/' . $file; } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; } else { $theme_file = false; } if ( false !== $theme_file ) { $override_data[] = basename( $theme_file ); } } } return $override_data; } /** * Search a specific post for text content. * * @param integer $post_id The id of the post to search. * @param string $text The text to search for. * @return string 'Yes' if post contains $text (otherwise 'No'). */ public static function post_contains_text( $post_id, $text ) { global $wpdb; // Search for the text anywhere in the post. $wildcarded = "%{$text}%"; $result = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT( * ) FROM {$wpdb->prefix}posts WHERE ID=%d AND {$wpdb->prefix}posts.post_content LIKE %s ", array( $post_id, $wildcarded ) ) ); return ( '0' !== $result ) ? 'Yes' : 'No'; } /** * Get tracker data for a specific block type on a woocommerce page. * * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. * @param string $woo_page_name The woo page to search, e.g. `cart`. * @return array Associative array of tracker data with keys: * - page_contains_block * - block_attributes */ public static function get_block_tracker_data( $block_name, $woo_page_name ) { $blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name ); $block_present = false; $attributes = array(); if ( $blocks && count( $blocks ) ) { // Return any customised attributes from the first block. $block_present = true; $attributes = $blocks[0]['attrs']; } return array( 'page_contains_block' => $block_present ? 'Yes' : 'No', 'block_attributes' => $attributes, ); } /** * Get info about the cart & checkout pages. * * @return array */ public static function get_cart_checkout_info() { $cart_page_id = wc_get_page_id( 'cart' ); $checkout_page_id = wc_get_page_id( 'checkout' ); $cart_block_data = self::get_block_tracker_data( 'woocommerce/cart', 'cart' ); $checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' ); return array( 'cart_page_contains_cart_shortcode' => self::post_contains_text( $cart_page_id, '[woocommerce_cart]' ), 'checkout_page_contains_checkout_shortcode' => self::post_contains_text( $checkout_page_id, '[woocommerce_checkout]' ), 'cart_page_contains_cart_block' => $cart_block_data['page_contains_block'], 'cart_block_attributes' => $cart_block_data['block_attributes'], 'checkout_page_contains_checkout_block' => $checkout_block_data['page_contains_block'], 'checkout_block_attributes' => $checkout_block_data['block_attributes'], ); } /** * Get info about WooCommerce Mobile App usage * * @return array */ public static function get_woocommerce_mobile_usage() { return get_option( 'woocommerce_mobile_app_usage' ); } } WC_Tracker::init(); includes/class-wc-order-factory.php 0000644 00000006172 15132754524 0013373 0 ustar 00 <?php /** * Order Factory * * The WooCommerce order factory creating the right order objects. * * @version 3.0.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * Order factory class */ class WC_Order_Factory { /** * Get order. * * @param mixed $order_id (default: false) Order ID to get. * @return WC_Order|bool */ public static function get_order( $order_id = false ) { $order_id = self::get_order_id( $order_id ); if ( ! $order_id ) { return false; } $order_type = WC_Data_Store::load( 'order' )->get_order_type( $order_id ); $order_type_data = wc_get_order_type( $order_type ); if ( $order_type_data ) { $classname = $order_type_data['class_name']; } else { $classname = false; } // Filter classname so that the class can be overridden if extended. $classname = apply_filters( 'woocommerce_order_class', $classname, $order_type, $order_id ); if ( ! class_exists( $classname ) ) { return false; } try { return new $classname( $order_id ); } catch ( Exception $e ) { wc_caught_exception( $e, __FUNCTION__, array( $order_id ) ); return false; } } /** * Get order item. * * @param int $item_id Order item ID to get. * @return WC_Order_Item|false if not found */ public static function get_order_item( $item_id = 0 ) { if ( is_numeric( $item_id ) ) { $item_type = WC_Data_Store::load( 'order-item' )->get_order_item_type( $item_id ); $id = $item_id; } elseif ( $item_id instanceof WC_Order_Item ) { $item_type = $item_id->get_type(); $id = $item_id->get_id(); } elseif ( is_object( $item_id ) && ! empty( $item_id->order_item_type ) ) { $id = $item_id->order_item_id; $item_type = $item_id->order_item_type; } else { $item_type = false; $id = false; } if ( $id && $item_type ) { $classname = false; switch ( $item_type ) { case 'line_item': case 'product': $classname = 'WC_Order_Item_Product'; break; case 'coupon': $classname = 'WC_Order_Item_Coupon'; break; case 'fee': $classname = 'WC_Order_Item_Fee'; break; case 'shipping': $classname = 'WC_Order_Item_Shipping'; break; case 'tax': $classname = 'WC_Order_Item_Tax'; break; } $classname = apply_filters( 'woocommerce_get_order_item_classname', $classname, $item_type, $id ); if ( $classname && class_exists( $classname ) ) { try { return new $classname( $id ); } catch ( Exception $e ) { return false; } } } return false; } /** * Get the order ID depending on what was passed. * * @since 3.0.0 * @param mixed $order Order data to convert to an ID. * @return int|bool false on failure */ public static function get_order_id( $order ) { global $post; if ( false === $order && is_a( $post, 'WP_Post' ) && 'shop_order' === get_post_type( $post ) ) { return absint( $post->ID ); } elseif ( is_numeric( $order ) ) { return $order; } elseif ( $order instanceof WC_Abstract_Order ) { return $order->get_id(); } elseif ( ! empty( $order->ID ) ) { return $order->ID; } else { return false; } } } includes/class-wc-regenerate-images.php 0000644 00000036445 15132754524 0014205 0 ustar 00 <?php /** * Regenerate Images Functionality * * All functionality pertaining to regenerating product images in realtime. * * @package WooCommerce\Classes * @version 3.5.0 * @since 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Regenerate Images Class */ class WC_Regenerate_Images { /** * Background process to regenerate all images * * @var WC_Regenerate_Images_Request */ protected static $background_process; /** * Stores size being generated on the fly. * * @var string */ protected static $regenerate_size; /** * Init function */ public static function init() { add_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 ); add_filter( 'wp_generate_attachment_metadata', array( __CLASS__, 'add_uncropped_metadata' ) ); add_filter( 'wp_get_attachment_image_src', array( __CLASS__, 'maybe_resize_image' ), 10, 4 ); // Not required when Jetpack Photon is in use. if ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ) ) { return; } if ( apply_filters( 'woocommerce_background_image_regeneration', true ) ) { include_once WC_ABSPATH . 'includes/class-wc-regenerate-images-request.php'; self::$background_process = new WC_Regenerate_Images_Request(); add_action( 'admin_init', array( __CLASS__, 'regenerating_notice' ) ); add_action( 'woocommerce_hide_regenerating_thumbnails_notice', array( __CLASS__, 'dismiss_regenerating_notice' ) ); // Regenerate thumbnails in the background after settings changes. Not ran on multisite to avoid multiple simultanious jobs. if ( ! is_multisite() ) { add_action( 'customize_save_after', array( __CLASS__, 'maybe_regenerate_images' ) ); add_action( 'after_switch_theme', array( __CLASS__, 'maybe_regenerate_images' ) ); } } } /** * If an intermediate size meta differs from the actual image size (settings were changed?) return false so the wrong size is not used. * * @param array $data Size data. * @param int $attachment_id Attachment ID. * @param string $size Size name. * @return array */ public static function filter_image_get_intermediate_size( $data, $attachment_id, $size ) { if ( ! is_string( $size ) || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { return $data; } // If we don't have sizes, we cannot proceed. if ( ! isset( $data['width'], $data['height'] ) ) { return $data; } // See if the image size has changed from our settings. if ( ! self::image_size_matches_settings( $data, $size ) ) { // If Photon is running we can just return false and let Jetpack handle regeneration. if ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ) ) { return false; } else { // If we get here, Jetpack is not running and we don't have the correct image sized stored. Try to return closest match. $size_data = wc_get_image_size( $size ); return image_get_intermediate_size( $attachment_id, array( absint( $size_data['width'] ), absint( $size_data['height'] ) ) ); } } return $data; } /** * We need to track if uncropped was on or off when generating the images. * * @param array $meta_data Array of meta data. * @return array */ public static function add_uncropped_metadata( $meta_data ) { $size_data = wc_get_image_size( 'woocommerce_thumbnail' ); if ( isset( $meta_data['sizes'], $meta_data['sizes']['woocommerce_thumbnail'] ) ) { $meta_data['sizes']['woocommerce_thumbnail']['uncropped'] = empty( $size_data['height'] ); } return $meta_data; } /** * See if an image's dimensions match actual settings. * * @param array $image Image dimensions array. * @param string $size Named size. * @return bool True if they match. False if they do not (may trigger regen). */ protected static function image_size_matches_settings( $image, $size ) { $target_size = wc_get_image_size( $size ); $uncropped = '' === $target_size['width'] || '' === $target_size['height']; if ( ! $uncropped ) { $ratio_match = wp_image_matches_ratio( $image['width'], $image['height'], $target_size['width'], $target_size['height'] ); // Size is invalid if the widths or crop setting don't match. if ( $ratio_match && $target_size['width'] !== $image['width'] ) { return false; } // Size is invalid if the heights don't match. if ( $ratio_match && $target_size['height'] && $target_size['height'] !== $image['height'] ) { return false; } } // If cropping mode has changed, regenerate the image. if ( $uncropped && empty( $image['uncropped'] ) ) { return false; } return true; } /** * Show notice when job is running in background. */ public static function regenerating_notice() { if ( ! self::$background_process->is_running() ) { WC_Admin_Notices::add_notice( 'regenerating_thumbnails' ); } else { WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' ); } } /** * Dismiss notice and cancel jobs. */ public static function dismiss_regenerating_notice() { if ( self::$background_process ) { self::$background_process->kill_process(); $log = wc_get_logger(); $log->info( __( 'Cancelled product image regeneration job.', 'woocommerce' ), array( 'source' => 'wc-image-regeneration', ) ); } WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' ); } /** * Regenerate images if the settings have changed since last re-generation. * * @return void */ public static function maybe_regenerate_images() { $size_hash = md5( wp_json_encode( array( wc_get_image_size( 'thumbnail' ), wc_get_image_size( 'single' ), wc_get_image_size( 'gallery_thumbnail' ), ) ) ); if ( update_option( 'woocommerce_maybe_regenerate_images_hash', $size_hash ) ) { // Size settings have changed. Trigger regen. self::queue_image_regeneration(); } } /** * Check if we should maybe generate a new image size if not already there. * * @param array $image Properties of the image. * @param int $attachment_id Attachment ID. * @param string|array $size Image size. * @param bool $icon If icon or not. * @return array */ public static function maybe_resize_image( $image, $attachment_id, $size, $icon ) { if ( ! apply_filters( 'woocommerce_resize_images', true ) ) { return $image; } // List of sizes we want to resize. Ignore others. if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { return $image; } $target_size = wc_get_image_size( $size ); $image_width = $image[1]; $image_height = $image[2]; $ratio_match = false; $target_uncropped = '' === $target_size['width'] || '' === $target_size['height'] || ! $target_size['crop']; // If '' is passed to either size, we test ratios against the original file. It's uncropped. if ( $target_uncropped ) { $full_size = self::get_full_size_image_dimensions( $attachment_id ); if ( ! $full_size || ! $full_size['width'] || ! $full_size['height'] ) { return $image; } $ratio_match = wp_image_matches_ratio( $image_width, $image_height, $full_size['width'], $full_size['height'] ); } else { $ratio_match = wp_image_matches_ratio( $image_width, $image_height, $target_size['width'], $target_size['height'] ); } if ( ! $ratio_match ) { $full_size = self::get_full_size_image_dimensions( $attachment_id ); if ( ! $full_size ) { return $image; } // Check if the actual image has a larger dimension than the requested image size. Smaller images are not zoom-cropped. if ( $image_width === $target_size['width'] && $full_size['height'] < $target_size['height'] ) { return $image; } if ( $image_height === $target_size['height'] && $full_size['width'] < $target_size['width'] ) { return $image; } // If the full size image is smaller both ways, don't scale it up. if ( $full_size['height'] < $target_size['height'] && $full_size['width'] < $target_size['width'] ) { return $image; } return self::resize_and_return_image( $attachment_id, $image, $size, $icon ); } return $image; } /** * Get full size image dimensions. * * @param int $attachment_id Attachment ID of image. * @return array Width and height. Empty array if the dimensions cannot be found. */ private static function get_full_size_image_dimensions( $attachment_id ) { $imagedata = wp_get_attachment_metadata( $attachment_id ); if ( ! $imagedata ) { return array(); } if ( ! isset( $imagedata['file'] ) && isset( $imagedata['sizes']['full'] ) ) { $imagedata['height'] = $imagedata['sizes']['full']['height']; $imagedata['width'] = $imagedata['sizes']['full']['width']; } return array( 'width' => $imagedata['width'], 'height' => $imagedata['height'], ); } /** * Ensure we are dealing with the correct image attachment * * @param int|WP_Post $attachment Attachment object or ID. * @return boolean */ public static function is_regeneratable( $attachment ) { if ( 'site-icon' === get_post_meta( is_object( $attachment ) ? $attachment->ID : $attachment, '_wp_attachment_context', true ) ) { return false; } if ( wp_attachment_is_image( $attachment ) ) { return true; } return false; } /** * Only regenerate images for the requested size. * * @param array $sizes Array of image sizes. * @return array */ public static function adjust_intermediate_image_sizes( $sizes ) { return array( self::$regenerate_size ); } /** * Generate the thumbnail filename and dimensions for a given file. * * @param string $fullsizepath Path to full size image. * @param int $thumbnail_width The width of the thumbnail. * @param int $thumbnail_height The height of the thumbnail. * @param bool $crop Whether to crop or not. * @return array|false An array of the filename, thumbnail width, and thumbnail height, or false on failure to resize such as the thumbnail being larger than the fullsize image. */ private static function get_image( $fullsizepath, $thumbnail_width, $thumbnail_height, $crop ) { list( $fullsize_width, $fullsize_height ) = getimagesize( $fullsizepath ); $dimensions = image_resize_dimensions( $fullsize_width, $fullsize_height, $thumbnail_width, $thumbnail_height, $crop ); $editor = wp_get_image_editor( $fullsizepath ); if ( is_wp_error( $editor ) ) { return false; } if ( ! $dimensions || ! is_array( $dimensions ) ) { return false; } list( , , , , $dst_w, $dst_h ) = $dimensions; $suffix = "{$dst_w}x{$dst_h}"; $file_ext = strtolower( pathinfo( $fullsizepath, PATHINFO_EXTENSION ) ); return array( 'filename' => $editor->generate_filename( $suffix, null, $file_ext ), 'width' => $dst_w, 'height' => $dst_h, ); } /** * Regenerate the image according to the required size * * @param int $attachment_id Attachment ID. * @param array $image Original Image. * @param string $size Size to return for new URL. * @param bool $icon If icon or not. * @return string */ private static function resize_and_return_image( $attachment_id, $image, $size, $icon ) { if ( ! self::is_regeneratable( $attachment_id ) ) { return $image; } $fullsizepath = get_attached_file( $attachment_id ); if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) { return $image; } if ( ! function_exists( 'wp_crop_image' ) ) { include ABSPATH . 'wp-admin/includes/image.php'; } self::$regenerate_size = is_customize_preview() ? $size . '_preview' : $size; if ( is_customize_preview() ) { $image_size = wc_get_image_size( $size ); // Make sure registered image size matches the size we're requesting. add_image_size( self::$regenerate_size, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] ); $thumbnail = self::get_image( $fullsizepath, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] ); // If the file is already there perhaps just load it if we're using the customizer. No need to store in meta data. if ( $thumbnail && file_exists( $thumbnail['filename'] ) ) { $wp_uploads = wp_upload_dir( null, false ); $wp_uploads_dir = $wp_uploads['basedir']; $wp_uploads_url = $wp_uploads['baseurl']; return array( 0 => str_replace( $wp_uploads_dir, $wp_uploads_url, $thumbnail['filename'] ), 1 => $thumbnail['width'], 2 => $thumbnail['height'], ); } } $metadata = wp_get_attachment_metadata( $attachment_id ); // Fix for images with no metadata. if ( ! is_array( $metadata ) ) { $metadata = array(); } // We only want to regen a specific image size. add_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) ); // This function will generate the new image sizes. $new_metadata = wp_generate_attachment_metadata( $attachment_id, $fullsizepath ); // Remove custom filter. remove_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) ); // If something went wrong lets just return the original image. if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) { return $image; } if ( isset( $new_metadata['sizes'][ self::$regenerate_size ] ) ) { $metadata['sizes'][ self::$regenerate_size ] = $new_metadata['sizes'][ self::$regenerate_size ]; wp_update_attachment_metadata( $attachment_id, $metadata ); } // Now we've done our regen, attempt to return the new size. $new_image = self::unfiltered_image_downsize( $attachment_id, self::$regenerate_size ); return $new_image ? $new_image : $image; } /** * Image downsize, without this classes filtering on the results. * * @param int $attachment_id Attachment ID. * @param string $size Size to downsize to. * @return string New image URL. */ private static function unfiltered_image_downsize( $attachment_id, $size ) { remove_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 ); $return = image_downsize( $attachment_id, $size ); add_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 ); return $return; } /** * Get list of images and queue them for regeneration * * @return void */ public static function queue_image_regeneration() { global $wpdb; // First lets cancel existing running queue to avoid running it more than once. self::$background_process->kill_process(); // Now lets find all product image attachments IDs and pop them onto the queue. $images = $wpdb->get_results( // @codingStandardsIgnoreLine "SELECT ID FROM $wpdb->posts WHERE post_type = 'attachment' AND post_mime_type LIKE 'image/%' ORDER BY ID DESC" ); foreach ( $images as $image ) { self::$background_process->push_to_queue( array( 'attachment_id' => $image->ID, ) ); } // Lets dispatch the queue to start processing. self::$background_process->save()->dispatch(); } } add_action( 'init', array( 'WC_Regenerate_Images', 'init' ) ); includes/class-wc-order-item-meta.php 0000644 00000013460 15132754524 0013604 0 ustar 00 <?php /** * Order Item Meta * * A Simple class for managing order item meta so plugins add it in the correct format. * * @package WooCommerce\Classes * @deprecated 3.0.0 wc_display_item_meta function is used instead. * @version 2.4 */ defined( 'ABSPATH' ) || exit; /** * Order item meta class. */ class WC_Order_Item_Meta { /** * For handling backwards compatibility. * * @var bool */ private $legacy = false; /** * Order item * * @var array|null */ private $item = null; /** * Post meta data * * @var array|null */ public $meta = null; /** * Product object. * * @var WC_Product|null */ public $product = null; /** * Constructor. * * @param array $item defaults to array(). * @param \WC_Product $product defaults to null. */ public function __construct( $item = array(), $product = null ) { wc_deprecated_function( 'WC_Order_Item_Meta::__construct', '3.1', 'WC_Order_Item_Product' ); // Backwards (pre 2.4) compatibility. if ( ! isset( $item['item_meta'] ) ) { $this->legacy = true; $this->meta = array_filter( (array) $item ); return; } $this->item = $item; $this->meta = array_filter( (array) $item['item_meta'] ); $this->product = $product; } /** * Display meta in a formatted list. * * @param bool $flat Flat (default: false). * @param bool $return Return (default: false). * @param string $hideprefix Hide prefix (default: _). * @param string $delimiter Delimiter used to separate items when $flat is true. * @return string|void */ public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) { $output = ''; $formatted_meta = $this->get_formatted( $hideprefix ); if ( ! empty( $formatted_meta ) ) { $meta_list = array(); foreach ( $formatted_meta as $meta ) { if ( $flat ) { $meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] ); } else { $meta_list[] = ' <dt class="variation-' . sanitize_html_class( sanitize_text_field( $meta['key'] ) ) . '">' . wp_kses_post( $meta['label'] ) . ':</dt> <dd class="variation-' . sanitize_html_class( sanitize_text_field( $meta['key'] ) ) . '">' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '</dd> '; } } if ( ! empty( $meta_list ) ) { if ( $flat ) { $output .= implode( $delimiter, $meta_list ); } else { $output .= '<dl class="variation">' . implode( '', $meta_list ) . '</dl>'; } } } $output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this, $flat ); if ( $return ) { return $output; } else { echo $output; // WPCS: XSS ok. } } /** * Return an array of formatted item meta in format e.g. * * Returns: array( * 'pa_size' => array( * 'label' => 'Size', * 'value' => 'Medium', * ) * ) * * @since 2.4 * @param string $hideprefix exclude meta when key is prefixed with this, defaults to '_'. * @return array */ public function get_formatted( $hideprefix = '_' ) { if ( $this->legacy ) { return $this->get_formatted_legacy( $hideprefix ); } $formatted_meta = array(); if ( ! empty( $this->item['item_meta_array'] ) ) { foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { if ( '' === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { continue; } $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); $meta_value = $meta->value; // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $attribute_key ) ) { $term = get_term_by( 'slug', $meta_value, $attribute_key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $meta_value = $term->name; } } $formatted_meta[ $meta_id ] = array( 'key' => $meta->key, 'label' => wc_attribute_label( $attribute_key, $this->product ), 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ), ); } } return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this ); } /** * Return an array of formatted item meta in format e.g. * Handles @deprecated args. * * @param string $hideprefix Hide prefix. * * @return array */ public function get_formatted_legacy( $hideprefix = '_' ) { if ( ! is_ajax() ) { wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' ); } $formatted_meta = array(); foreach ( $this->meta as $meta_key => $meta_values ) { if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) === $hideprefix ) ) { continue; } foreach ( (array) $meta_values as $meta_value ) { // Skip serialised meta. if ( is_serialized( $meta_value ) ) { continue; } $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $attribute_key ) ) { $term = get_term_by( 'slug', $meta_value, $attribute_key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $meta_value = $term->name; } } // Unique key required. $formatted_meta_key = $meta_key; $loop = 0; while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) { $loop ++; $formatted_meta_key = $meta_key . '-' . $loop; } $formatted_meta[ $formatted_meta_key ] = array( 'key' => $meta_key, 'label' => wc_attribute_label( $attribute_key, $this->product ), 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $this->meta, $this->item ), ); } } return $formatted_meta; } } includes/class-wc-regenerate-images-request.php 0000644 00000020333 15132754524 0015660 0 ustar 00 <?php /** * All functionality to regenerate images in the background when settings change. * * @package WooCommerce\Classes * @version 3.3.0 * @since 3.3.0 */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Background_Process', false ) ) { include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php'; } /** * Class that extends WC_Background_Process to process image regeneration in the background. */ class WC_Regenerate_Images_Request extends WC_Background_Process { /** * Stores the attachment ID being processed. * * @var integer */ protected $attachment_id = 0; /** * Initiate new background process. */ public function __construct() { // Uses unique prefix per blog so each blog has separate queue. $this->prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_regenerate_images'; // This is needed to prevent timeouts due to threading. See https://core.trac.wordpress.org/ticket/36534. @putenv( 'MAGICK_THREAD_LIMIT=1' ); // @codingStandardsIgnoreLine. parent::__construct(); } /** * Is job running? * * @return boolean */ public function is_running() { return $this->is_queue_empty(); } /** * Limit each task ran per batch to 1 for image regen. * * @return bool */ protected function batch_limit_exceeded() { return true; } /** * Determines whether an attachment can have its thumbnails regenerated. * * Adapted from Regenerate Thumbnails by Alex Mills. * * @param WP_Post $attachment An attachment's post object. * @return bool Whether the given attachment can have its thumbnails regenerated. */ protected function is_regeneratable( $attachment ) { if ( 'site-icon' === get_post_meta( $attachment->ID, '_wp_attachment_context', true ) ) { return false; } if ( wp_attachment_is_image( $attachment ) ) { return true; } return false; } /** * Code to execute for each item in the queue * * @param mixed $item Queue item to iterate over. * @return bool */ protected function task( $item ) { if ( ! is_array( $item ) && ! isset( $item['attachment_id'] ) ) { return false; } $this->attachment_id = absint( $item['attachment_id'] ); $attachment = get_post( $this->attachment_id ); if ( ! $attachment || 'attachment' !== $attachment->post_type || ! $this->is_regeneratable( $attachment ) ) { return false; } if ( ! function_exists( 'wp_crop_image' ) ) { include ABSPATH . 'wp-admin/includes/image.php'; } $log = wc_get_logger(); $log->info( sprintf( // translators: %s: ID of the attachment. __( 'Regenerating images for attachment ID: %s', 'woocommerce' ), $this->attachment_id ), array( 'source' => 'wc-image-regeneration', ) ); $fullsizepath = get_attached_file( $this->attachment_id ); // Check if the file exists, if not just remove item from queue. if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) { return false; } $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); // We only want to regen WC images. add_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); // We only want to resize images if they do not already exist. add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); // This function will generate the new image sizes. $new_metadata = wp_generate_attachment_metadata( $this->attachment_id, $fullsizepath ); // Remove custom filters. remove_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); // If something went wrong lets just remove the item from the queue. if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) { return false; } if ( ! empty( $old_metadata ) && ! empty( $old_metadata['sizes'] ) && is_array( $old_metadata['sizes'] ) ) { foreach ( $old_metadata['sizes'] as $old_size => $old_size_data ) { if ( empty( $new_metadata['sizes'][ $old_size ] ) ) { $new_metadata['sizes'][ $old_size ] = $old_metadata['sizes'][ $old_size ]; } } // Handle legacy sizes. if ( isset( $new_metadata['sizes']['shop_thumbnail'], $new_metadata['sizes']['woocommerce_gallery_thumbnail'] ) ) { $new_metadata['sizes']['shop_thumbnail'] = $new_metadata['sizes']['woocommerce_gallery_thumbnail']; } if ( isset( $new_metadata['sizes']['shop_catalog'], $new_metadata['sizes']['woocommerce_thumbnail'] ) ) { $new_metadata['sizes']['shop_catalog'] = $new_metadata['sizes']['woocommerce_thumbnail']; } if ( isset( $new_metadata['sizes']['shop_single'], $new_metadata['sizes']['woocommerce_single'] ) ) { $new_metadata['sizes']['shop_single'] = $new_metadata['sizes']['woocommerce_single']; } } // Update the meta data with the new size values. wp_update_attachment_metadata( $this->attachment_id, $new_metadata ); // We made it till the end, now lets remove the item from the queue. return false; } /** * Filters the list of thumbnail sizes to only include those which have missing files. * * @param array $sizes An associative array of registered thumbnail image sizes. * @param array $metadata An associative array of fullsize image metadata: width, height, file. * @param int $attachment_id Attachment ID. Only passed from WP 5.0+. * @return array An associative array of image sizes. */ public function filter_image_sizes_to_only_missing_thumbnails( $sizes, $metadata, $attachment_id = null ) { $attachment_id = is_null( $attachment_id ) ? $this->attachment_id : $attachment_id; if ( ! $sizes || ! $attachment_id ) { return $sizes; } $fullsizepath = get_attached_file( $attachment_id ); $editor = wp_get_image_editor( $fullsizepath ); if ( is_wp_error( $editor ) ) { return $sizes; } $metadata = wp_get_attachment_metadata( $attachment_id ); // This is based on WP_Image_Editor_GD::multi_resize() and others. foreach ( $sizes as $size => $size_data ) { if ( empty( $metadata['sizes'][ $size ] ) ) { continue; } if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { continue; } if ( ! isset( $size_data['width'] ) ) { $size_data['width'] = null; } if ( ! isset( $size_data['height'] ) ) { $size_data['height'] = null; } if ( ! isset( $size_data['crop'] ) ) { $size_data['crop'] = false; } $image_sizes = getimagesize( $fullsizepath ); if ( false === $image_sizes ) { continue; } list( $orig_w, $orig_h ) = $image_sizes; $dimensions = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] ); if ( ! $dimensions || ! is_array( $dimensions ) ) { continue; } $info = pathinfo( $fullsizepath ); $ext = $info['extension']; $dst_w = $dimensions[4]; $dst_h = $dimensions[5]; $suffix = "{$dst_w}x{$dst_h}"; $dst_rel_path = str_replace( '.' . $ext, '', $fullsizepath ); $thumbnail = "{$dst_rel_path}-{$suffix}.{$ext}"; if ( $dst_w === $metadata['sizes'][ $size ]['width'] && $dst_h === $metadata['sizes'][ $size ]['height'] && file_exists( $thumbnail ) ) { unset( $sizes[ $size ] ); } } return $sizes; } /** * Returns the sizes we want to regenerate. * * @param array $sizes Sizes to generate. * @return array */ public function adjust_intermediate_image_sizes( $sizes ) { // Prevent a filter loop. $unfiltered_sizes = array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' ); static $in_filter = false; if ( $in_filter ) { return $unfiltered_sizes; } $in_filter = true; $filtered_sizes = apply_filters( 'woocommerce_regenerate_images_intermediate_image_sizes', $unfiltered_sizes ); $in_filter = false; return $filtered_sizes; } /** * This runs once the job has completed all items on the queue. * * @return void */ protected function complete() { parent::complete(); $log = wc_get_logger(); $log->info( __( 'Completed product image regeneration job.', 'woocommerce' ), array( 'source' => 'wc-image-regeneration', ) ); } } includes/class-wc-shipping-zones.php 0000644 00000010012 15132754524 0013554 0 ustar 00 <?php /** * Handles storage and retrieval of shipping zones * * @package WooCommerce\Classes * @version 3.3.0 * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Shipping zones class. */ class WC_Shipping_Zones { /** * Get shipping zones from the database. * * @since 2.6.0 * @param string $context Getting shipping methods for what context. Valid values, admin, json. * @return array Array of arrays. */ public static function get_zones( $context = 'admin' ) { $data_store = WC_Data_Store::load( 'shipping-zone' ); $raw_zones = $data_store->get_zones(); $zones = array(); foreach ( $raw_zones as $raw_zone ) { $zone = new WC_Shipping_Zone( $raw_zone ); $zones[ $zone->get_id() ] = $zone->get_data(); $zones[ $zone->get_id() ]['zone_id'] = $zone->get_id(); $zones[ $zone->get_id() ]['formatted_zone_location'] = $zone->get_formatted_location(); $zones[ $zone->get_id() ]['shipping_methods'] = $zone->get_shipping_methods( false, $context ); } return $zones; } /** * Get shipping zone using it's ID * * @since 2.6.0 * @param int $zone_id Zone ID. * @return WC_Shipping_Zone|bool */ public static function get_zone( $zone_id ) { return self::get_zone_by( 'zone_id', $zone_id ); } /** * Get shipping zone by an ID. * * @since 2.6.0 * @param string $by Get by 'zone_id' or 'instance_id'. * @param int $id ID. * @return WC_Shipping_Zone|bool */ public static function get_zone_by( $by = 'zone_id', $id = 0 ) { $zone_id = false; switch ( $by ) { case 'zone_id': $zone_id = $id; break; case 'instance_id': $data_store = WC_Data_Store::load( 'shipping-zone' ); $zone_id = $data_store->get_zone_id_by_instance_id( $id ); break; } if ( false !== $zone_id ) { try { return new WC_Shipping_Zone( $zone_id ); } catch ( Exception $e ) { return false; } } return false; } /** * Get shipping zone using it's ID. * * @since 2.6.0 * @param int $instance_id Instance ID. * @return bool|WC_Shipping_Method */ public static function get_shipping_method( $instance_id ) { $data_store = WC_Data_Store::load( 'shipping-zone' ); $raw_shipping_method = $data_store->get_method( $instance_id ); $wc_shipping = WC_Shipping::instance(); $allowed_classes = $wc_shipping->get_shipping_method_class_names(); if ( ! empty( $raw_shipping_method ) && in_array( $raw_shipping_method->method_id, array_keys( $allowed_classes ), true ) ) { $class_name = $allowed_classes[ $raw_shipping_method->method_id ]; if ( is_object( $class_name ) ) { $class_name = get_class( $class_name ); } return new $class_name( $raw_shipping_method->instance_id ); } return false; } /** * Delete a zone using it's ID * * @param int $zone_id Zone ID. * @since 2.6.0 */ public static function delete_zone( $zone_id ) { $zone = new WC_Shipping_Zone( $zone_id ); $zone->delete(); } /** * Find a matching zone for a given package. * * @since 2.6.0 * @uses wc_make_numeric_postcode() * @param array $package Shipping package. * @return WC_Shipping_Zone */ public static function get_zone_matching_package( $package ) { $country = strtoupper( wc_clean( $package['destination']['country'] ) ); $state = strtoupper( wc_clean( $package['destination']['state'] ) ); $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); $cache_key = WC_Cache_Helper::get_cache_prefix( 'shipping_zones' ) . 'wc_shipping_zone_' . md5( sprintf( '%s+%s+%s', $country, $state, $postcode ) ); $matching_zone_id = wp_cache_get( $cache_key, 'shipping_zones' ); if ( false === $matching_zone_id ) { $data_store = WC_Data_Store::load( 'shipping-zone' ); $matching_zone_id = $data_store->get_zone_id_from_package( $package ); wp_cache_set( $cache_key, $matching_zone_id, 'shipping_zones' ); } return new WC_Shipping_Zone( $matching_zone_id ? $matching_zone_id : 0 ); } } includes/class-wc-coupon.php 0000644 00000102667 15132754524 0012124 0 ustar 00 <?php /** * WooCommerce coupons. * * The WooCommerce coupons class gets coupon data from storage and checks coupon validity. * * @package WooCommerce\Classes * @version 3.0.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; require_once dirname( __FILE__ ) . '/legacy/class-wc-legacy-coupon.php'; /** * Coupon class. */ class WC_Coupon extends WC_Legacy_Coupon { /** * Data array, with defaults. * * @since 3.0.0 * @var array */ protected $data = array( 'code' => '', 'amount' => 0, 'date_created' => null, 'date_modified' => null, 'date_expires' => null, 'discount_type' => 'fixed_cart', 'description' => '', 'usage_count' => 0, 'individual_use' => false, 'product_ids' => array(), 'excluded_product_ids' => array(), 'usage_limit' => 0, 'usage_limit_per_user' => 0, 'limit_usage_to_x_items' => null, 'free_shipping' => false, 'product_categories' => array(), 'excluded_product_categories' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'email_restrictions' => array(), 'used_by' => array(), 'virtual' => false, ); // Coupon message codes. const E_WC_COUPON_INVALID_FILTERED = 100; const E_WC_COUPON_INVALID_REMOVED = 101; const E_WC_COUPON_NOT_YOURS_REMOVED = 102; const E_WC_COUPON_ALREADY_APPLIED = 103; const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104; const E_WC_COUPON_NOT_EXIST = 105; const E_WC_COUPON_USAGE_LIMIT_REACHED = 106; const E_WC_COUPON_EXPIRED = 107; const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET = 108; const E_WC_COUPON_NOT_APPLICABLE = 109; const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; const E_WC_COUPON_PLEASE_ENTER = 111; const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK = 115; const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116; const WC_COUPON_SUCCESS = 200; const WC_COUPON_REMOVED = 201; /** * Cache group. * * @var string */ protected $cache_group = 'coupons'; /** * Coupon constructor. Loads coupon data. * * @param mixed $data Coupon data, object, ID or code. */ public function __construct( $data = '' ) { parent::__construct( $data ); // If we already have a coupon object, read it again. if ( $data instanceof WC_Coupon ) { $this->set_id( absint( $data->get_id() ) ); $this->read_object_from_database(); return; } // This filter allows custom coupon objects to be created on the fly. $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this ); if ( $coupon ) { $this->read_manual_coupon( $data, $coupon ); return; } // Try to load coupon using ID or code. if ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) { $this->set_id( $data ); } elseif ( ! empty( $data ) ) { $id = wc_get_coupon_id_by_code( $data ); // Need to support numeric strings for backwards compatibility. if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) { $this->set_id( $data ); } else { $this->set_id( $id ); $this->set_code( $data ); } } else { $this->set_object_read( true ); } $this->read_object_from_database(); } /** * If the object has an ID, read using the data store. * * @since 3.4.1 */ protected function read_object_from_database() { $this->data_store = WC_Data_Store::load( 'coupon' ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /** * Checks the coupon type. * * @param string $type Array or string of types. * @return bool */ public function is_type( $type ) { return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) ); } /** * Prefix for action and filter hooks on data. * * @since 3.0.0 * @return string */ protected function get_hook_prefix() { return 'woocommerce_coupon_get_'; } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- | | Methods for getting data from the coupon object. | */ /** * Get coupon code. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_code( $context = 'view' ) { return $this->get_prop( 'code', $context ); } /** * Get coupon description. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_description( $context = 'view' ) { return $this->get_prop( 'description', $context ); } /** * Get discount type. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_discount_type( $context = 'view' ) { return $this->get_prop( 'discount_type', $context ); } /** * Get coupon amount. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return float */ public function get_amount( $context = 'view' ) { return wc_format_decimal( $this->get_prop( 'amount', $context ) ); } /** * Get coupon expiration date. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_expires( $context = 'view' ) { return $this->get_prop( 'date_expires', $context ); } /** * Get date_created * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { return $this->get_prop( 'date_created', $context ); } /** * Get date_modified * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { return $this->get_prop( 'date_modified', $context ); } /** * Get coupon usage count. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_usage_count( $context = 'view' ) { return $this->get_prop( 'usage_count', $context ); } /** * Get the "indvidual use" checkbox status. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return bool */ public function get_individual_use( $context = 'view' ) { return $this->get_prop( 'individual_use', $context ); } /** * Get product IDs this coupon can apply to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_product_ids( $context = 'view' ) { return $this->get_prop( 'product_ids', $context ); } /** * Get product IDs that this coupon should not apply to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_excluded_product_ids( $context = 'view' ) { return $this->get_prop( 'excluded_product_ids', $context ); } /** * Get coupon usage limit. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_usage_limit( $context = 'view' ) { return $this->get_prop( 'usage_limit', $context ); } /** * Get coupon usage limit per customer (for a single customer) * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer */ public function get_usage_limit_per_user( $context = 'view' ) { return $this->get_prop( 'usage_limit_per_user', $context ); } /** * Usage limited to certain amount of items * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return integer|null */ public function get_limit_usage_to_x_items( $context = 'view' ) { return $this->get_prop( 'limit_usage_to_x_items', $context ); } /** * If this coupon grants free shipping or not. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return bool */ public function get_free_shipping( $context = 'view' ) { return $this->get_prop( 'free_shipping', $context ); } /** * Get product categories this coupon can apply to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_product_categories( $context = 'view' ) { return $this->get_prop( 'product_categories', $context ); } /** * Get product categories this coupon cannot not apply to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_excluded_product_categories( $context = 'view' ) { return $this->get_prop( 'excluded_product_categories', $context ); } /** * If this coupon should exclude items on sale. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return bool */ public function get_exclude_sale_items( $context = 'view' ) { return $this->get_prop( 'exclude_sale_items', $context ); } /** * Get minimum spend amount. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return float */ public function get_minimum_amount( $context = 'view' ) { return $this->get_prop( 'minimum_amount', $context ); } /** * Get maximum spend amount. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return float */ public function get_maximum_amount( $context = 'view' ) { return $this->get_prop( 'maximum_amount', $context ); } /** * Get emails to check customer usage restrictions. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_email_restrictions( $context = 'view' ) { return $this->get_prop( 'email_restrictions', $context ); } /** * Get records of all users who have used the current coupon. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_used_by( $context = 'view' ) { return $this->get_prop( 'used_by', $context ); } /** * If the filter is added through the woocommerce_get_shop_coupon_data filter, it's virtual and not in the DB. * * @since 3.2.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return boolean */ public function get_virtual( $context = 'view' ) { return (bool) $this->get_prop( 'virtual', $context ); } /** * Get discount amount for a cart item. * * @param float $discounting_amount Amount the coupon is being applied to. * @param array|null $cart_item Cart item being discounted if applicable. * @param boolean $single True if discounting a single qty item, false if its the line. * @return float Amount this coupon has discounted. */ public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { $discount = 0; $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; if ( $this->is_type( array( 'percent' ) ) ) { $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { /** * This is the most complex discount - we need to divide the discount between rows based on their price in. * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. * with no price (free) don't get discounted. * * Get item discount by dividing item cost by subtotal to get a %. * * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. */ if ( wc_prices_include_tax() ) { $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; } else { $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; } $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; } elseif ( $this->is_type( 'fixed_product' ) ) { $discount = min( $this->get_amount(), $discounting_amount ); $discount = $single ? $discount : $discount * $cart_item_qty; } return apply_filters( 'woocommerce_coupon_get_discount_amount', NumberUtil::round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), $discounting_amount, $cart_item, $single, $this ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting coupon data. These should not update anything in the | database itself and should only change what is stored in the class | object. | */ /** * Set coupon code. * * @since 3.0.0 * @param string $code Coupon code. */ public function set_code( $code ) { $this->set_prop( 'code', wc_format_coupon_code( $code ) ); } /** * Set coupon description. * * @since 3.0.0 * @param string $description Description. */ public function set_description( $description ) { $this->set_prop( 'description', $description ); } /** * Set discount type. * * @since 3.0.0 * @param string $discount_type Discount type. */ public function set_discount_type( $discount_type ) { if ( 'percent_product' === $discount_type ) { $discount_type = 'percent'; // Backwards compatibility. } if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) { $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); } $this->set_prop( 'discount_type', $discount_type ); } /** * Set amount. * * @since 3.0.0 * @param float $amount Amount. */ public function set_amount( $amount ) { $amount = wc_format_decimal( $amount ); if ( ! is_numeric( $amount ) ) { $amount = 0; } if ( $amount < 0 ) { $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); } if ( 'percent' === $this->get_discount_type() && $amount > 100 ) { $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); } $this->set_prop( 'amount', $amount ); } /** * Set expiration date. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. */ public function set_date_expires( $date ) { $this->set_date_prop( 'date_expires', $date ); } /** * Set date_created * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. */ public function set_date_created( $date ) { $this->set_date_prop( 'date_created', $date ); } /** * Set date_modified * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. */ public function set_date_modified( $date ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set how many times this coupon has been used. * * @since 3.0.0 * @param int $usage_count Usage count. */ public function set_usage_count( $usage_count ) { $this->set_prop( 'usage_count', absint( $usage_count ) ); } /** * Set if this coupon can only be used once. * * @since 3.0.0 * @param bool $is_individual_use If is for individual use. */ public function set_individual_use( $is_individual_use ) { $this->set_prop( 'individual_use', (bool) $is_individual_use ); } /** * Set the product IDs this coupon can be used with. * * @since 3.0.0 * @param array $product_ids Products IDs. */ public function set_product_ids( $product_ids ) { $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); } /** * Set the product IDs this coupon cannot be used with. * * @since 3.0.0 * @param array $excluded_product_ids Exclude product IDs. */ public function set_excluded_product_ids( $excluded_product_ids ) { $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); } /** * Set the amount of times this coupon can be used. * * @since 3.0.0 * @param int $usage_limit Usage limit. */ public function set_usage_limit( $usage_limit ) { $this->set_prop( 'usage_limit', absint( $usage_limit ) ); } /** * Set the amount of times this coupon can be used per user. * * @since 3.0.0 * @param int $usage_limit Usage limit. */ public function set_usage_limit_per_user( $usage_limit ) { $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); } /** * Set usage limit to x number of items. * * @since 3.0.0 * @param int|null $limit_usage_to_x_items Limit usage to X items. */ public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); } /** * Set if this coupon enables free shipping or not. * * @since 3.0.0 * @param bool $free_shipping If grant free shipping. */ public function set_free_shipping( $free_shipping ) { $this->set_prop( 'free_shipping', (bool) $free_shipping ); } /** * Set the product category IDs this coupon can be used with. * * @since 3.0.0 * @param array $product_categories List of product categories. */ public function set_product_categories( $product_categories ) { $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); } /** * Set the product category IDs this coupon cannot be used with. * * @since 3.0.0 * @param array $excluded_product_categories List of excluded product categories. */ public function set_excluded_product_categories( $excluded_product_categories ) { $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); } /** * Set if this coupon should excluded sale items or not. * * @since 3.0.0 * @param bool $exclude_sale_items If should exclude sale items. */ public function set_exclude_sale_items( $exclude_sale_items ) { $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); } /** * Set the minimum spend amount. * * @since 3.0.0 * @param float $amount Minium amount. */ public function set_minimum_amount( $amount ) { $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); } /** * Set the maximum spend amount. * * @since 3.0.0 * @param float $amount Maximum amount. */ public function set_maximum_amount( $amount ) { $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); } /** * Set email restrictions. * * @since 3.0.0 * @param array $emails List of emails. */ public function set_email_restrictions( $emails = array() ) { $emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) ); foreach ( $emails as $email ) { if ( ! is_email( $email ) ) { $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); } } $this->set_prop( 'email_restrictions', $emails ); } /** * Set which users have used this coupon. * * @since 3.0.0 * @param array $used_by List of user IDs. */ public function set_used_by( $used_by ) { $this->set_prop( 'used_by', array_filter( $used_by ) ); } /** * Set coupon virtual state. * * @param boolean $virtual Whether it is virtual or not. * @since 3.2.0 */ public function set_virtual( $virtual ) { $this->set_prop( 'virtual', (bool) $virtual ); } /* |-------------------------------------------------------------------------- | Other Actions |-------------------------------------------------------------------------- */ /** * Developers can programmatically return coupons. This function will read those values into our WC_Coupon class. * * @since 3.0.0 * @param string $code Coupon code. * @param array $coupon Array of coupon properties. */ public function read_manual_coupon( $code, $coupon ) { foreach ( $coupon as $key => $value ) { switch ( $key ) { case 'excluded_product_ids': case 'exclude_product_ids': if ( ! is_array( $coupon[ $key ] ) ) { wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); $coupon['excluded_product_ids'] = wc_string_to_array( $value ); } break; case 'exclude_product_categories': case 'excluded_product_categories': if ( ! is_array( $coupon[ $key ] ) ) { wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); $coupon['excluded_product_categories'] = wc_string_to_array( $value ); } break; case 'product_ids': if ( ! is_array( $coupon[ $key ] ) ) { wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); $coupon[ $key ] = wc_string_to_array( $value ); } break; case 'individual_use': case 'free_shipping': case 'exclude_sale_items': if ( ! is_bool( $coupon[ $key ] ) ) { wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); $coupon[ $key ] = wc_string_to_bool( $value ); } break; case 'expiry_date': $coupon['date_expires'] = $value; break; } } $this->set_props( $coupon ); $this->set_code( $code ); $this->set_id( 0 ); $this->set_virtual( true ); } /** * Increase usage count for current coupon. * * @param string $used_by Either user ID or billing email. * @param WC_Order $order If provided, will clear the coupons held by this order. */ public function increase_usage_count( $used_by = '', $order = null ) { if ( $this->get_id() && $this->data_store ) { $new_count = $this->data_store->increase_usage_count( $this, $used_by, $order ); // Bypass set_prop and remove pending changes since the data store saves the count already. $this->data['usage_count'] = $new_count; if ( isset( $this->changes['usage_count'] ) ) { unset( $this->changes['usage_count'] ); } } } /** * Decrease usage count for current coupon. * * @param string $used_by Either user ID or billing email. */ public function decrease_usage_count( $used_by = '' ) { if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); // Bypass set_prop and remove pending changes since the data store saves the count already. $this->data['usage_count'] = $new_count; if ( isset( $this->changes['usage_count'] ) ) { unset( $this->changes['usage_count'] ); } } } /* |-------------------------------------------------------------------------- | Validation & Error Handling |-------------------------------------------------------------------------- */ /** * Returns the error_message string. * @return string */ public function get_error_message() { return $this->error_message; } /** * Check if a coupon is valid for the cart. * * @deprecated 3.2.0 In favor of WC_Discounts->is_coupon_valid. * @return bool */ public function is_valid() { $discounts = new WC_Discounts( WC()->cart ); $valid = $discounts->is_coupon_valid( $this ); if ( is_wp_error( $valid ) ) { $this->error_message = $valid->get_error_message(); return false; } return $valid; } /** * Check if a coupon is valid. * * @return bool */ public function is_valid_for_cart() { return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); } /** * Check if a coupon is valid for a product. * * @param WC_Product $product Product instance. * @param array $values Values. * @return bool */ public function is_valid_for_product( $product, $values = array() ) { if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); } $valid = false; $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); $product_ids = array( $product->get_id(), $product->get_parent_id() ); // Specific products get the discount. if ( count( $this->get_product_ids() ) && count( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { $valid = true; } // Category discounts. if ( count( $this->get_product_categories() ) && count( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { $valid = true; } // No product ids - all items discounted. if ( ! count( $this->get_product_ids() ) && ! count( $this->get_product_categories() ) ) { $valid = true; } // Specific product IDs excluded from the discount. if ( count( $this->get_excluded_product_ids() ) && count( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { $valid = false; } // Specific categories excluded from the discount. if ( count( $this->get_excluded_product_categories() ) && count( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { $valid = false; } // Sale Items excluded from discount. if ( $this->get_exclude_sale_items() && $product->is_on_sale() ) { $valid = false; } return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); } /** * Converts one of the WC_Coupon message/error codes to a message string and. * displays the message/error. * * @param int $msg_code Message/error code. */ public function add_coupon_message( $msg_code ) { $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); if ( ! $msg ) { return; } if ( $msg_code < 200 ) { wc_add_notice( $msg, 'error' ); } else { wc_add_notice( $msg ); } } /** * Map one of the WC_Coupon message codes to a message string. * * @param integer $msg_code Message code. * @return string Message/error string. */ public function get_coupon_message( $msg_code ) { switch ( $msg_code ) { case self::WC_COUPON_SUCCESS: $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); break; case self::WC_COUPON_REMOVED: $msg = __( 'Coupon code removed successfully.', 'woocommerce' ); break; default: $msg = ''; break; } return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); } /** * Map one of the WC_Coupon error codes to a message string. * * @param int $err_code Message/error code. * @return string Message/error string */ public function get_coupon_error( $err_code ) { switch ( $err_code ) { case self::E_WC_COUPON_INVALID_FILTERED: $err = __( 'Coupon is not valid.', 'woocommerce' ); break; case self::E_WC_COUPON_NOT_EXIST: /* translators: %s: coupon code */ $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $this->get_code() ) ); break; case self::E_WC_COUPON_INVALID_REMOVED: /* translators: %s: coupon code */ $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); break; case self::E_WC_COUPON_NOT_YOURS_REMOVED: /* translators: %s: coupon code */ $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); break; case self::E_WC_COUPON_ALREADY_APPLIED: $err = __( 'Coupon code already applied!', 'woocommerce' ); break; case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: /* translators: %s: coupon code */ $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), esc_html( $this->get_code() ) ); break; case self::E_WC_COUPON_USAGE_LIMIT_REACHED: $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); break; case self::E_WC_COUPON_EXPIRED: $err = __( 'This coupon has expired.', 'woocommerce' ); break; case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: /* translators: %s: coupon minimum amount */ $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); break; case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: /* translators: %s: coupon maximum amount */ $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); break; case self::E_WC_COUPON_NOT_APPLICABLE: $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); break; case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK: if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) { /* translators: %s: myaccount page link. */ $err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the <a href="%s">my account page</a>.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) ); } else { $err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); } break; case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST: $err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' ); break; case self::E_WC_COUPON_EXCLUDED_PRODUCTS: // Store excluded products that are in cart in $products. $products = array(); if ( ! WC()->cart->is_empty() ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( in_array( intval( $cart_item['product_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['variation_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['data']->get_parent_id() ), $this->get_excluded_product_ids(), true ) ) { $products[] = $cart_item['data']->get_name(); } } } /* translators: %s: products list */ $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); break; case self::E_WC_COUPON_EXCLUDED_CATEGORIES: // Store excluded categories that are in cart in $categories. $categories = array(); if ( ! WC()->cart->is_empty() ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ); if ( count( $intersect ) > 0 ) { foreach ( $intersect as $cat_id ) { $cat = get_term( $cat_id, 'product_cat' ); $categories[] = $cat->name; } } } } /* translators: %s: categories list */ $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); break; case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); break; default: $err = ''; break; } return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); } /** * Map one of the WC_Coupon error codes to an error string. * No coupon instance will be available where a coupon does not exist, * so this static method exists. * * @param int $err_code Error code. * @return string Error string. */ public static function get_generic_coupon_error( $err_code ) { switch ( $err_code ) { case self::E_WC_COUPON_NOT_EXIST: $err = __( 'Coupon does not exist!', 'woocommerce' ); break; case self::E_WC_COUPON_PLEASE_ENTER: $err = __( 'Please enter a coupon code.', 'woocommerce' ); break; default: $err = ''; break; } // When using this static method, there is no $this to pass to filter. return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); } } includes/class-wc-data-exception.php 0000644 00000002451 15132754524 0013514 0 ustar 00 <?php /** * WooCommerce Data Exception Class * * Extends Exception to provide additional data. * * @package WooCommerce\Classes * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Data exception class. */ class WC_Data_Exception extends Exception { /** * Sanitized error code. * * @var string */ protected $error_code; /** * Error extra data. * * @var array */ protected $error_data; /** * Setup exception. * * @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'. * @param int $http_status_code Proper HTTP status code to respond with, e.g. 400. * @param array $data Extra error data. */ public function __construct( $code, $message, $http_status_code = 400, $data = array() ) { $this->error_code = $code; $this->error_data = array_merge( array( 'status' => $http_status_code ), $data ); parent::__construct( $message, $http_status_code ); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns error data. * * @return array */ public function getErrorData() { return $this->error_data; } } includes/class-wc-order-query.php 0000644 00000005067 15132754524 0013073 0 ustar 00 <?php /** * Parameter-based Order querying * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query * * @package WooCommerce\Classes * @version 3.1.0 * @since 3.1.0 */ defined( 'ABSPATH' ) || exit; /** * Order query class. */ class WC_Order_Query extends WC_Object_Query { /** * Valid query vars for orders. * * @return array */ protected function get_default_query_vars() { return array_merge( parent::get_default_query_vars(), array( 'status' => array_keys( wc_get_order_statuses() ), 'type' => wc_get_order_types( 'view-orders' ), 'currency' => '', 'version' => '', 'prices_include_tax' => '', 'date_created' => '', 'date_modified' => '', 'date_completed' => '', 'date_paid' => '', 'discount_total' => '', 'discount_tax' => '', 'shipping_total' => '', 'shipping_tax' => '', 'cart_tax' => '', 'total' => '', 'total_tax' => '', 'customer' => '', 'customer_id' => '', 'order_key' => '', 'billing_first_name' => '', 'billing_last_name' => '', 'billing_company' => '', 'billing_address_1' => '', 'billing_address_2' => '', 'billing_city' => '', 'billing_state' => '', 'billing_postcode' => '', 'billing_country' => '', 'billing_email' => '', 'billing_phone' => '', 'shipping_first_name' => '', 'shipping_last_name' => '', 'shipping_company' => '', 'shipping_address_1' => '', 'shipping_address_2' => '', 'shipping_city' => '', 'shipping_state' => '', 'shipping_postcode' => '', 'shipping_country' => '', 'shipping_phone' => '', 'payment_method' => '', 'payment_method_title' => '', 'transaction_id' => '', 'customer_ip_address' => '', 'customer_user_agent' => '', 'created_via' => '', 'customer_note' => '', ) ); } /** * Get orders matching the current query vars. * * @return array|object of WC_Order objects * * @throws Exception When WC_Data_Store validation fails. */ public function get_orders() { $args = apply_filters( 'woocommerce_order_query_args', $this->get_query_vars() ); $results = WC_Data_Store::load( 'order' )->query( $args ); return apply_filters( 'woocommerce_order_query', $results, $args ); } } includes/class-wc-shipping-zone.php 0000644 00000032120 15132754524 0013375 0 ustar 00 <?php /** * Represents a single shipping zone * * @since 2.6.0 * @version 3.0.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; require_once __DIR__ . '/legacy/class-wc-legacy-shipping-zone.php'; /** * WC_Shipping_Zone class. */ class WC_Shipping_Zone extends WC_Legacy_Shipping_Zone { /** * Zone ID * * @var int|null */ protected $id = null; /** * This is the name of this object type. * * @var string */ protected $object_type = 'shipping_zone'; /** * Zone Data. * * @var array */ protected $data = array( 'zone_name' => '', 'zone_order' => 0, 'zone_locations' => array(), ); /** * Constructor for zones. * * @param int|object $zone Zone ID to load from the DB or zone object. */ public function __construct( $zone = null ) { if ( is_numeric( $zone ) && ! empty( $zone ) ) { $this->set_id( $zone ); } elseif ( is_object( $zone ) ) { $this->set_id( $zone->zone_id ); } elseif ( 0 === $zone || '0' === $zone ) { $this->set_id( 0 ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'shipping-zone' ); if ( false === $this->get_object_read() ) { $this->data_store->read( $this ); } } /** * -------------------------------------------------------------------------- * Getters * -------------------------------------------------------------------------- */ /** * Get zone name. * * @param string $context View or edit context. * @return string */ public function get_zone_name( $context = 'view' ) { return $this->get_prop( 'zone_name', $context ); } /** * Get zone order. * * @param string $context View or edit context. * @return int */ public function get_zone_order( $context = 'view' ) { return $this->get_prop( 'zone_order', $context ); } /** * Get zone locations. * * @param string $context View or edit context. * @return array of zone objects */ public function get_zone_locations( $context = 'view' ) { return $this->get_prop( 'zone_locations', $context ); } /** * Return a text string representing what this zone is for. * * @param int $max Max locations to return. * @param string $context View or edit context. * @return string */ public function get_formatted_location( $max = 10, $context = 'view' ) { $location_parts = array(); $all_continents = WC()->countries->get_continents(); $all_countries = WC()->countries->get_countries(); $all_states = WC()->countries->get_states(); $locations = $this->get_zone_locations( $context ); $continents = array_filter( $locations, array( $this, 'location_is_continent' ) ); $countries = array_filter( $locations, array( $this, 'location_is_country' ) ); $states = array_filter( $locations, array( $this, 'location_is_state' ) ); $postcodes = array_filter( $locations, array( $this, 'location_is_postcode' ) ); foreach ( $continents as $location ) { $location_parts[] = $all_continents[ $location->code ]['name']; } foreach ( $countries as $location ) { $location_parts[] = $all_countries[ $location->code ]; } foreach ( $states as $location ) { $location_codes = explode( ':', $location->code ); $location_parts[] = $all_states[ $location_codes[0] ][ $location_codes[1] ]; } foreach ( $postcodes as $location ) { $location_parts[] = $location->code; } // Fix display of encoded characters. $location_parts = array_map( 'html_entity_decode', $location_parts ); if ( count( $location_parts ) > $max ) { $remaining = count( $location_parts ) - $max; // @codingStandardsIgnoreStart return sprintf( _n( '%s and %d other region', '%s and %d other regions', $remaining, 'woocommerce' ), implode( ', ', array_splice( $location_parts, 0, $max ) ), $remaining ); // @codingStandardsIgnoreEnd } elseif ( ! empty( $location_parts ) ) { return implode( ', ', $location_parts ); } else { return __( 'Everywhere', 'woocommerce' ); } } /** * Get shipping methods linked to this zone. * * @param bool $enabled_only Only return enabled methods. * @param string $context Getting shipping methods for what context. Valid values, admin, json. * @return array of objects */ public function get_shipping_methods( $enabled_only = false, $context = 'admin' ) { if ( null === $this->get_id() ) { return array(); } $raw_methods = $this->data_store->get_methods( $this->get_id(), $enabled_only ); $wc_shipping = WC_Shipping::instance(); $allowed_classes = $wc_shipping->get_shipping_method_class_names(); $methods = array(); foreach ( $raw_methods as $raw_method ) { if ( in_array( $raw_method->method_id, array_keys( $allowed_classes ), true ) ) { $class_name = $allowed_classes[ $raw_method->method_id ]; $instance_id = $raw_method->instance_id; // The returned array may contain instances of shipping methods, as well // as classes. If the "class" is an instance, just use it. If not, // create an instance. if ( is_object( $class_name ) ) { $class_name_of_instance = get_class( $class_name ); $methods[ $instance_id ] = new $class_name_of_instance( $instance_id ); } else { // If the class is not an object, it should be a string. It's better // to double check, to be sure (a class must be a string, anything) // else would be useless. if ( is_string( $class_name ) && class_exists( $class_name ) ) { $methods[ $instance_id ] = new $class_name( $instance_id ); } } // Let's make sure that we have an instance before setting its attributes. if ( is_object( $methods[ $instance_id ] ) ) { $methods[ $instance_id ]->method_order = absint( $raw_method->method_order ); $methods[ $instance_id ]->enabled = $raw_method->is_enabled ? 'yes' : 'no'; $methods[ $instance_id ]->has_settings = $methods[ $instance_id ]->has_settings(); $methods[ $instance_id ]->settings_html = $methods[ $instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $instance_id ]->get_admin_options_html() : false; $methods[ $instance_id ]->method_description = wp_kses_post( wpautop( $methods[ $instance_id ]->method_description ) ); } if ( 'json' === $context ) { // We don't want the entire object in this context, just the public props. $methods[ $instance_id ] = (object) get_object_vars( $methods[ $instance_id ] ); unset( $methods[ $instance_id ]->instance_form_fields, $methods[ $instance_id ]->form_fields ); } } } uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' ); return apply_filters( 'woocommerce_shipping_zone_shipping_methods', $methods, $raw_methods, $allowed_classes, $this ); } /** * -------------------------------------------------------------------------- * Setters * -------------------------------------------------------------------------- */ /** * Set zone name. * * @param string $set Value to set. */ public function set_zone_name( $set ) { $this->set_prop( 'zone_name', wc_clean( $set ) ); } /** * Set zone order. Value to set. * * @param int $set Value to set. */ public function set_zone_order( $set ) { $this->set_prop( 'zone_order', absint( $set ) ); } /** * Set zone locations. * * @since 3.0.0 * @param array $locations Value to set. */ public function set_zone_locations( $locations ) { if ( 0 !== $this->get_id() ) { $this->set_prop( 'zone_locations', $locations ); } } /** * -------------------------------------------------------------------------- * Other * -------------------------------------------------------------------------- */ /** * Save zone data to the database. * * @return int */ public function save() { if ( ! $this->get_zone_name() ) { $this->set_zone_name( $this->generate_zone_name() ); } if ( ! $this->data_store ) { return $this->get_id(); } /** * Trigger action before saving to the DB. Allows you to adjust object props before save. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); if ( null !== $this->get_id() ) { $this->data_store->update( $this ); } else { $this->data_store->create( $this ); } /** * Trigger action after saving to the DB. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); return $this->get_id(); } /** * Generate a zone name based on location. * * @return string */ protected function generate_zone_name() { $zone_name = $this->get_formatted_location(); if ( empty( $zone_name ) ) { $zone_name = __( 'Zone', 'woocommerce' ); } return $zone_name; } /** * Location type detection. * * @param object $location Location to check. * @return boolean */ private function location_is_continent( $location ) { return 'continent' === $location->type; } /** * Location type detection. * * @param object $location Location to check. * @return boolean */ private function location_is_country( $location ) { return 'country' === $location->type; } /** * Location type detection. * * @param object $location Location to check. * @return boolean */ private function location_is_state( $location ) { return 'state' === $location->type; } /** * Location type detection. * * @param object $location Location to check. * @return boolean */ private function location_is_postcode( $location ) { return 'postcode' === $location->type; } /** * Is passed location type valid? * * @param string $type Type to check. * @return boolean */ public function is_valid_location_type( $type ) { return in_array( $type, apply_filters( 'woocommerce_valid_location_types', array( 'postcode', 'state', 'country', 'continent' ) ), true ); } /** * Add location (state or postcode) to a zone. * * @param string $code Location code. * @param string $type state or postcode. */ public function add_location( $code, $type ) { if ( 0 !== $this->get_id() && $this->is_valid_location_type( $type ) ) { if ( 'postcode' === $type ) { $code = trim( strtoupper( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $code ) ) ); // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. } $location = array( 'code' => wc_clean( $code ), 'type' => wc_clean( $type ), ); $zone_locations = $this->get_prop( 'zone_locations', 'edit' ); $zone_locations[] = (object) $location; $this->set_prop( 'zone_locations', $zone_locations ); } } /** * Clear all locations for this zone. * * @param array|string $types of location to clear. */ public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) { if ( ! is_array( $types ) ) { $types = array( $types ); } $zone_locations = $this->get_prop( 'zone_locations', 'edit' ); foreach ( $zone_locations as $key => $values ) { if ( in_array( $values->type, $types, true ) ) { unset( $zone_locations[ $key ] ); } } $zone_locations = array_values( $zone_locations ); // reindex. $this->set_prop( 'zone_locations', $zone_locations ); } /** * Set locations. * * @param array $locations Array of locations. */ public function set_locations( $locations = array() ) { $this->clear_locations(); foreach ( $locations as $location ) { $this->add_location( $location['code'], $location['type'] ); } } /** * Add a shipping method to this zone. * * @param string $type shipping method type. * @return int new instance_id, 0 on failure */ public function add_shipping_method( $type ) { if ( null === $this->get_id() ) { $this->save(); } $instance_id = 0; $wc_shipping = WC_Shipping::instance(); $allowed_classes = $wc_shipping->get_shipping_method_class_names(); $count = $this->data_store->get_method_count( $this->get_id() ); if ( in_array( $type, array_keys( $allowed_classes ), true ) ) { $instance_id = $this->data_store->add_method( $this->get_id(), $type, $count + 1 ); } if ( $instance_id ) { do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_id() ); } WC_Cache_Helper::get_transient_version( 'shipping', true ); return $instance_id; } /** * Delete a shipping method from a zone. * * @param int $instance_id Shipping method instance ID. * @return True on success, false on failure */ public function delete_shipping_method( $instance_id ) { if ( null === $this->get_id() ) { return false; } // Get method details. $method = $this->data_store->get_method( $instance_id ); if ( $method ) { $this->data_store->delete_method( $instance_id ); do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method->method_id, $this->get_id() ); } WC_Cache_Helper::get_transient_version( 'shipping', true ); return true; } } includes/wc-update-functions.php 0000644 00000206233 15132754524 0013000 0 ustar 00 <?php /** * WooCommerce Updates * * Functions for updating data, used by the background updater. * * @package WooCommerce\Functions * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\AssignDefaultCategory; /** * Update file paths for 2.0 * * @return void */ function wc_update_200_file_paths() { global $wpdb; // Upgrade old style files paths to support multiple file paths. $existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_path' AND meta_value != '';" ); if ( $existing_file_paths ) { foreach ( $existing_file_paths as $existing_file_path ) { $old_file_path = trim( $existing_file_path->meta_value ); if ( ! empty( $old_file_path ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize $file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_id = %s WHERE product_id = %d", md5( $old_file_path ), $existing_file_path->post_id ) ); } } } } /** * Update permalinks for 2.0 * * @return void */ function wc_update_200_permalinks() { // Setup default permalinks if shop page is defined. $permalinks = get_option( 'woocommerce_permalinks' ); $shop_page_id = wc_get_page_id( 'shop' ); if ( empty( $permalinks ) && $shop_page_id > 0 ) { $base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop'; $category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : ''; $category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' ); $tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' ); if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) { $product_base = trailingslashit( $base_slug ); } else { $product_slug = get_option( 'woocommerce_product_slug' ); if ( false !== $product_slug && ! empty( $product_slug ) ) { $product_base = trailingslashit( $product_slug ); } else { $product_base = trailingslashit( _x( 'product', 'slug', 'woocommerce' ) ); } } if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) { $product_base .= trailingslashit( '%product_cat%' ); } $permalinks = array( 'product_base' => untrailingslashit( $product_base ), 'category_base' => untrailingslashit( $category_base . $category_slug ), 'attribute_base' => untrailingslashit( $category_base ), 'tag_base' => untrailingslashit( $category_base . $tag_slug ), ); update_option( 'woocommerce_permalinks', $permalinks ); } } /** * Update sub-category display options for 2.0 * * @return void */ function wc_update_200_subcat_display() { // Update subcat display settings. if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) { if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { update_option( 'woocommerce_shop_page_display', 'subcategories' ); } else { update_option( 'woocommerce_shop_page_display', 'both' ); } } if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) { if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { update_option( 'woocommerce_category_archive_display', 'subcategories' ); } else { update_option( 'woocommerce_category_archive_display', 'both' ); } } } /** * Update tax rates for 2.0 * * @return void */ function wc_update_200_taxrates() { global $wpdb; // Update tax rates. $loop = 0; $tax_rates = get_option( 'woocommerce_tax_rates' ); if ( $tax_rates ) { foreach ( $tax_rates as $tax_rate ) { foreach ( $tax_rate['countries'] as $country => $states ) { $states = array_reverse( $states ); foreach ( $states as $state ) { if ( '*' === $state ) { $state = ''; } $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_country' => $country, 'tax_rate_state' => $state, 'tax_rate' => $tax_rate['rate'], 'tax_rate_name' => $tax_rate['label'], 'tax_rate_priority' => 1, 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, 'tax_rate_order' => $loop, 'tax_rate_class' => $tax_rate['class'], ) ); $loop++; } } } } $local_tax_rates = get_option( 'woocommerce_local_tax_rates' ); if ( $local_tax_rates ) { foreach ( $local_tax_rates as $tax_rate ) { $location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city'; if ( '*' === $tax_rate['state'] ) { $tax_rate['state'] = ''; } $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_country' => $tax_rate['country'], 'tax_rate_state' => $tax_rate['state'], 'tax_rate' => $tax_rate['rate'], 'tax_rate_name' => $tax_rate['label'], 'tax_rate_priority' => 2, 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, 'tax_rate_order' => $loop, 'tax_rate_class' => $tax_rate['class'], ) ); $tax_rate_id = $wpdb->insert_id; if ( $tax_rate['locations'] ) { foreach ( $tax_rate['locations'] as $location ) { $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rate_locations', array( 'location_code' => $location, 'tax_rate_id' => $tax_rate_id, 'location_type' => $location_type, ) ); } } $loop++; } } update_option( 'woocommerce_tax_rates_backup', $tax_rates ); update_option( 'woocommerce_local_tax_rates_backup', $local_tax_rates ); delete_option( 'woocommerce_tax_rates' ); delete_option( 'woocommerce_local_tax_rates' ); } /** * Update order item line items for 2.0 * * @return void */ function wc_update_200_line_items() { global $wpdb; // Now its time for the massive update to line items - move them to the new DB tables. // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_items' WHERE meta_key = '_order_items_old'. $order_item_rows = $wpdb->get_results( "SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_items'" ); foreach ( $order_item_rows as $order_item_row ) { $order_items = (array) maybe_unserialize( $order_item_row->meta_value ); foreach ( $order_items as $order_item ) { if ( ! isset( $order_item['line_total'] ) && isset( $order_item['taxrate'] ) && isset( $order_item['cost'] ) ) { $order_item['line_tax'] = number_format( ( $order_item['cost'] * $order_item['qty'] ) * ( $order_item['taxrate'] / 100 ), 2, '.', '' ); $order_item['line_total'] = $order_item['cost'] * $order_item['qty']; $order_item['line_subtotal_tax'] = $order_item['line_tax']; $order_item['line_subtotal'] = $order_item['line_total']; } $order_item['line_tax'] = isset( $order_item['line_tax'] ) ? $order_item['line_tax'] : 0; $order_item['line_total'] = isset( $order_item['line_total'] ) ? $order_item['line_total'] : 0; $order_item['line_subtotal_tax'] = isset( $order_item['line_subtotal_tax'] ) ? $order_item['line_subtotal_tax'] : 0; $order_item['line_subtotal'] = isset( $order_item['line_subtotal'] ) ? $order_item['line_subtotal'] : 0; $item_id = wc_add_order_item( $order_item_row->post_id, array( 'order_item_name' => $order_item['name'], 'order_item_type' => 'line_item', ) ); // Add line item meta. if ( $item_id ) { wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) ); wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] ); wc_add_order_item_meta( $item_id, '_product_id', $order_item['id'] ); wc_add_order_item_meta( $item_id, '_variation_id', $order_item['variation_id'] ); wc_add_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $order_item['line_subtotal'] ) ); wc_add_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $order_item['line_subtotal_tax'] ) ); wc_add_order_item_meta( $item_id, '_line_total', wc_format_decimal( $order_item['line_total'] ) ); wc_add_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $order_item['line_tax'] ) ); $meta_rows = array(); // Insert meta. if ( ! empty( $order_item['item_meta'] ) ) { foreach ( $order_item['item_meta'] as $key => $meta ) { // Backwards compatibility. if ( is_array( $meta ) && isset( $meta['meta_name'] ) ) { $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $meta['meta_name'] ) . '","' . esc_sql( $meta['meta_value'] ) . '")'; } else { $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $key ) . '","' . esc_sql( $meta ) . '")'; } } } // Insert meta rows at once. if ( count( $meta_rows ) > 0 ) { $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}woocommerce_order_itemmeta ( order_item_id, meta_key, meta_value ) VALUES " . implode( ',', $meta_rows ) . ';', // @codingStandardsIgnoreLine $order_item_row->post_id ) ); } // Delete from DB (rename). $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_order_items_old' WHERE meta_key = '_order_items' AND post_id = %d", $order_item_row->post_id ) ); } unset( $meta_rows, $item_id, $order_item ); } } // Do the same kind of update for order_taxes - move to lines. // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_taxes' WHERE meta_key = '_order_taxes_old'. $order_tax_rows = $wpdb->get_results( "SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_taxes'" ); foreach ( $order_tax_rows as $order_tax_row ) { $order_taxes = (array) maybe_unserialize( $order_tax_row->meta_value ); if ( ! empty( $order_taxes ) ) { foreach ( $order_taxes as $order_tax ) { if ( ! isset( $order_tax['label'] ) || ! isset( $order_tax['cart_tax'] ) || ! isset( $order_tax['shipping_tax'] ) ) { continue; } $item_id = wc_add_order_item( $order_tax_row->post_id, array( 'order_item_name' => $order_tax['label'], 'order_item_type' => 'tax', ) ); // Add line item meta. if ( $item_id ) { wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) ); wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) ); wc_add_order_item_meta( $item_id, 'shipping_tax_amount', wc_clean( $order_tax['shipping_tax'] ) ); } // Delete from DB (rename). $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_order_taxes_old' WHERE meta_key = '_order_taxes' AND post_id = %d", $order_tax_row->post_id ) ); unset( $tax_amount ); } } } } /** * Update image settings for 2.0 * * @return void */ function wc_update_200_images() { // Grab the pre 2.0 Image options and use to populate the new image options settings, // cleaning up afterwards like nice people do. foreach ( array( 'catalog', 'single', 'thumbnail' ) as $value ) { $old_settings = array_filter( array( 'width' => get_option( 'woocommerce_' . $value . '_image_width' ), 'height' => get_option( 'woocommerce_' . $value . '_image_height' ), 'crop' => get_option( 'woocommerce_' . $value . '_image_crop' ), ) ); if ( ! empty( $old_settings ) && update_option( 'shop_' . $value . '_image_size', $old_settings ) ) { delete_option( 'woocommerce_' . $value . '_image_width' ); delete_option( 'woocommerce_' . $value . '_image_height' ); delete_option( 'woocommerce_' . $value . '_image_crop' ); } } } /** * Update DB version for 2.0 * * @return void */ function wc_update_200_db_version() { WC_Install::update_db_version( '2.0.0' ); } /** * Update Brazilian States for 2.0.9 * * @return void */ function wc_update_209_brazillian_state() { global $wpdb; // phpcs:disable WordPress.DB.SlowDBQuery // Update brazillian state codes. $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 'BA', ), array( 'meta_key' => '_billing_state', 'meta_value' => 'BH', ) ); $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 'BA', ), array( 'meta_key' => '_shipping_state', 'meta_value' => 'BH', ) ); $wpdb->update( $wpdb->usermeta, array( 'meta_value' => 'BA', ), array( 'meta_key' => 'billing_state', 'meta_value' => 'BH', ) ); $wpdb->update( $wpdb->usermeta, array( 'meta_value' => 'BA', ), array( 'meta_key' => 'shipping_state', 'meta_value' => 'BH', ) ); // phpcs:enable WordPress.DB.SlowDBQuery } /** * Update DB version for 2.0.9 * * @return void */ function wc_update_209_db_version() { WC_Install::update_db_version( '2.0.9' ); } /** * Remove pages for 2.1 * * @return void */ function wc_update_210_remove_pages() { // Pages no longer used. wp_trash_post( get_option( 'woocommerce_pay_page_id' ) ); wp_trash_post( get_option( 'woocommerce_thanks_page_id' ) ); wp_trash_post( get_option( 'woocommerce_view_order_page_id' ) ); wp_trash_post( get_option( 'woocommerce_change_password_page_id' ) ); wp_trash_post( get_option( 'woocommerce_edit_address_page_id' ) ); wp_trash_post( get_option( 'woocommerce_lost_password_page_id' ) ); } /** * Update file paths to support multiple files for 2.1 * * @return void */ function wc_update_210_file_paths() { global $wpdb; // Upgrade file paths to support multiple file paths + names etc. $existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_paths' AND meta_value != '';" ); if ( $existing_file_paths ) { foreach ( $existing_file_paths as $existing_file_path ) { $needs_update = false; $new_value = array(); $value = maybe_unserialize( trim( $existing_file_path->meta_value ) ); if ( $value ) { foreach ( $value as $key => $file ) { if ( ! is_array( $file ) ) { $needs_update = true; $new_value[ $key ] = array( 'file' => $file, 'name' => wc_get_filename_from_url( $file ), ); } else { $new_value[ $key ] = $file; } } if ( $needs_update ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize $new_value = serialize( $new_value ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) ); } } } } } /** * Update DB version for 2.1 * * @return void */ function wc_update_210_db_version() { WC_Install::update_db_version( '2.1.0' ); } /** * Update shipping options for 2.2 * * @return void */ function wc_update_220_shipping() { $woocommerce_ship_to_destination = 'shipping'; if ( get_option( 'woocommerce_ship_to_billing_address_only' ) === 'yes' ) { $woocommerce_ship_to_destination = 'billing_only'; } elseif ( get_option( 'woocommerce_ship_to_billing' ) === 'yes' ) { $woocommerce_ship_to_destination = 'billing'; } add_option( 'woocommerce_ship_to_destination', $woocommerce_ship_to_destination, '', 'no' ); } /** * Update order statuses for 2.2 * * @return void */ function wc_update_220_order_status() { global $wpdb; $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-pending' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'pending%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-processing' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'processing%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-on-hold' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'on-hold%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-completed' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'completed%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-cancelled' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'cancelled%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-refunded' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'refunded%';" ); $wpdb->query( "UPDATE {$wpdb->posts} as posts LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) LEFT JOIN {$wpdb->terms} AS term USING( term_id ) SET posts.post_status = 'wc-failed' WHERE posts.post_type = 'shop_order' AND posts.post_status = 'publish' AND tax.taxonomy = 'shop_order_status' AND term.slug LIKE 'failed%';" ); } /** * Update variations for 2.2 * * @return void */ function wc_update_220_variations() { global $wpdb; // Update variations which manage stock. $update_variations = $wpdb->get_results( "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock' LEFT OUTER JOIN {$wpdb->postmeta} as postmeta2 ON posts.ID = postmeta2.post_id AND postmeta2.meta_key = '_manage_stock' WHERE posts.post_type = 'product_variation' AND postmeta.meta_value IS NOT NULL AND postmeta.meta_value != '' AND postmeta2.meta_value IS NULL" ); foreach ( $update_variations as $variation ) { $parent_backorders = get_post_meta( $variation->variation_parent, '_backorders', true ); add_post_meta( $variation->variation_id, '_manage_stock', 'yes', true ); add_post_meta( $variation->variation_id, '_backorders', $parent_backorders ? $parent_backorders : 'no', true ); } } /** * Update attributes for 2.2 * * @return void */ function wc_update_220_attributes() { global $wpdb; // Update taxonomy names with correct sanitized names. $attribute_taxonomies = $wpdb->get_results( 'SELECT attribute_name, attribute_id FROM ' . $wpdb->prefix . 'woocommerce_attribute_taxonomies' ); foreach ( $attribute_taxonomies as $attribute_taxonomy ) { $sanitized_attribute_name = wc_sanitize_taxonomy_name( $attribute_taxonomy->attribute_name ); if ( $sanitized_attribute_name !== $attribute_taxonomy->attribute_name ) { if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT 1=1 FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $sanitized_attribute_name ) ) ) { // Update attribute. $wpdb->update( "{$wpdb->prefix}woocommerce_attribute_taxonomies", array( 'attribute_name' => $sanitized_attribute_name, ), array( 'attribute_id' => $attribute_taxonomy->attribute_id, ) ); // Update terms. $wpdb->update( $wpdb->term_taxonomy, array( 'taxonomy' => wc_attribute_taxonomy_name( $sanitized_attribute_name ) ), array( 'taxonomy' => 'pa_' . $attribute_taxonomy->attribute_name ) ); } } } delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); } /** * Update DB version for 2.2 * * @return void */ function wc_update_220_db_version() { WC_Install::update_db_version( '2.2.0' ); } /** * Update options for 2.3 * * @return void */ function wc_update_230_options() { // _money_spent and _order_count may be out of sync - clear them delete_metadata( 'user', 0, '_money_spent', '', true ); delete_metadata( 'user', 0, '_order_count', '', true ); delete_metadata( 'user', 0, '_last_order', '', true ); // To prevent taxes being hidden when using a default 'no address' in a store with tax inc prices, set the woocommerce_default_customer_address to use the store base address by default. if ( '' === get_option( 'woocommerce_default_customer_address', false ) && wc_prices_include_tax() ) { update_option( 'woocommerce_default_customer_address', 'base' ); } } /** * Update DB version for 2.3 * * @return void */ function wc_update_230_db_version() { WC_Install::update_db_version( '2.3.0' ); } /** * Update calc discount options for 2.4 * * @return void */ function wc_update_240_options() { /** * Coupon discount calculations. * Maintain the old coupon logic for upgrades. */ update_option( 'woocommerce_calc_discounts_sequentially', 'yes' ); } /** * Update shipping methods for 2.4 * * @return void */ function wc_update_240_shipping_methods() { /** * Flat Rate Shipping. * Update legacy options to new math based options. */ $shipping_methods = array( 'woocommerce_flat_rates' => new WC_Shipping_Legacy_Flat_Rate(), 'woocommerce_international_delivery_flat_rates' => new WC_Shipping_Legacy_International_Delivery(), ); foreach ( $shipping_methods as $flat_rate_option_key => $shipping_method ) { // Stop this running more than once if routine is repeated. if ( version_compare( $shipping_method->get_option( 'version', 0 ), '2.4.0', '<' ) ) { $shipping_classes = WC()->shipping()->get_shipping_classes(); $has_classes = count( $shipping_classes ) > 0; $cost_key = $has_classes ? 'no_class_cost' : 'cost'; $min_fee = $shipping_method->get_option( 'minimum_fee' ); $math_cost_strings = array( 'cost' => array(), 'no_class_cost' => array(), ); $math_cost_strings[ $cost_key ][] = $shipping_method->get_option( 'cost' ); $fee = $shipping_method->get_option( 'fee' ); if ( $fee ) { $math_cost_strings[ $cost_key ][] = strstr( $fee, '%' ) ? '[fee percent="' . str_replace( '%', '', $fee ) . '" min="' . esc_attr( $min_fee ) . '"]' : $fee; } foreach ( $shipping_classes as $shipping_class ) { $rate_key = 'class_cost_' . $shipping_class->slug; $math_cost_strings[ $rate_key ] = $math_cost_strings['no_class_cost']; } $flat_rates = array_filter( (array) get_option( $flat_rate_option_key, array() ) ); if ( $flat_rates ) { foreach ( $flat_rates as $shipping_class => $rate ) { $rate_key = 'class_cost_' . $shipping_class; if ( $rate['cost'] || $rate['fee'] ) { $math_cost_strings[ $rate_key ][] = $rate['cost']; $math_cost_strings[ $rate_key ][] = strstr( $rate['fee'], '%' ) ? '[fee percent="' . str_replace( '%', '', $rate['fee'] ) . '" min="' . esc_attr( $min_fee ) . '"]' : $rate['fee']; } } } if ( 'item' === $shipping_method->type ) { foreach ( $math_cost_strings as $key => $math_cost_string ) { $math_cost_strings[ $key ] = array_filter( array_map( 'trim', $math_cost_strings[ $key ] ) ); if ( ! empty( $math_cost_strings[ $key ] ) ) { $last_key = max( 0, count( $math_cost_strings[ $key ] ) - 1 ); $math_cost_strings[ $key ][0] = '( ' . $math_cost_strings[ $key ][0]; $math_cost_strings[ $key ][ $last_key ] .= ' ) * [qty]'; } } } $math_cost_strings['cost'][] = $shipping_method->get_option( 'cost_per_order' ); // Save settings. foreach ( $math_cost_strings as $option_id => $math_cost_string ) { $shipping_method->settings[ $option_id ] = implode( ' + ', array_filter( $math_cost_string ) ); } $shipping_method->settings['version'] = '2.4.0'; $shipping_method->settings['type'] = 'item' === $shipping_method->settings['type'] ? 'class' : $shipping_method->settings['type']; update_option( $shipping_method->plugin_id . $shipping_method->id . '_settings', $shipping_method->settings ); } } } /** * Update API keys for 2.4 * * @return void */ function wc_update_240_api_keys() { global $wpdb; /** * Update the old user API keys to the new Apps keys. */ $api_users = $wpdb->get_results( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'woocommerce_api_consumer_key'" ); $apps_keys = array(); // Get user data. foreach ( $api_users as $_user ) { $user = get_userdata( $_user->user_id ); $apps_keys[] = array( 'user_id' => $user->ID, 'permissions' => $user->woocommerce_api_key_permissions, 'consumer_key' => wc_api_hash( $user->woocommerce_api_consumer_key ), 'consumer_secret' => $user->woocommerce_api_consumer_secret, 'truncated_key' => substr( $user->woocommerce_api_consumer_secret, -7 ), ); } if ( ! empty( $apps_keys ) ) { // Create new apps. foreach ( $apps_keys as $app ) { $wpdb->insert( $wpdb->prefix . 'woocommerce_api_keys', $app, array( '%d', '%s', '%s', '%s', '%s', ) ); } // Delete old user keys from usermeta. foreach ( $api_users as $_user ) { $user_id = intval( $_user->user_id ); delete_user_meta( $user_id, 'woocommerce_api_consumer_key' ); delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' ); delete_user_meta( $user_id, 'woocommerce_api_key_permissions' ); } } } /** * Update webhooks for 2.4 * * @return void */ function wc_update_240_webhooks() { // phpcs:disable WordPress.DB.SlowDBQuery /** * Webhooks. * Make sure order.update webhooks get the woocommerce_order_edit_status hook. */ $order_update_webhooks = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'shop_webhook', 'meta_key' => '_topic', 'meta_value' => 'order.updated', ) ); foreach ( $order_update_webhooks as $order_update_webhook ) { $webhook = new WC_Webhook( $order_update_webhook->ID ); $webhook->set_topic( 'order.updated' ); } // phpcs:enable WordPress.DB.SlowDBQuery } /** * Update refunds for 2.4 * * @return void */ function wc_update_240_refunds() { global $wpdb; /** * Refunds for full refunded orders. * Update fully refunded orders to ensure they have a refund line item so reports add up. */ $refunded_orders = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'shop_order', 'post_status' => array( 'wc-refunded' ), ) ); // Ensure emails are disabled during this update routine. remove_all_actions( 'woocommerce_order_status_refunded_notification' ); remove_all_actions( 'woocommerce_order_partially_refunded_notification' ); remove_action( 'woocommerce_order_status_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); remove_action( 'woocommerce_order_partially_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); foreach ( $refunded_orders as $refunded_order ) { $order_total = get_post_meta( $refunded_order->ID, '_order_total', true ); $refunded_total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM( postmeta.meta_value ) FROM $wpdb->postmeta AS postmeta INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) WHERE postmeta.meta_key = '_refund_amount' AND postmeta.post_id = posts.ID", $refunded_order->ID ) ); if ( $order_total > $refunded_total ) { wc_create_refund( array( 'amount' => $order_total - $refunded_total, 'reason' => __( 'Order fully refunded', 'woocommerce' ), 'order_id' => $refunded_order->ID, 'line_items' => array(), 'date' => $refunded_order->post_modified, ) ); } } wc_delete_shop_order_transients(); } /** * Update DB version for 2.4 * * @return void */ function wc_update_240_db_version() { WC_Install::update_db_version( '2.4.0' ); } /** * Update variations for 2.4.1 * * @return void */ function wc_update_241_variations() { global $wpdb; // Select variations that don't have any _stock_status implemented on WooCommerce 2.2. $update_variations = $wpdb->get_results( "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status' WHERE posts.post_type = 'product_variation' AND postmeta.meta_value IS NULL" ); foreach ( $update_variations as $variation ) { // Get the parent _stock_status. $parent_stock_status = get_post_meta( $variation->variation_parent, '_stock_status', true ); // Set the _stock_status. add_post_meta( $variation->variation_id, '_stock_status', $parent_stock_status ? $parent_stock_status : 'instock', true ); // Delete old product children array. delete_transient( 'wc_product_children_' . $variation->variation_parent ); } // Invalidate old transients such as wc_var_price. WC_Cache_Helper::get_transient_version( 'product', true ); } /** * Update DB version for 2.4.1 * * @return void */ function wc_update_241_db_version() { WC_Install::update_db_version( '2.4.1' ); } /** * Update currency settings for 2.5 * * @return void */ function wc_update_250_currency() { global $wpdb; // Fix currency settings for LAK currency. $current_currency = get_option( 'woocommerce_currency' ); if ( 'KIP' === $current_currency ) { update_option( 'woocommerce_currency', 'LAK' ); } // phpcs:disable WordPress.DB.SlowDBQuery // Update LAK currency code. $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 'LAK', ), array( 'meta_key' => '_order_currency', 'meta_value' => 'KIP', ) ); // phpcs:enable WordPress.DB.SlowDBQuery } /** * Update DB version for 2.5 * * @return void */ function wc_update_250_db_version() { WC_Install::update_db_version( '2.5.0' ); } /** * Update ship to countries options for 2.6 * * @return void */ function wc_update_260_options() { // woocommerce_calc_shipping option has been removed in 2.6. if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { update_option( 'woocommerce_ship_to_countries', 'disabled' ); } WC_Admin_Notices::add_notice( 'legacy_shipping' ); } /** * Update term meta for 2.6 * * @return void */ function wc_update_260_termmeta() { global $wpdb; /** * Migrate term meta to WordPress tables. */ if ( get_option( 'db_version' ) >= 34370 && $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_termmeta';" ) ) { if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); wp_cache_flush(); } } } /** * Update zones for 2.6 * * @return void */ function wc_update_260_zones() { global $wpdb; /** * Old (table rate) shipping zones to new core shipping zones migration. * zone_enabled and zone_type are no longer used, but it's safe to leave them be. */ if ( $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_shipping_zones` LIKE 'zone_enabled';" ) ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_type` `zone_type` VARCHAR(40) NOT NULL DEFAULT '';" ); $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_enabled` `zone_enabled` INT(1) NOT NULL DEFAULT 1;" ); } } /** * Update zone methods for 2.6 * * @return void */ function wc_update_260_zone_methods() { global $wpdb; /** * Shipping zones in WC 2.6.0 use a table named woocommerce_shipping_zone_methods. * Migrate the old data out of woocommerce_shipping_zone_shipping_methods into the new table and port over any known options (used by table rates and flat rate boxes). */ if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_shipping_zone_shipping_methods';" ) ) { $old_methods = $wpdb->get_results( "SELECT zone_id, shipping_method_type, shipping_method_order, shipping_method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods;" ); if ( $old_methods ) { $max_new_id = $wpdb->get_var( "SELECT MAX(instance_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ); $max_old_id = $wpdb->get_var( "SELECT MAX(shipping_method_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods" ); // Avoid ID conflicts. $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods AUTO_INCREMENT = %d;", max( $max_new_id, $max_old_id ) + 1 ) ); // Store changes. $changes = array(); // Move data. foreach ( $old_methods as $old_method ) { $wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'zone_id' => $old_method->zone_id, 'method_id' => $old_method->shipping_method_type, 'method_order' => $old_method->shipping_method_order, ) ); $new_instance_id = $wpdb->insert_id; // Move main settings. $older_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '-' . $old_method->shipping_method_id . '_settings'; $old_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '_' . $old_method->shipping_method_id . '_settings'; add_option( 'woocommerce_' . $old_method->shipping_method_type . '_' . $new_instance_id . '_settings', get_option( $old_settings_key, get_option( $older_settings_key ) ) ); // Handling for table rate and flat rate box shipping. if ( 'table_rate' === $old_method->shipping_method_type ) { // Move priority settings. add_option( 'woocommerce_table_rate_default_priority_' . $new_instance_id, get_option( 'woocommerce_table_rate_default_priority_' . $old_method->shipping_method_id ) ); add_option( 'woocommerce_table_rate_priorities_' . $new_instance_id, get_option( 'woocommerce_table_rate_priorities_' . $old_method->shipping_method_id ) ); // Move rates. $wpdb->update( $wpdb->prefix . 'woocommerce_shipping_table_rates', array( 'shipping_method_id' => $new_instance_id, ), array( 'shipping_method_id' => $old_method->shipping_method_id, ) ); } elseif ( 'flat_rate_boxes' === $old_method->shipping_method_type ) { $wpdb->update( $wpdb->prefix . 'woocommerce_shipping_flat_rate_boxes', array( 'shipping_method_id' => $new_instance_id, ), array( 'shipping_method_id' => $old_method->shipping_method_id, ) ); } $changes[ $old_method->shipping_method_id ] = $new_instance_id; } // $changes contains keys (old method ids) and values (new instance ids) if extra processing is needed in plugins. // Store this to an option so extensions can pick it up later, then fire an action. update_option( 'woocommerce_updated_instance_ids', $changes ); do_action( 'woocommerce_updated_instance_ids', $changes ); } } // Change ranges used to ... $wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code = REPLACE( location_code, '-', '...' );" ); } /** * Update refunds for 2.6 * * @return void */ function wc_update_260_refunds() { global $wpdb; /** * Refund item qty should be negative. */ $wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_order_itemmeta as item_meta LEFT JOIN {$wpdb->prefix}woocommerce_order_items as items ON item_meta.order_item_id = items.order_item_id LEFT JOIN {$wpdb->posts} as posts ON items.order_id = posts.ID SET item_meta.meta_value = item_meta.meta_value * -1 WHERE item_meta.meta_value > 0 AND item_meta.meta_key = '_qty' AND posts.post_type = 'shop_order_refund'" ); } /** * Update DB version for 2.6 * * @return void */ function wc_update_260_db_version() { WC_Install::update_db_version( '2.6.0' ); } /** * Update webhooks for 3.0 * * @return void */ function wc_update_300_webhooks() { // phpcs:disable WordPress.DB.SlowDBQuery /** * Make sure product.update webhooks get the woocommerce_product_quick_edit_save * and woocommerce_product_bulk_edit_save hooks. */ $product_update_webhooks = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'shop_webhook', 'meta_key' => '_topic', 'meta_value' => 'product.updated', ) ); foreach ( $product_update_webhooks as $product_update_webhook ) { $webhook = new WC_Webhook( $product_update_webhook->ID ); $webhook->set_topic( 'product.updated' ); } // phpcs:enable WordPress.DB.SlowDBQuery } /** * Add an index to the field comment_type to improve the response time of the query * used by WC_Comments::wp_count_comments() to get the number of comments by type. */ function wc_update_300_comment_type_index() { global $wpdb; $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); if ( is_null( $index_exists ) ) { // Add an index to the field comment_type to improve the response time of the query // used by WC_Comments::wp_count_comments() to get the number of comments by type. $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); } } /** * Update grouped products for 3.0 * * @return void */ function wc_update_300_grouped_products() { global $wpdb; $parents = $wpdb->get_col( "SELECT DISTINCT( post_parent ) FROM {$wpdb->posts} WHERE post_parent > 0 AND post_type = 'product';" ); foreach ( $parents as $parent_id ) { $parent = wc_get_product( $parent_id ); if ( $parent && $parent->is_type( 'grouped' ) ) { $children_ids = get_posts( array( 'post_parent' => $parent_id, 'posts_per_page' => -1, 'post_type' => 'product', 'fields' => 'ids', ) ); update_post_meta( $parent_id, '_children', $children_ids ); // Update children to remove the parent. $wpdb->update( $wpdb->posts, array( 'post_parent' => 0, ), array( 'post_parent' => $parent_id, ) ); } } } /** * Update shipping tax classes for 3.0 * * @return void */ function wc_update_300_settings() { $woocommerce_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); if ( '' === $woocommerce_shipping_tax_class ) { update_option( 'woocommerce_shipping_tax_class', 'inherit' ); } elseif ( 'standard' === $woocommerce_shipping_tax_class ) { update_option( 'woocommerce_shipping_tax_class', '' ); } } /** * Convert meta values into term for product visibility. */ function wc_update_300_product_visibility() { global $wpdb; WC_Install::create_terms(); $featured_term = get_term_by( 'name', 'featured', 'product_visibility' ); if ( $featured_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_featured' AND meta_value = 'yes';", $featured_term->term_taxonomy_id ) ); } $exclude_search_term = get_term_by( 'name', 'exclude-from-search', 'product_visibility' ); if ( $exclude_search_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'catalog');", $exclude_search_term->term_taxonomy_id ) ); } $exclude_catalog_term = get_term_by( 'name', 'exclude-from-catalog', 'product_visibility' ); if ( $exclude_catalog_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'search');", $exclude_catalog_term->term_taxonomy_id ) ); } $outofstock_term = get_term_by( 'name', 'outofstock', 'product_visibility' ); if ( $outofstock_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = 'outofstock';", $outofstock_term->term_taxonomy_id ) ); } $rating_term = get_term_by( 'name', 'rated-1', 'product_visibility' ); if ( $rating_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 1;", $rating_term->term_taxonomy_id ) ); } $rating_term = get_term_by( 'name', 'rated-2', 'product_visibility' ); if ( $rating_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 2;", $rating_term->term_taxonomy_id ) ); } $rating_term = get_term_by( 'name', 'rated-3', 'product_visibility' ); if ( $rating_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 3;", $rating_term->term_taxonomy_id ) ); } $rating_term = get_term_by( 'name', 'rated-4', 'product_visibility' ); if ( $rating_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 4;", $rating_term->term_taxonomy_id ) ); } $rating_term = get_term_by( 'name', 'rated-5', 'product_visibility' ); if ( $rating_term ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 5;", $rating_term->term_taxonomy_id ) ); } } /** * Update DB Version. */ function wc_update_300_db_version() { WC_Install::update_db_version( '3.0.0' ); } /** * Add an index to the downloadable product permissions table to improve performance of update_user_by_order_id. */ function wc_update_310_downloadable_products() { global $wpdb; $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE column_name = 'order_id' and key_name = 'order_id'" ); if ( is_null( $index_exists ) ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX order_id (order_id)" ); } } /** * Find old order notes and ensure they have the correct type for exclusion. */ function wc_update_310_old_comments() { global $wpdb; $wpdb->query( "UPDATE $wpdb->comments comments LEFT JOIN $wpdb->posts as posts ON comments.comment_post_ID = posts.ID SET comment_type = 'order_note' WHERE posts.post_type = 'shop_order' AND comment_type = '';" ); } /** * Update DB Version. */ function wc_update_310_db_version() { WC_Install::update_db_version( '3.1.0' ); } /** * Update shop_manager capabilities. */ function wc_update_312_shop_manager_capabilities() { $role = get_role( 'shop_manager' ); $role->remove_cap( 'unfiltered_html' ); } /** * Update DB Version. */ function wc_update_312_db_version() { WC_Install::update_db_version( '3.1.2' ); } /** * Update state codes for Mexico. */ function wc_update_320_mexican_states() { global $wpdb; $mx_states = array( 'Distrito Federal' => 'CMX', 'Jalisco' => 'JAL', 'Nuevo Leon' => 'NLE', 'Aguascalientes' => 'AGS', 'Baja California' => 'BCN', 'Baja California Sur' => 'BCS', 'Campeche' => 'CAM', 'Chiapas' => 'CHP', 'Chihuahua' => 'CHH', 'Coahuila' => 'COA', 'Colima' => 'COL', 'Durango' => 'DGO', 'Guanajuato' => 'GTO', 'Guerrero' => 'GRO', 'Hidalgo' => 'HGO', 'Estado de Mexico' => 'MEX', 'Michoacan' => 'MIC', 'Morelos' => 'MOR', 'Nayarit' => 'NAY', 'Oaxaca' => 'OAX', 'Puebla' => 'PUE', 'Queretaro' => 'QRO', 'Quintana Roo' => 'ROO', 'San Luis Potosi' => 'SLP', 'Sinaloa' => 'SIN', 'Sonora' => 'SON', 'Tabasco' => 'TAB', 'Tamaulipas' => 'TMP', 'Tlaxcala' => 'TLA', 'Veracruz' => 'VER', 'Yucatan' => 'YUC', 'Zacatecas' => 'ZAC', ); foreach ( $mx_states as $old => $new ) { $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %s WHERE meta_key IN ( '_billing_state', '_shipping_state' ) AND meta_value = %s", $new, $old ) ); $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_locations", array( 'location_code' => 'MX:' . $new, ), array( 'location_code' => 'MX:' . $old, ) ); $wpdb->update( "{$wpdb->prefix}woocommerce_tax_rates", array( 'tax_rate_state' => strtoupper( $new ), ), array( 'tax_rate_state' => strtoupper( $old ), ) ); } } /** * Update DB Version. */ function wc_update_320_db_version() { WC_Install::update_db_version( '3.2.0' ); } /** * Update image settings to use new aspect ratios and widths. */ function wc_update_330_image_options() { $old_thumbnail_size = get_option( 'shop_catalog_image_size', array() ); $old_single_size = get_option( 'shop_single_image_size', array() ); if ( ! empty( $old_thumbnail_size['width'] ) ) { $width = absint( $old_thumbnail_size['width'] ); $height = absint( $old_thumbnail_size['height'] ); $hard_crop = ! empty( $old_thumbnail_size['crop'] ); if ( ! $width ) { $width = 300; } if ( ! $height ) { $height = $width; } update_option( 'woocommerce_thumbnail_image_width', $width ); // Calculate cropping mode from old image options. if ( ! $hard_crop ) { update_option( 'woocommerce_thumbnail_cropping', 'uncropped' ); } elseif ( $width === $height ) { update_option( 'woocommerce_thumbnail_cropping', '1:1' ); } else { $ratio = $width / $height; $fraction = wc_decimal_to_fraction( $ratio ); if ( $fraction ) { update_option( 'woocommerce_thumbnail_cropping', 'custom' ); update_option( 'woocommerce_thumbnail_cropping_custom_width', $fraction[0] ); update_option( 'woocommerce_thumbnail_cropping_custom_height', $fraction[1] ); } } } // Single is uncropped. if ( ! empty( $old_single_size['width'] ) ) { update_option( 'woocommerce_single_image_width', absint( $old_single_size['width'] ) ); } } /** * Migrate webhooks from post type to CRUD. */ function wc_update_330_webhooks() { register_post_type( 'shop_webhook' ); // Map statuses from post_type to Webhooks CRUD. $statuses = array( 'publish' => 'active', 'draft' => 'paused', 'pending' => 'disabled', ); $posts = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'shop_webhook', 'post_status' => 'any', ) ); foreach ( $posts as $post ) { $webhook = new WC_Webhook(); $webhook->set_name( $post->post_title ); $webhook->set_status( isset( $statuses[ $post->post_status ] ) ? $statuses[ $post->post_status ] : 'disabled' ); $webhook->set_delivery_url( get_post_meta( $post->ID, '_delivery_url', true ) ); $webhook->set_secret( get_post_meta( $post->ID, '_secret', true ) ); $webhook->set_topic( get_post_meta( $post->ID, '_topic', true ) ); $webhook->set_api_version( get_post_meta( $post->ID, '_api_version', true ) ); $webhook->set_user_id( $post->post_author ); $webhook->set_pending_delivery( false ); $webhook->save(); wp_delete_post( $post->ID, true ); } unregister_post_type( 'shop_webhook' ); } /** * Assign default cat to all products with no cats. */ function wc_update_330_set_default_product_cat() { /* * When a product category is deleted, we need to check * if the product has no categories assigned. Then assign * it a default category. */ wc_get_container()->get( AssignDefaultCategory::class )->maybe_assign_default_product_cat(); } /** * Update product stock status to use the new onbackorder status. */ function wc_update_330_product_stock_status() { global $wpdb; if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { return; } $min_stock_amount = (int) get_option( 'woocommerce_notify_no_stock_amount', 0 ); // Get all products that have stock management enabled, stock less than or equal to min stock amount, and backorders enabled. $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT t1.post_id FROM $wpdb->postmeta t1 INNER JOIN $wpdb->postmeta t2 ON t1.post_id = t2.post_id AND t1.meta_key = '_manage_stock' AND t1.meta_value = 'yes' AND t2.meta_key = '_stock' AND t2.meta_value <= %d INNER JOIN $wpdb->postmeta t3 ON t2.post_id = t3.post_id AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )", $min_stock_amount ) ); if ( empty( $post_ids ) ) { return; } $post_ids = array_map( 'absint', $post_ids ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared // Set the status to onbackorder for those products. $wpdb->query( "UPDATE $wpdb->postmeta SET meta_value = 'onbackorder' WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )' ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } /** * Clear addons page transients */ function wc_update_330_clear_transients() { delete_transient( 'wc_addons_sections' ); delete_transient( 'wc_addons_featured' ); } /** * Set PayPal's sandbox credentials. */ function wc_update_330_set_paypal_sandbox_credentials() { $paypal_settings = get_option( 'woocommerce_paypal_settings' ); if ( isset( $paypal_settings['testmode'] ) && 'yes' === $paypal_settings['testmode'] ) { foreach ( array( 'api_username', 'api_password', 'api_signature' ) as $credential ) { if ( ! empty( $paypal_settings[ $credential ] ) ) { $paypal_settings[ 'sandbox_' . $credential ] = $paypal_settings[ $credential ]; } } update_option( 'woocommerce_paypal_settings', $paypal_settings ); } } /** * Update DB Version. */ function wc_update_330_db_version() { WC_Install::update_db_version( '3.3.0' ); } /** * Update state codes for Ireland and BD. */ function wc_update_340_states() { $country_states = array( 'IE' => array( 'CK' => 'CO', 'DN' => 'D', 'GY' => 'G', 'TY' => 'TA', ), 'BD' => array( 'BAG' => 'BD-05', 'BAN' => 'BD-01', 'BAR' => 'BD-02', 'BARI' => 'BD-06', 'BHO' => 'BD-07', 'BOG' => 'BD-03', 'BRA' => 'BD-04', 'CHA' => 'BD-09', 'CHI' => 'BD-10', 'CHU' => 'BD-12', 'COX' => 'BD-11', 'COM' => 'BD-08', 'DHA' => 'BD-13', 'DIN' => 'BD-14', 'FAR' => 'BD-15', 'FEN' => 'BD-16', 'GAI' => 'BD-19', 'GAZI' => 'BD-18', 'GOP' => 'BD-17', 'HAB' => 'BD-20', 'JAM' => 'BD-21', 'JES' => 'BD-22', 'JHA' => 'BD-25', 'JHE' => 'BD-23', 'JOY' => 'BD-24', 'KHA' => 'BD-29', 'KHU' => 'BD-27', 'KIS' => 'BD-26', 'KUR' => 'BD-28', 'KUS' => 'BD-30', 'LAK' => 'BD-31', 'LAL' => 'BD-32', 'MAD' => 'BD-36', 'MAG' => 'BD-37', 'MAN' => 'BD-33', 'MEH' => 'BD-39', 'MOU' => 'BD-38', 'MUN' => 'BD-35', 'MYM' => 'BD-34', 'NAO' => 'BD-48', 'NAR' => 'BD-43', 'NARG' => 'BD-40', 'NARD' => 'BD-42', 'NAT' => 'BD-44', 'NAW' => 'BD-45', 'NET' => 'BD-41', 'NIL' => 'BD-46', 'NOA' => 'BD-47', 'PAB' => 'BD-49', 'PAN' => 'BD-52', 'PAT' => 'BD-51', 'PIR' => 'BD-50', 'RAJB' => 'BD-53', 'RAJ' => 'BD-54', 'RAN' => 'BD-56', 'RANP' => 'BD-55', 'SAT' => 'BD-58', 'SHA' => 'BD-57', 'SIR' => 'BD-59', 'SUN' => 'BD-61', 'SYL' => 'BD-60', 'TAN' => 'BD-63', 'THA' => 'BD-64', ), ); update_option( 'woocommerce_update_340_states', $country_states ); } /** * Update next state in the queue. * * @return bool True to run again, false if completed. */ function wc_update_340_state() { global $wpdb; $country_states = array_filter( (array) get_option( 'woocommerce_update_340_states', array() ) ); if ( empty( $country_states ) ) { return false; } foreach ( $country_states as $country => $states ) { foreach ( $states as $old => $new ) { $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %s WHERE meta_key IN ( '_billing_state', '_shipping_state' ) AND meta_value = %s", $new, $old ) ); $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_locations", array( 'location_code' => $country . ':' . $new, ), array( 'location_code' => $country . ':' . $old, ) ); $wpdb->update( "{$wpdb->prefix}woocommerce_tax_rates", array( 'tax_rate_state' => strtoupper( $new ), ), array( 'tax_rate_state' => strtoupper( $old ), ) ); unset( $country_states[ $country ][ $old ] ); if ( empty( $country_states[ $country ] ) ) { unset( $country_states[ $country ] ); } break 2; } } if ( ! empty( $country_states ) ) { return update_option( 'woocommerce_update_340_states', $country_states ); } delete_option( 'woocommerce_update_340_states' ); return false; } /** * Set last active prop for users. */ function wc_update_340_last_active() { global $wpdb; // @codingStandardsIgnoreStart. $wpdb->query( $wpdb->prepare( " INSERT INTO {$wpdb->usermeta} (user_id, meta_key, meta_value) SELECT DISTINCT users.ID, 'wc_last_active', %s FROM {$wpdb->users} as users LEFT OUTER JOIN {$wpdb->usermeta} AS usermeta ON users.ID = usermeta.user_id AND usermeta.meta_key = 'wc_last_active' WHERE usermeta.meta_value IS NULL ", (string) strtotime( date( 'Y-m-d', current_time( 'timestamp', true ) ) ) ) ); // @codingStandardsIgnoreEnd. } /** * Update DB Version. */ function wc_update_340_db_version() { WC_Install::update_db_version( '3.4.0' ); } /** * Remove duplicate foreign keys * * @return void */ function wc_update_343_cleanup_foreign_keys() { global $wpdb; $results = $wpdb->get_results( "SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' AND CONSTRAINT_NAME LIKE '%wc_download_log_ib%' AND CONSTRAINT_TYPE = 'FOREIGN KEY' AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" ); if ( $results ) { foreach ( $results as $fk ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } } /** * Update DB version. * * @return void */ function wc_update_343_db_version() { WC_Install::update_db_version( '3.4.3' ); } /** * Recreate user roles so existing users will get the new capabilities. * * @return void */ function wc_update_344_recreate_roles() { WC_Install::remove_roles(); WC_Install::create_roles(); } /** * Update DB version. * * @return void */ function wc_update_344_db_version() { WC_Install::update_db_version( '3.4.4' ); } /** * Set the comment type to 'review' for product reviews that don't have a comment type. */ function wc_update_350_reviews_comment_type() { global $wpdb; $wpdb->query( "UPDATE {$wpdb->prefix}comments JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}comments.comment_post_ID AND ( {$wpdb->prefix}posts.post_type = 'product' OR {$wpdb->prefix}posts.post_type = 'product_variation' ) SET {$wpdb->prefix}comments.comment_type = 'review' WHERE {$wpdb->prefix}comments.comment_type = ''" ); } /** * Update DB Version. */ function wc_update_350_db_version() { WC_Install::update_db_version( '3.5.0' ); } /** * Drop the fk_wc_download_log_permission_id FK as we use a new one with the table and blog prefix for MS compatability. * * @return void */ function wc_update_352_drop_download_log_fk() { global $wpdb; $results = $wpdb->get_results( "SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' AND CONSTRAINT_NAME = 'fk_wc_download_log_permission_id' AND CONSTRAINT_TYPE = 'FOREIGN KEY' AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" ); // We only need to drop the old key as WC_Install::create_tables() takes care of creating the new FK. if ( $results ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY fk_wc_download_log_permission_id" ); // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared } } /** * Remove edit_user capabilities from shop managers and use "translated" capabilities instead. * See wc_shop_manager_has_capability function. */ function wc_update_354_modify_shop_manager_caps() { global $wp_roles; if ( ! class_exists( 'WP_Roles' ) ) { return; } if ( ! isset( $wp_roles ) ) { $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine } $wp_roles->remove_cap( 'shop_manager', 'edit_users' ); } /** * Update DB Version. */ function wc_update_354_db_version() { WC_Install::update_db_version( '3.5.4' ); } /** * Update product lookup tables in bulk. */ function wc_update_360_product_lookup_tables() { wc_update_product_lookup_tables(); } /** * Renames ordering meta to be consistent across taxonomies. */ function wc_update_360_term_meta() { global $wpdb; $wpdb->query( "UPDATE {$wpdb->termmeta} SET meta_key = 'order' WHERE meta_key LIKE 'order_pa_%';" ); } /** * Add new user_order_remaining_expires to speed up user download permission fetching. * * @return void */ function wc_update_360_downloadable_product_permissions_index() { global $wpdb; $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE key_name = 'user_order_remaining_expires'" ); if ( is_null( $index_exists ) ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)" ); } } /** * Update DB Version. */ function wc_update_360_db_version() { WC_Install::update_db_version( '3.6.0' ); } /** * Put tax classes into a DB table. * * @return void */ function wc_update_370_tax_rate_classes() { global $wpdb; $classes = array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ); if ( $classes ) { foreach ( $classes as $class ) { if ( empty( $class ) ) { continue; } WC_Tax::create_tax_class( $class ); } } delete_option( 'woocommerce_tax_classes' ); } /** * Update currency settings for 3.7.0 * * @return void */ function wc_update_370_mro_std_currency() { global $wpdb; // Fix currency settings for MRU and STN currency. $current_currency = get_option( 'woocommerce_currency' ); if ( 'MRO' === $current_currency ) { update_option( 'woocommerce_currency', 'MRU' ); } if ( 'STD' === $current_currency ) { update_option( 'woocommerce_currency', 'STN' ); } // Update MRU currency code. $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 'MRU', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ), array( 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => 'MRO', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ) ); // Update STN currency code. $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 'STN', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ), array( 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => 'STD', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ) ); } /** * Update DB Version. */ function wc_update_370_db_version() { WC_Install::update_db_version( '3.7.0' ); } /** * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not * be publicly accessible. */ function wc_update_390_move_maxmind_database() { // Make sure to use all of the correct filters to pull the local database path. $old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 ); // Generate a prefix for the old file and store it in the integration as it would expect it. $prefix = wp_generate_password( 32, false ); update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) ); // Generate the new path in the same way that the integration will. $uploads_dir = wp_upload_dir(); $new_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb'; $new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 ); $new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @rename( $old_path, $new_path ); } /** * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days. */ function wc_update_390_change_geolocation_database_update_cron() { wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' ); } /** * Update DB version. */ function wc_update_390_db_version() { WC_Install::update_db_version( '3.9.0' ); } /** * Increase column size */ function wc_update_400_increase_size_of_column() { global $wpdb; $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `min_price` decimal(19,4) NULL default NULL" ); $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `max_price` decimal(19,4) NULL default NULL" ); } /** * Reset ActionScheduler migration status. Needs AS >= 3.0 shipped with WC >= 4.0. */ function wc_update_400_reset_action_scheduler_migration_status() { if ( class_exists( 'ActionScheduler_DataController' ) && method_exists( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) ) { \ActionScheduler_DataController::mark_migration_incomplete(); } } /** * Update DB version. */ function wc_update_400_db_version() { WC_Install::update_db_version( '4.0.0' ); } /** * Register attributes as terms for variable products, in increments of 100 products. * * This migration was added to support a new mechanism to improve the filtering of * variable products by attribute (https://github.com/woocommerce/woocommerce/pull/26260), * however that mechanism was later reverted (https://github.com/woocommerce/woocommerce/pull/27625) * due to numerous issues found. Thus the migration is no longer needed. * * @return bool true if the migration needs to be run again. */ function wc_update_440_insert_attribute_terms_for_variable_products() { return false; } /** * Update DB version. */ function wc_update_440_db_version() { WC_Install::update_db_version( '4.4.0' ); } /** * Update DB version to 4.5.0. */ function wc_update_450_db_version() { WC_Install::update_db_version( '4.5.0' ); } /** * Sanitize all coupons code. * * @return bool True to run again, false if completed. */ function wc_update_450_sanitize_coupons_code() { global $wpdb; $coupon_id = 0; $last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' ); $coupons = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10", $last_coupon_id ), ARRAY_A ); if ( empty( $coupons ) ) { delete_option( 'woocommerce_update_450_last_coupon_id' ); return false; } foreach ( $coupons as $key => $data ) { $coupon_id = intval( $data['ID'] ); $code = trim( wp_filter_kses( $data['post_title'] ) ); if ( ! empty( $code ) && $data['post_title'] !== $code ) { $wpdb->update( $wpdb->posts, array( 'post_title' => $code, ), array( 'ID' => $coupon_id, ), array( '%s', ), array( '%d', ) ); // Clean cache. clean_post_cache( $coupon_id ); wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' ); } } // Start the run again. if ( $coupon_id ) { return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id ); } delete_option( 'woocommerce_update_450_last_coupon_id' ); return false; } /** * Fixes product review count that might have been incorrect. * * See @link https://github.com/woocommerce/woocommerce/issues/27688. */ function wc_update_500_fix_product_review_count() { global $wpdb; $product_id = 0; $last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' ); $products_data = $wpdb->get_results( $wpdb->prepare( " SELECT post_id, meta_value FROM $wpdb->postmeta JOIN $wpdb->posts ON $wpdb->postmeta.post_id = $wpdb->posts.ID WHERE post_type = 'product' AND post_status = 'publish' AND post_id > %d AND meta_key = '_wc_review_count' ORDER BY post_id ASC LIMIT 10 ", $last_product_id ), ARRAY_A ); if ( empty( $products_data ) ) { delete_option( 'woocommerce_update_500_last_product_id' ); return false; } $product_ids_to_check = array_column( $products_data, 'post_id' ); $actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check ); foreach ( $products_data as $product_data ) { $product_id = intval( $product_data['post_id'] ); $current_review_count = intval( $product_data['meta_value'] ); if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) { WC_Comments::clear_transients( $product_id ); } } // Start the run again. if ( $product_id ) { return update_option( 'woocommerce_update_500_last_product_id', $product_id ); } delete_option( 'woocommerce_update_500_last_product_id' ); return false; } /** * Update DB version to 5.0.0. */ function wc_update_500_db_version() { WC_Install::update_db_version( '5.0.0' ); } /** * Creates the refund and returns policy page. * * See @link https://github.com/woocommerce/woocommerce/issues/29235. */ function wc_update_560_create_refund_returns_page() { /** * Filter on the pages created to return what we expect. * * @param array $pages The default WC pages. */ function filter_created_pages( $pages ) { $page_to_create = array( 'refund_returns' ); return array_intersect_key( $pages, array_flip( $page_to_create ) ); } add_filter( 'woocommerce_create_pages', 'filter_created_pages' ); WC_Install::create_pages(); remove_filter( 'woocommerce_create_pages', 'filter_created_pages' ); } /** * Update DB version to 5.6.0. */ function wc_update_560_db_version() { WC_Install::update_db_version( '5.6.0' ); } includes/class-wc-product-variation.php 0000644 00000041567 15132754524 0014274 0 ustar 00 <?php /** * Product Variation * * The WooCommerce product variation class handles product variation data. * * @package WooCommerce\Classes * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Product variation class. */ class WC_Product_Variation extends WC_Product_Simple { /** * Post type. * * @var string */ protected $post_type = 'product_variation'; /** * Parent data. * * @var array */ protected $parent_data = array( 'title' => '', 'sku' => '', 'manage_stock' => '', 'backorders' => '', 'stock_quantity' => '', 'weight' => '', 'length' => '', 'width' => '', 'height' => '', 'tax_class' => '', 'shipping_class_id' => '', 'image_id' => '', 'purchase_note' => '', ); /** * Override the default constructor to set custom defaults. * * @param int|WC_Product|object $product Product to init. */ public function __construct( $product = 0 ) { $this->data['tax_class'] = 'parent'; $this->data['attribute_summary'] = ''; parent::__construct( $product ); } /** * Prefix for action and filter hooks on data. * * @since 3.0.0 * @return string */ protected function get_hook_prefix() { return 'woocommerce_product_variation_get_'; } /** * Get internal type. * * @return string */ public function get_type() { return 'variation'; } /** * If the stock level comes from another product ID. * * @since 3.0.0 * @return int */ public function get_stock_managed_by_id() { return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id(); } /** * Get the product's title. For variations this is the parent product name. * * @return string */ public function get_title() { return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this ); } /** * Get product name with SKU or ID. Used within admin. * * @return string Formatted product name */ public function get_formatted_name() { if ( $this->get_sku() ) { $identifier = $this->get_sku(); } else { $identifier = '#' . $this->get_id(); } $formatted_variation_list = wc_get_formatted_variation( $this, true, true, true ); return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '<span class="description">' . $formatted_variation_list . '</span>'; } /** * Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false. * * @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true. * @return array of attributes and their values for this variation. */ public function get_variation_attributes( $with_prefix = true ) { $attributes = $this->get_attributes(); $variation_attributes = array(); $prefix = $with_prefix ? 'attribute_' : ''; foreach ( $attributes as $key => $value ) { $variation_attributes[ $prefix . $key ] = $value; } return $variation_attributes; } /** * Returns a single product attribute as a string. * * @param string $attribute to get. * @return string */ public function get_attribute( $attribute ) { $attributes = $this->get_attributes(); $attribute = sanitize_title( $attribute ); if ( isset( $attributes[ $attribute ] ) ) { $value = $attributes[ $attribute ]; $term = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false; return ! is_wp_error( $term ) && $term ? $term->name : $value; } $att_str = 'pa_' . $attribute; if ( isset( $attributes[ $att_str ] ) ) { $value = $attributes[ $att_str ]; $term = taxonomy_exists( $att_str ) ? get_term_by( 'slug', $value, $att_str ) : false; return ! is_wp_error( $term ) && $term ? $term->name : $value; } return ''; } /** * Wrapper for get_permalink. Adds this variations attributes to the URL. * * @param array|null $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes. * @return string */ public function get_permalink( $item_object = null ) { $url = get_permalink( $this->get_parent_id() ); if ( ! empty( $item_object['variation'] ) ) { $data = $item_object['variation']; } elseif ( ! empty( $item_object['item_meta_array'] ) ) { $data_keys = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) ); $data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' ); $data = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() ); } else { $data = $this->get_variation_attributes(); } $data = array_filter( $data, 'wc_array_filter_default_attributes' ); if ( empty( $data ) ) { return $url; } // Filter and encode keys and values so this is not broken by add_query_arg. $data = array_map( 'urlencode', $data ); $keys = array_map( 'urlencode', array_keys( $data ) ); return add_query_arg( array_combine( $keys, $data ), $url ); } /** * Get the add to url used mainly in loops. * * @return string */ public function add_to_cart_url() { $url = $this->is_purchasable() ? remove_query_arg( 'added-to-cart', add_query_arg( array( 'variation_id' => $this->get_id(), 'add-to-cart' => $this->get_parent_id(), ), $this->get_permalink() ) ) : $this->get_permalink(); return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); } /** * Get SKU (Stock-keeping unit) - product unique ID. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_sku( $context = 'view' ) { $value = $this->get_prop( 'sku', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this ); } return $value; } /** * Returns the product's weight. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_weight( $context = 'view' ) { $value = $this->get_prop( 'weight', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this ); } return $value; } /** * Returns the product length. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_length( $context = 'view' ) { $value = $this->get_prop( 'length', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this ); } return $value; } /** * Returns the product width. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_width( $context = 'view' ) { $value = $this->get_prop( 'width', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this ); } return $value; } /** * Returns the product height. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_height( $context = 'view' ) { $value = $this->get_prop( 'height', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this ); } return $value; } /** * Returns the tax class. * * Does not use get_prop so it can handle 'parent' inheritance correctly. * * @param string $context view, edit, or unfiltered. * @return string */ public function get_tax_class( $context = 'view' ) { $value = null; if ( array_key_exists( 'tax_class', $this->data ) ) { $value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class']; if ( 'edit' !== $context && 'parent' === $value ) { $value = $this->parent_data['tax_class']; } if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this ); } } return $value; } /** * Return if product manage stock. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return boolean|string true, false, or parent. */ public function get_manage_stock( $context = 'view' ) { $value = $this->get_prop( 'manage_stock', $context ); // Inherit value from parent. if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) { $value = 'parent'; } return $value; } /** * Returns number of items available for sale. * * @param string $context What the value is for. Valid values are view and edit. * @return int|null */ public function get_stock_quantity( $context = 'view' ) { $value = $this->get_prop( 'stock_quantity', $context ); // Inherit value from parent. if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { $value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this ); } return $value; } /** * Get backorders. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return string yes no or notify */ public function get_backorders( $context = 'view' ) { $value = $this->get_prop( 'backorders', $context ); // Inherit value from parent. if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { $value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this ); } return $value; } /** * Get main image ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_image_id( $context = 'view' ) { $image_id = $this->get_prop( 'image_id', $context ); if ( 'view' === $context && ! $image_id ) { $image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this ); } return $image_id; } /** * Get purchase note. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_purchase_note( $context = 'view' ) { $value = $this->get_prop( 'purchase_note', $context ); // Inherit value from parent. if ( 'view' === $context && empty( $value ) ) { $value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this ); } return $value; } /** * Get shipping class ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_shipping_class_id( $context = 'view' ) { $shipping_class_id = $this->get_prop( 'shipping_class_id', $context ); if ( 'view' === $context && ! $shipping_class_id ) { $shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this ); } return $shipping_class_id; } /** * Get catalog visibility. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_catalog_visibility( $context = 'view' ) { return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this ); } /** * Get attribute summary. * * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. * * @param string $context What the value is for. Valid values are view and edit. * * @since 3.6.0 * @return string */ public function get_attribute_summary( $context = 'view' ) { return $this->get_prop( 'attribute_summary', $context ); } /** * Set attribute summary. * * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. * * @since 3.6.0 * @param string $attribute_summary Summary of attribute names and values assigned to the variation. */ public function set_attribute_summary( $attribute_summary ) { $this->set_prop( 'attribute_summary', $attribute_summary ); } /* |-------------------------------------------------------------------------- | CRUD methods |-------------------------------------------------------------------------- */ /** * Set the parent data array for this variation. * * @since 3.0.0 * @param array $parent_data parent data array for this variation. */ public function set_parent_data( $parent_data ) { $parent_data = wp_parse_args( $parent_data, array( 'title' => '', 'status' => '', 'sku' => '', 'manage_stock' => 'no', 'backorders' => 'no', 'stock_quantity' => '', 'weight' => '', 'length' => '', 'width' => '', 'height' => '', 'tax_class' => '', 'shipping_class_id' => 0, 'image_id' => 0, 'purchase_note' => '', 'catalog_visibility' => 'visible', ) ); // Normalize tax class. $parent_data['tax_class'] = sanitize_title( $parent_data['tax_class'] ); $parent_data['tax_class'] = 'standard' === $parent_data['tax_class'] ? '' : $parent_data['tax_class']; $valid_classes = $this->get_valid_tax_classes(); if ( ! in_array( $parent_data['tax_class'], $valid_classes, true ) ) { $parent_data['tax_class'] = ''; } $this->parent_data = $parent_data; } /** * Get the parent data array for this variation. * * @since 3.0.0 * @return array */ public function get_parent_data() { return $this->parent_data; } /** * Set attributes. Unlike the parent product which uses terms, variations are assigned * specific attributes using name value pairs. * * @param array $raw_attributes array of raw attributes. */ public function set_attributes( $raw_attributes ) { $raw_attributes = (array) $raw_attributes; $attributes = array(); foreach ( $raw_attributes as $key => $value ) { // Remove attribute prefix which meta gets stored with. if ( 0 === strpos( $key, 'attribute_' ) ) { $key = substr( $key, 10 ); } $attributes[ $key ] = $value; } $this->set_prop( 'attributes', $attributes ); } /** * Returns whether or not the product has any visible attributes. * * Variations are mapped to specific attributes unlike products, and the return * value of ->get_attributes differs. Therefore this returns false. * * @return boolean */ public function has_attributes() { return false; } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- */ /** * Returns false if the product cannot be bought. * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable. * * @return bool */ public function is_purchasable() { return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable() && ( 'publish' === $this->parent_data['status'] || current_user_can( 'edit_post', $this->get_parent_id() ) ), $this ); } /** * Controls whether this particular variation will appear greyed-out (inactive) or not (active). * Used by extensions to make incompatible variations appear greyed-out, etc. * Other possible uses: prevent out-of-stock variations from being selected. * * @return bool */ public function variation_is_active() { return apply_filters( 'woocommerce_variation_is_active', true, $this ); } /** * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed. * Instead, a suitable 'unavailable' message is displayed. * Invisible by default: Disabled variations and variations with an empty price. * * @return bool */ public function variation_is_visible() { return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this ); } /** * Return valid tax classes. Adds 'parent' to the default list of valid tax classes. * * @return array valid tax classes */ protected function get_valid_tax_classes() { $valid_classes = WC_Tax::get_tax_class_slugs(); $valid_classes[] = 'parent'; return $valid_classes; } } includes/wc-account-functions.php 0000644 00000031652 15132754524 0013153 0 ustar 00 <?php /** * WooCommerce Account Functions * * Functions for account specific things. * * @package WooCommerce\Functions * @version 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Returns the url to the lost password endpoint url. * * @param string $default_url Default lost password URL. * @return string */ function wc_lostpassword_url( $default_url = '' ) { // Avoid loading too early. if ( ! did_action( 'init' ) ) { return $default_url; } // Don't redirect to the woocommerce endpoint on global network admin lost passwords. if ( is_multisite() && isset( $_GET['redirect_to'] ) && false !== strpos( wp_unslash( $_GET['redirect_to'] ), network_admin_url() ) ) { // WPCS: input var ok, sanitization ok, CSRF ok. return $default_url; } $wc_account_page_url = wc_get_page_permalink( 'myaccount' ); $wc_account_page_exists = wc_get_page_id( 'myaccount' ) > 0; $lost_password_endpoint = get_option( 'woocommerce_myaccount_lost_password_endpoint' ); if ( $wc_account_page_exists && ! empty( $lost_password_endpoint ) ) { return wc_get_endpoint_url( $lost_password_endpoint, '', $wc_account_page_url ); } else { return $default_url; } } add_filter( 'lostpassword_url', 'wc_lostpassword_url', 10, 1 ); /** * Get the link to the edit account details page. * * @return string */ function wc_customer_edit_account_url() { $edit_account_url = wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ); return apply_filters( 'woocommerce_customer_edit_account_url', $edit_account_url ); } /** * Get the edit address slug translation. * * @param string $id Address ID. * @param bool $flip Flip the array to make it possible to retrieve the values from both sides. * * @return string Address slug i18n. */ function wc_edit_address_i18n( $id, $flip = false ) { $slugs = apply_filters( 'woocommerce_edit_address_slugs', array( 'billing' => sanitize_title( _x( 'billing', 'edit-address-slug', 'woocommerce' ) ), 'shipping' => sanitize_title( _x( 'shipping', 'edit-address-slug', 'woocommerce' ) ), ) ); if ( $flip ) { $slugs = array_flip( $slugs ); } if ( ! isset( $slugs[ $id ] ) ) { return $id; } return $slugs[ $id ]; } /** * Get My Account menu items. * * @since 2.6.0 * @return array */ function wc_get_account_menu_items() { $endpoints = array( 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), ); $items = array( 'dashboard' => __( 'Dashboard', 'woocommerce' ), 'orders' => __( 'Orders', 'woocommerce' ), 'downloads' => __( 'Downloads', 'woocommerce' ), 'edit-address' => _n( 'Addresses', 'Address', (int) wc_shipping_enabled(), 'woocommerce' ), 'payment-methods' => __( 'Payment methods', 'woocommerce' ), 'edit-account' => __( 'Account details', 'woocommerce' ), 'customer-logout' => __( 'Logout', 'woocommerce' ), ); // Remove missing endpoints. foreach ( $endpoints as $endpoint_id => $endpoint ) { if ( empty( $endpoint ) ) { unset( $items[ $endpoint_id ] ); } } // Check if payment gateways support add new payment methods. if ( isset( $items['payment-methods'] ) ) { $support_payment_methods = false; foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway ) { if ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) { $support_payment_methods = true; break; } } if ( ! $support_payment_methods ) { unset( $items['payment-methods'] ); } } return apply_filters( 'woocommerce_account_menu_items', $items, $endpoints ); } /** * Get account menu item classes. * * @since 2.6.0 * @param string $endpoint Endpoint. * @return string */ function wc_get_account_menu_item_classes( $endpoint ) { global $wp; $classes = array( 'woocommerce-MyAccount-navigation-link', 'woocommerce-MyAccount-navigation-link--' . $endpoint, ); // Set current item class. $current = isset( $wp->query_vars[ $endpoint ] ); if ( 'dashboard' === $endpoint && ( isset( $wp->query_vars['page'] ) || empty( $wp->query_vars ) ) ) { $current = true; // Dashboard is not an endpoint, so needs a custom check. } elseif ( 'orders' === $endpoint && isset( $wp->query_vars['view-order'] ) ) { $current = true; // When looking at individual order, highlight Orders list item (to signify where in the menu the user currently is). } elseif ( 'payment-methods' === $endpoint && isset( $wp->query_vars['add-payment-method'] ) ) { $current = true; } if ( $current ) { $classes[] = 'is-active'; } $classes = apply_filters( 'woocommerce_account_menu_item_classes', $classes, $endpoint ); return implode( ' ', array_map( 'sanitize_html_class', $classes ) ); } /** * Get account endpoint URL. * * @since 2.6.0 * @param string $endpoint Endpoint. * @return string */ function wc_get_account_endpoint_url( $endpoint ) { if ( 'dashboard' === $endpoint ) { return wc_get_page_permalink( 'myaccount' ); } if ( 'customer-logout' === $endpoint ) { return wc_logout_url(); } return wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) ); } /** * Get My Account > Orders columns. * * @since 2.6.0 * @return array */ function wc_get_account_orders_columns() { $columns = apply_filters( 'woocommerce_account_orders_columns', array( 'order-number' => __( 'Order', 'woocommerce' ), 'order-date' => __( 'Date', 'woocommerce' ), 'order-status' => __( 'Status', 'woocommerce' ), 'order-total' => __( 'Total', 'woocommerce' ), 'order-actions' => __( 'Actions', 'woocommerce' ), ) ); // Deprecated filter since 2.6.0. return apply_filters( 'woocommerce_my_account_my_orders_columns', $columns ); } /** * Get My Account > Downloads columns. * * @since 2.6.0 * @return array */ function wc_get_account_downloads_columns() { $columns = apply_filters( 'woocommerce_account_downloads_columns', array( 'download-product' => __( 'Product', 'woocommerce' ), 'download-remaining' => __( 'Downloads remaining', 'woocommerce' ), 'download-expires' => __( 'Expires', 'woocommerce' ), 'download-file' => __( 'Download', 'woocommerce' ), 'download-actions' => ' ', ) ); if ( ! has_filter( 'woocommerce_account_download_actions' ) ) { unset( $columns['download-actions'] ); } return $columns; } /** * Get My Account > Payment methods columns. * * @since 2.6.0 * @return array */ function wc_get_account_payment_methods_columns() { return apply_filters( 'woocommerce_account_payment_methods_columns', array( 'method' => __( 'Method', 'woocommerce' ), 'expires' => __( 'Expires', 'woocommerce' ), 'actions' => ' ', ) ); } /** * Get My Account > Payment methods types * * @since 2.6.0 * @return array */ function wc_get_account_payment_methods_types() { return apply_filters( 'woocommerce_payment_methods_types', array( 'cc' => __( 'Credit card', 'woocommerce' ), 'echeck' => __( 'eCheck', 'woocommerce' ), ) ); } /** * Get account orders actions. * * @since 3.2.0 * @param int|WC_Order $order Order instance or ID. * @return array */ function wc_get_account_orders_actions( $order ) { if ( ! is_object( $order ) ) { $order_id = absint( $order ); $order = wc_get_order( $order_id ); } $actions = array( 'pay' => array( 'url' => $order->get_checkout_payment_url(), 'name' => __( 'Pay', 'woocommerce' ), ), 'view' => array( 'url' => $order->get_view_order_url(), 'name' => __( 'View', 'woocommerce' ), ), 'cancel' => array( 'url' => $order->get_cancel_order_url( wc_get_page_permalink( 'myaccount' ) ), 'name' => __( 'Cancel', 'woocommerce' ), ), ); if ( ! $order->needs_payment() ) { unset( $actions['pay'] ); } if ( ! in_array( $order->get_status(), apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ), true ) ) { unset( $actions['cancel'] ); } return apply_filters( 'woocommerce_my_account_my_orders_actions', $actions, $order ); } /** * Get account formatted address. * * @since 3.2.0 * @param string $address_type Address type. * Accepts: 'billing' or 'shipping'. * Default to 'billing'. * @param int $customer_id Customer ID. * Default to 0. * @return string */ function wc_get_account_formatted_address( $address_type = 'billing', $customer_id = 0 ) { $getter = "get_{$address_type}"; $address = array(); if ( 0 === $customer_id ) { $customer_id = get_current_user_id(); } $customer = new WC_Customer( $customer_id ); if ( is_callable( array( $customer, $getter ) ) ) { $address = $customer->$getter(); unset( $address['email'], $address['tel'] ); } return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_my_account_my_address_formatted_address', $address, $customer->get_id(), $address_type ) ); } /** * Returns an array of a user's saved payments list for output on the account tab. * * @since 2.6 * @param array $list List of payment methods passed from wc_get_customer_saved_methods_list(). * @param int $customer_id The customer to fetch payment methods for. * @return array Filtered list of customers payment methods. */ function wc_get_account_saved_payment_methods_list( $list, $customer_id ) { $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id ); foreach ( $payment_tokens as $payment_token ) { $delete_url = wc_get_endpoint_url( 'delete-payment-method', $payment_token->get_id() ); $delete_url = wp_nonce_url( $delete_url, 'delete-payment-method-' . $payment_token->get_id() ); $set_default_url = wc_get_endpoint_url( 'set-default-payment-method', $payment_token->get_id() ); $set_default_url = wp_nonce_url( $set_default_url, 'set-default-payment-method-' . $payment_token->get_id() ); $type = strtolower( $payment_token->get_type() ); $list[ $type ][] = array( 'method' => array( 'gateway' => $payment_token->get_gateway_id(), ), 'expires' => esc_html__( 'N/A', 'woocommerce' ), 'is_default' => $payment_token->is_default(), 'actions' => array( 'delete' => array( 'url' => $delete_url, 'name' => esc_html__( 'Delete', 'woocommerce' ), ), ), ); $key = key( array_slice( $list[ $type ], -1, 1, true ) ); if ( ! $payment_token->is_default() ) { $list[ $type ][ $key ]['actions']['default'] = array( 'url' => $set_default_url, 'name' => esc_html__( 'Make default', 'woocommerce' ), ); } $list[ $type ][ $key ] = apply_filters( 'woocommerce_payment_methods_list_item', $list[ $type ][ $key ], $payment_token ); } return $list; } add_filter( 'woocommerce_saved_payment_methods_list', 'wc_get_account_saved_payment_methods_list', 10, 2 ); /** * Controls the output for credit cards on the my account page. * * @since 2.6 * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token $payment_token The payment token associated with this method entry. * @return array Filtered item. */ function wc_get_account_saved_payment_methods_list_item_cc( $item, $payment_token ) { if ( 'cc' !== strtolower( $payment_token->get_type() ) ) { return $item; } $card_type = $payment_token->get_card_type(); $item['method']['last4'] = $payment_token->get_last4(); $item['method']['brand'] = ( ! empty( $card_type ) ? ucfirst( $card_type ) : esc_html__( 'Credit card', 'woocommerce' ) ); $item['expires'] = $payment_token->get_expiry_month() . '/' . substr( $payment_token->get_expiry_year(), -2 ); return $item; } add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_cc', 10, 2 ); /** * Controls the output for eChecks on the my account page. * * @since 2.6 * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token $payment_token The payment token associated with this method entry. * @return array Filtered item. */ function wc_get_account_saved_payment_methods_list_item_echeck( $item, $payment_token ) { if ( 'echeck' !== strtolower( $payment_token->get_type() ) ) { return $item; } $item['method']['last4'] = $payment_token->get_last4(); $item['method']['brand'] = esc_html__( 'eCheck', 'woocommerce' ); return $item; } add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_echeck', 10, 2 ); includes/shortcodes/class-wc-shortcode-checkout.php 0000644 00000025004 15132754524 0016560 0 ustar 00 <?php /** * Checkout Shortcode * * Used on the checkout page, the checkout shortcode displays the checkout process. * * @package WooCommerce\Shortcodes\Checkout * @version 2.0.0 */ defined( 'ABSPATH' ) || exit; /** * Shortcode checkout class. */ class WC_Shortcode_Checkout { /** * Get the shortcode content. * * @param array $atts Shortcode attributes. * @return string */ public static function get( $atts ) { return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts ); } /** * Output the shortcode. * * @param array $atts Shortcode attributes. */ public static function output( $atts ) { global $wp; // Check cart class is loaded or abort. if ( is_null( WC()->cart ) ) { return; } // Backwards compatibility with old pay and thanks link arguments. if ( isset( $_GET['order'] ) && isset( $_GET['key'] ) ) { // WPCS: input var ok, CSRF ok. wc_deprecated_argument( __CLASS__ . '->' . __FUNCTION__, '2.1', '"order" is no longer used to pass an order ID. Use the order-pay or order-received endpoint instead.' ); // Get the order to work out what we are showing. $order_id = absint( $_GET['order'] ); // WPCS: input var ok. $order = wc_get_order( $order_id ); if ( $order && $order->has_status( 'pending' ) ) { $wp->query_vars['order-pay'] = absint( $_GET['order'] ); // WPCS: input var ok. } else { $wp->query_vars['order-received'] = absint( $_GET['order'] ); // WPCS: input var ok. } } // Handle checkout actions. if ( ! empty( $wp->query_vars['order-pay'] ) ) { self::order_pay( $wp->query_vars['order-pay'] ); } elseif ( isset( $wp->query_vars['order-received'] ) ) { self::order_received( $wp->query_vars['order-received'] ); } else { self::checkout(); } } /** * Show the pay page. * * @throws Exception When validate fails. * @param int $order_id Order ID. */ private static function order_pay( $order_id ) { do_action( 'before_woocommerce_pay' ); $order_id = absint( $order_id ); // Pay for existing order. if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok. try { $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. $order = wc_get_order( $order_id ); // Order or payment link is invalid. if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { throw new Exception( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ) ); } // Logged out customer does not have permission to pay for this order. if ( ! current_user_can( 'pay_for_order', $order_id ) && ! is_user_logged_in() ) { echo '<div class="woocommerce-info">' . esc_html__( 'Please log in to your account below to continue to the payment form.', 'woocommerce' ) . '</div>'; woocommerce_login_form( array( 'redirect' => $order->get_checkout_payment_url(), ) ); return; } // Add notice if logged in customer is trying to pay for guest order. if ( ! $order->get_user_id() && is_user_logged_in() ) { // If order has does not have same billing email then current logged in user then show warning. if ( $order->get_billing_email() !== wp_get_current_user()->user_email ) { wc_print_notice( __( 'You are paying for a guest order. Please continue with payment only if you recognize this order.', 'woocommerce' ), 'error' ); } } // Logged in customer trying to pay for someone else's order. if ( ! current_user_can( 'pay_for_order', $order_id ) ) { throw new Exception( __( 'This order cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ) ); } // Does not need payment. if ( ! $order->needs_payment() ) { /* translators: %s: order status */ throw new Exception( sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ) ); } // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) { $quantities = array(); foreach ( $order->get_items() as $item_key => $item ) { if ( $item && is_callable( array( $item, 'get_product' ) ) ) { $product = $item->get_product(); if ( ! $product ) { continue; } $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity(); } } foreach ( $order->get_items() as $item_key => $item ) { if ( $item && is_callable( array( $item, 'get_product' ) ) ) { $product = $item->get_product(); if ( ! $product ) { continue; } if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) { /* translators: %s: product name */ throw new Exception( sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) ); } // We only need to check products managing stock, with a limited stock qty. if ( ! $product->managing_stock() || $product->backorders_allowed() ) { continue; } // Check stock based on all items in the cart and consider any held stock within pending orders. $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() ); $required_stock = $quantities[ $product->get_stock_managed_by_id() ]; if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) { /* translators: 1: product name 2: quantity in stock */ throw new Exception( sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) ); } } } } WC()->customer->set_props( array( 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, ) ); WC()->customer->save(); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( count( $available_gateways ) ) { current( $available_gateways )->set_current(); } wc_get_template( 'checkout/form-pay.php', array( 'order' => $order, 'available_gateways' => $available_gateways, 'order_button_text' => apply_filters( 'woocommerce_pay_order_button_text', __( 'Pay for order', 'woocommerce' ) ), ) ); } catch ( Exception $e ) { wc_print_notice( $e->getMessage(), 'error' ); } } elseif ( $order_id ) { // Pay for order after checkout step. $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. $order = wc_get_order( $order_id ); if ( $order && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { if ( $order->needs_payment() ) { wc_get_template( 'checkout/order-receipt.php', array( 'order' => $order ) ); } else { /* translators: %s: order status */ wc_print_notice( sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ), 'error' ); } } else { wc_print_notice( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ), 'error' ); } } else { wc_print_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); } do_action( 'after_woocommerce_pay' ); } /** * Show the thanks page. * * @param int $order_id Order ID. */ private static function order_received( $order_id = 0 ) { $order = false; // Get the order. $order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $order_id ) ); $order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) ); // WPCS: input var ok, CSRF ok. if ( $order_id > 0 ) { $order = wc_get_order( $order_id ); if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) { $order = false; } } // Empty awaiting payment session. unset( WC()->session->order_awaiting_payment ); // In case order is created from admin, but paid by the actual customer, store the ip address of the payer // when they visit the payment confirmation page. if ( $order && $order->is_created_via( 'admin' ) ) { $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); $order->save(); } // Empty current cart. wc_empty_cart(); wc_get_template( 'checkout/thankyou.php', array( 'order' => $order ) ); } /** * Show the checkout. */ private static function checkout() { // Show non-cart errors. do_action( 'woocommerce_before_checkout_form_cart_notices' ); // Check cart has contents. if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { return; } // Check cart contents for errors. do_action( 'woocommerce_check_cart_items' ); // Calc totals. WC()->cart->calculate_totals(); // Get checkout object. $checkout = WC()->checkout(); if ( empty( $_POST ) && wc_notice_count( 'error' ) > 0 ) { // WPCS: input var ok, CSRF ok. wc_get_template( 'checkout/cart-errors.php', array( 'checkout' => $checkout ) ); wc_clear_notices(); } else { $non_js_checkout = ! empty( $_POST['woocommerce_checkout_update_totals'] ); // WPCS: input var ok, CSRF ok. if ( wc_notice_count( 'error' ) === 0 && $non_js_checkout ) { wc_add_notice( __( 'The order totals have been updated. Please confirm your order by pressing the "Place order" button at the bottom of the page.', 'woocommerce' ) ); } wc_get_template( 'checkout/form-checkout.php', array( 'checkout' => $checkout ) ); } } } includes/shortcodes/class-wc-shortcode-cart.php 0000644 00000007223 15132754524 0015707 0 ustar 00 <?php /** * Cart Shortcode * * Used on the cart page, the cart shortcode displays the cart contents and interface for coupon codes and other cart bits and pieces. * * @package WooCommerce\Shortcodes\Cart * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Shortcode cart class. */ class WC_Shortcode_Cart { /** * Calculate shipping for the cart. * * @throws Exception When some data is invalid. */ public static function calculate_shipping() { try { WC()->shipping()->reset_shipping(); $address = array(); $address['country'] = isset( $_POST['calc_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_country'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. $address['state'] = isset( $_POST['calc_shipping_state'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_state'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. $address['postcode'] = isset( $_POST['calc_shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_postcode'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. $address['city'] = isset( $_POST['calc_shipping_city'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_city'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. $address = apply_filters( 'woocommerce_cart_calculate_shipping_address', $address ); if ( $address['postcode'] && ! WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) { throw new Exception( __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ) ); } elseif ( $address['postcode'] ) { $address['postcode'] = wc_format_postcode( $address['postcode'], $address['country'] ); } if ( $address['country'] ) { if ( ! WC()->customer->get_billing_first_name() ) { WC()->customer->set_billing_location( $address['country'], $address['state'], $address['postcode'], $address['city'] ); } WC()->customer->set_shipping_location( $address['country'], $address['state'], $address['postcode'], $address['city'] ); } else { WC()->customer->set_billing_address_to_base(); WC()->customer->set_shipping_address_to_base(); } WC()->customer->set_calculated_shipping( true ); WC()->customer->save(); wc_add_notice( __( 'Shipping costs updated.', 'woocommerce' ), 'notice' ); do_action( 'woocommerce_calculated_shipping' ); } catch ( Exception $e ) { if ( ! empty( $e ) ) { wc_add_notice( $e->getMessage(), 'error' ); } } } /** * Output the cart shortcode. * * @param array $atts Shortcode attributes. */ public static function output( $atts ) { if ( ! apply_filters( 'woocommerce_output_cart_shortcode_content', true ) ) { return; } // Constants. wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); $atts = shortcode_atts( array(), $atts, 'woocommerce_cart' ); $nonce_value = wc_get_var( $_REQUEST['woocommerce-shipping-calculator-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. // Update Shipping. Nonce check uses new value and old value (woocommerce-cart). @todo remove in 4.0. if ( ! empty( $_POST['calc_shipping'] ) && ( wp_verify_nonce( $nonce_value, 'woocommerce-shipping-calculator' ) || wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) ) { // WPCS: input var ok. self::calculate_shipping(); // Also calc totals before we check items so subtotals etc are up to date. WC()->cart->calculate_totals(); } // Check cart items are valid. do_action( 'woocommerce_check_cart_items' ); // Calc totals. WC()->cart->calculate_totals(); if ( WC()->cart->is_empty() ) { wc_get_template( 'cart/cart-empty.php' ); } else { wc_get_template( 'cart/cart.php' ); } } } includes/shortcodes/class-wc-shortcode-products.php 0000644 00000051121 15132754524 0016615 0 ustar 00 <?php /** * Products shortcode * * @package WooCommerce\Shortcodes * @version 3.2.4 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Products shortcode class. */ class WC_Shortcode_Products { /** * Shortcode type. * * @since 3.2.0 * @var string */ protected $type = 'products'; /** * Attributes. * * @since 3.2.0 * @var array */ protected $attributes = array(); /** * Query args. * * @since 3.2.0 * @var array */ protected $query_args = array(); /** * Set custom visibility. * * @since 3.2.0 * @var bool */ protected $custom_visibility = false; /** * Initialize shortcode. * * @since 3.2.0 * @param array $attributes Shortcode attributes. * @param string $type Shortcode type. */ public function __construct( $attributes = array(), $type = 'products' ) { $this->type = $type; $this->attributes = $this->parse_attributes( $attributes ); $this->query_args = $this->parse_query_args(); } /** * Get shortcode attributes. * * @since 3.2.0 * @return array */ public function get_attributes() { return $this->attributes; } /** * Get query args. * * @since 3.2.0 * @return array */ public function get_query_args() { return $this->query_args; } /** * Get shortcode type. * * @since 3.2.0 * @return string */ public function get_type() { return $this->type; } /** * Get shortcode content. * * @since 3.2.0 * @return string */ public function get_content() { return $this->product_loop(); } /** * Parse attributes. * * @since 3.2.0 * @param array $attributes Shortcode attributes. * @return array */ protected function parse_attributes( $attributes ) { $attributes = $this->parse_legacy_attributes( $attributes ); $attributes = shortcode_atts( array( 'limit' => '-1', // Results limit. 'columns' => '', // Number of columns. 'rows' => '', // Number of rows. If defined, limit will be ignored. 'orderby' => '', // menu_order, title, date, rand, price, popularity, rating, or id. 'order' => '', // ASC or DESC. 'ids' => '', // Comma separated IDs. 'skus' => '', // Comma separated SKUs. 'category' => '', // Comma separated category slugs or ids. 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. 'attribute' => '', // Single attribute slug. 'terms' => '', // Comma separated term slugs or ids. 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. 'tag' => '', // Comma separated tag slugs. 'tag_operator' => 'IN', // Operator to compare tags. Possible values are 'IN', 'NOT IN', 'AND'. 'visibility' => 'visible', // Product visibility setting. Possible values are 'visible', 'catalog', 'search', 'hidden'. 'class' => '', // HTML class. 'page' => 1, // Page for pagination. 'paginate' => false, // Should results be paginated. 'cache' => true, // Should shortcode output be cached. ), $attributes, $this->type ); if ( ! absint( $attributes['columns'] ) ) { $attributes['columns'] = wc_get_default_products_per_row(); } return $attributes; } /** * Parse legacy attributes. * * @since 3.2.0 * @param array $attributes Attributes. * @return array */ protected function parse_legacy_attributes( $attributes ) { $mapping = array( 'per_page' => 'limit', 'operator' => 'cat_operator', 'filter' => 'terms', ); foreach ( $mapping as $old => $new ) { if ( isset( $attributes[ $old ] ) ) { $attributes[ $new ] = $attributes[ $old ]; unset( $attributes[ $old ] ); } } return $attributes; } /** * Parse query args. * * @since 3.2.0 * @return array */ protected function parse_query_args() { $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, 'no_found_rows' => false === wc_string_to_bool( $this->attributes['paginate'] ), 'orderby' => empty( $_GET['orderby'] ) ? $this->attributes['orderby'] : wc_clean( wp_unslash( $_GET['orderby'] ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended ); $orderby_value = explode( '-', $query_args['orderby'] ); $orderby = esc_attr( $orderby_value[0] ); $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : strtoupper( $this->attributes['order'] ); $query_args['orderby'] = $orderby; $query_args['order'] = $order; if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { $this->attributes['page'] = absint( empty( $_GET['product-page'] ) ? 1 : $_GET['product-page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } if ( ! empty( $this->attributes['rows'] ) ) { $this->attributes['limit'] = $this->attributes['columns'] * $this->attributes['rows']; } $ordering_args = WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ); $query_args['orderby'] = $ordering_args['orderby']; $query_args['order'] = $ordering_args['order']; if ( $ordering_args['meta_key'] ) { $query_args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key } $query_args['posts_per_page'] = intval( $this->attributes['limit'] ); if ( 1 < $this->attributes['page'] ) { $query_args['paged'] = absint( $this->attributes['page'] ); } $query_args['meta_query'] = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $query_args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query // Visibility. $this->set_visibility_query_args( $query_args ); // SKUs. $this->set_skus_query_args( $query_args ); // IDs. $this->set_ids_query_args( $query_args ); // Set specific types query args. if ( method_exists( $this, "set_{$this->type}_query_args" ) ) { $this->{"set_{$this->type}_query_args"}( $query_args ); } // Attributes. $this->set_attributes_query_args( $query_args ); // Categories. $this->set_categories_query_args( $query_args ); // Tags. $this->set_tags_query_args( $query_args ); $query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $this->attributes, $this->type ); // Always query only IDs. $query_args['fields'] = 'ids'; return $query_args; } /** * Set skus query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_skus_query_args( &$query_args ) { if ( ! empty( $this->attributes['skus'] ) ) { $skus = array_map( 'trim', explode( ',', $this->attributes['skus'] ) ); $query_args['meta_query'][] = array( 'key' => '_sku', 'value' => 1 === count( $skus ) ? $skus[0] : $skus, 'compare' => 1 === count( $skus ) ? '=' : 'IN', ); } } /** * Set ids query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_ids_query_args( &$query_args ) { if ( ! empty( $this->attributes['ids'] ) ) { $ids = array_map( 'trim', explode( ',', $this->attributes['ids'] ) ); if ( 1 === count( $ids ) ) { $query_args['p'] = $ids[0]; } else { $query_args['post__in'] = $ids; } } } /** * Set attributes query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_attributes_query_args( &$query_args ) { if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) { $taxonomy = strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] ); $terms = $this->attributes['terms'] ? array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ) : array(); $field = 'slug'; if ( $terms && is_numeric( $terms[0] ) ) { $field = 'term_id'; $terms = array_map( 'absint', $terms ); // Check numeric slugs. foreach ( $terms as $term ) { $the_term = get_term_by( 'slug', $term, $taxonomy ); if ( false !== $the_term ) { $terms[] = $the_term->term_id; } } } // If no terms were specified get all products that are in the attribute taxonomy. if ( ! $terms ) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'fields' => 'ids', ) ); $field = 'term_id'; } // We always need to search based on the slug as well, this is to accommodate numeric slugs. $query_args['tax_query'][] = array( 'taxonomy' => $taxonomy, 'terms' => $terms, 'field' => $field, 'operator' => $this->attributes['terms_operator'], ); } } /** * Set categories query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_categories_query_args( &$query_args ) { if ( ! empty( $this->attributes['category'] ) ) { $categories = array_map( 'sanitize_title', explode( ',', $this->attributes['category'] ) ); $field = 'slug'; if ( is_numeric( $categories[0] ) ) { $field = 'term_id'; $categories = array_map( 'absint', $categories ); // Check numeric slugs. foreach ( $categories as $cat ) { $the_cat = get_term_by( 'slug', $cat, 'product_cat' ); if ( false !== $the_cat ) { $categories[] = $the_cat->term_id; } } } $query_args['tax_query'][] = array( 'taxonomy' => 'product_cat', 'terms' => $categories, 'field' => $field, 'operator' => $this->attributes['cat_operator'], /* * When cat_operator is AND, the children categories should be excluded, * as only products belonging to all the children categories would be selected. */ 'include_children' => 'AND' === $this->attributes['cat_operator'] ? false : true, ); } } /** * Set tags query args. * * @since 3.3.0 * @param array $query_args Query args. */ protected function set_tags_query_args( &$query_args ) { if ( ! empty( $this->attributes['tag'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => 'product_tag', 'terms' => array_map( 'sanitize_title', explode( ',', $this->attributes['tag'] ) ), 'field' => 'slug', 'operator' => $this->attributes['tag_operator'], ); } } /** * Set sale products query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_sale_products_query_args( &$query_args ) { $query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() ); } /** * Set best selling products query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_best_selling_products_query_args( &$query_args ) { $query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key $query_args['order'] = 'DESC'; $query_args['orderby'] = 'meta_value_num'; } /** * Set top rated products query args. * * @since 3.6.5 * @param array $query_args Query args. */ protected function set_top_rated_products_query_args( &$query_args ) { $query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key $query_args['order'] = 'DESC'; $query_args['orderby'] = 'meta_value_num'; } /** * Set visibility as hidden. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_visibility_hidden_query_args( &$query_args ) { $this->custom_visibility = true; $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), 'field' => 'name', 'operator' => 'AND', 'include_children' => false, ); } /** * Set visibility as catalog. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_visibility_catalog_query_args( &$query_args ) { $this->custom_visibility = true; $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => 'exclude-from-search', 'field' => 'name', 'operator' => 'IN', 'include_children' => false, ); $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => 'exclude-from-catalog', 'field' => 'name', 'operator' => 'NOT IN', 'include_children' => false, ); } /** * Set visibility as search. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_visibility_search_query_args( &$query_args ) { $this->custom_visibility = true; $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => 'exclude-from-catalog', 'field' => 'name', 'operator' => 'IN', 'include_children' => false, ); $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => 'exclude-from-search', 'field' => 'name', 'operator' => 'NOT IN', 'include_children' => false, ); } /** * Set visibility as featured. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_visibility_featured_query_args( &$query_args ) { $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'terms' => 'featured', 'field' => 'name', 'operator' => 'IN', 'include_children' => false, ); } /** * Set visibility query args. * * @since 3.2.0 * @param array $query_args Query args. */ protected function set_visibility_query_args( &$query_args ) { if ( method_exists( $this, 'set_visibility_' . $this->attributes['visibility'] . '_query_args' ) ) { $this->{'set_visibility_' . $this->attributes['visibility'] . '_query_args'}( $query_args ); } else { $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } } /** * Set product as visible when querying for hidden products. * * @since 3.2.0 * @param bool $visibility Product visibility. * @return bool */ public function set_product_as_visible( $visibility ) { return $this->custom_visibility ? true : $visibility; } /** * Get wrapper classes. * * @since 3.2.0 * @param int $columns Number of columns. * @return array */ protected function get_wrapper_classes( $columns ) { $classes = array( 'woocommerce' ); if ( 'product' !== $this->type ) { $classes[] = 'columns-' . $columns; } $classes[] = $this->attributes['class']; return $classes; } /** * Generate and return the transient name for this shortcode based on the query args. * * @since 3.3.0 * @return string */ protected function get_transient_name() { $transient_name = 'wc_product_loop_' . md5( wp_json_encode( $this->query_args ) . $this->type ); if ( 'rand' === $this->query_args['orderby'] ) { // When using rand, we'll cache a number of random queries and pull those to avoid querying rand on each page load. $rand_index = wp_rand( 0, max( 1, absint( apply_filters( 'woocommerce_product_query_max_rand_cache_count', 5 ) ) ) ); $transient_name .= $rand_index; } return $transient_name; } /** * Run the query and return an array of data, including queried ids and pagination information. * * @since 3.3.0 * @return object Object with the following props; ids, per_page, found_posts, max_num_pages, current_page */ protected function get_query_results() { $transient_name = $this->get_transient_name(); $transient_version = WC_Cache_Helper::get_transient_version( 'product_query' ); $cache = wc_string_to_bool( $this->attributes['cache'] ) === true; $transient_value = $cache ? get_transient( $transient_name ) : false; if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { $results = $transient_value['value']; } else { $query = new WP_Query( $this->query_args ); $paginated = ! $query->get( 'no_found_rows' ); $results = (object) array( 'ids' => wp_parse_id_list( $query->posts ), 'total' => $paginated ? (int) $query->found_posts : count( $query->posts ), 'total_pages' => $paginated ? (int) $query->max_num_pages : 1, 'per_page' => (int) $query->get( 'posts_per_page' ), 'current_page' => $paginated ? (int) max( 1, $query->get( 'paged', 1 ) ) : 1, ); if ( $cache ) { $transient_value = array( 'version' => $transient_version, 'value' => $results, ); set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); } } // Remove ordering query arguments which may have been added by get_catalog_ordering_args. WC()->query->remove_ordering_args(); /** * Filter shortcode products query results. * * @since 4.0.0 * @param stdClass $results Query results. * @param WC_Shortcode_Products $this WC_Shortcode_Products instance. */ return apply_filters( 'woocommerce_shortcode_products_query_results', $results, $this ); } /** * Loop over found products. * * @since 3.2.0 * @return string */ protected function product_loop() { $columns = absint( $this->attributes['columns'] ); $classes = $this->get_wrapper_classes( $columns ); $products = $this->get_query_results(); ob_start(); if ( $products && $products->ids ) { // Prime caches to reduce future queries. if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $products->ids ); } // Setup the loop. wc_setup_loop( array( 'columns' => $columns, 'name' => $this->type, 'is_shortcode' => true, 'is_search' => false, 'is_paginated' => wc_string_to_bool( $this->attributes['paginate'] ), 'total' => $products->total, 'total_pages' => $products->total_pages, 'per_page' => $products->per_page, 'current_page' => $products->current_page, ) ); $original_post = $GLOBALS['post']; do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes ); // Fire standard shop loop hooks when paginating results so we can show result counts and so on. if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { do_action( 'woocommerce_before_shop_loop' ); } woocommerce_product_loop_start(); if ( wc_get_loop_prop( 'total' ) ) { foreach ( $products->ids as $product_id ) { $GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited setup_postdata( $GLOBALS['post'] ); // Set custom product visibility when quering hidden products. add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); // Render product template. wc_get_template_part( 'content', 'product' ); // Restore product visibility. remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); } } $GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited woocommerce_product_loop_end(); // Fire standard shop loop hooks when paginating results so we can show result counts and so on. if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { do_action( 'woocommerce_after_shop_loop' ); } do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes ); wp_reset_postdata(); wc_reset_loop(); } else { do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes ); } return '<div class="' . esc_attr( implode( ' ', $classes ) ) . '">' . ob_get_clean() . '</div>'; } /** * Order by rating. * * @since 3.2.0 * @param array $args Query args. * @return array */ public static function order_by_rating_post_clauses( $args ) { global $wpdb; $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; $args['join'] .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)"; $args['orderby'] = "$wpdb->commentmeta.meta_value DESC"; $args['groupby'] = "$wpdb->posts.ID"; return $args; } } includes/shortcodes/class-wc-shortcode-my-account.php 0000644 00000027420 15132754524 0017036 0 ustar 00 <?php /** * My Account Shortcodes * * Shows the 'my account' section where the customer can view past orders and update their information. * * @package WooCommerce\Shortcodes\My_Account * @version 2.0.0 */ defined( 'ABSPATH' ) || exit; /** * Shortcode my account class. */ class WC_Shortcode_My_Account { /** * Get the shortcode content. * * @param array $atts Shortcode attributes. * * @return string */ public static function get( $atts ) { return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts ); } /** * Output the shortcode. * * @param array $atts Shortcode attributes. */ public static function output( $atts ) { global $wp; // Check cart class is loaded or abort. if ( is_null( WC()->cart ) ) { return; } if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) { $message = apply_filters( 'woocommerce_my_account_message', '' ); if ( ! empty( $message ) ) { wc_add_notice( $message ); } // After password reset, add confirmation message. if ( ! empty( $_GET['password-reset'] ) ) { // WPCS: input var ok, CSRF ok. wc_add_notice( __( 'Your password has been reset successfully.', 'woocommerce' ) ); } if ( isset( $wp->query_vars['lost-password'] ) ) { self::lost_password(); } else { wc_get_template( 'myaccount/form-login.php' ); } } else { // Start output buffer since the html may need discarding for BW compatibility. ob_start(); if ( isset( $wp->query_vars['customer-logout'] ) ) { /* translators: %s: logout url */ wc_add_notice( sprintf( __( 'Are you sure you want to log out? <a href="%s">Confirm and log out</a>', 'woocommerce' ), wc_logout_url() ) ); } // Collect notices before output. $notices = wc_get_notices(); // Output the new account page. self::my_account( $atts ); /** * Deprecated my-account.php template handling. This code should be * removed in a future release. * * If woocommerce_account_content did not run, this is an old template * so we need to render the endpoint content again. */ if ( ! did_action( 'woocommerce_account_content' ) ) { if ( ! empty( $wp->query_vars ) ) { foreach ( $wp->query_vars as $key => $value ) { if ( 'pagename' === $key ) { continue; } if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { ob_clean(); // Clear previous buffer. wc_set_notices( $notices ); wc_print_notices(); do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); break; } } wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' ); } } // Send output buffer. ob_end_flush(); } } /** * My account page. * * @param array $atts Shortcode attributes. */ private static function my_account( $atts ) { $args = shortcode_atts( array( 'order_count' => 15, // @deprecated 2.6.0. Keep for backward compatibility. ), $atts, 'woocommerce_my_account' ); wc_get_template( 'myaccount/my-account.php', array( 'current_user' => get_user_by( 'id', get_current_user_id() ), 'order_count' => 'all' === $args['order_count'] ? -1 : $args['order_count'], ) ); } /** * View order page. * * @param int $order_id Order ID. */ public static function view_order( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || ! current_user_can( 'view_order', $order_id ) ) { echo '<div class="woocommerce-error">' . esc_html__( 'Invalid order.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '" class="wc-forward">' . esc_html__( 'My account', 'woocommerce' ) . '</a></div>'; return; } // Backwards compatibility. $status = new stdClass(); $status->name = wc_get_order_status_name( $order->get_status() ); wc_get_template( 'myaccount/view-order.php', array( 'status' => $status, // @deprecated 2.2. 'order' => $order, 'order_id' => $order->get_id(), ) ); } /** * Edit account details page. */ public static function edit_account() { wc_get_template( 'myaccount/form-edit-account.php', array( 'user' => get_user_by( 'id', get_current_user_id() ) ) ); } /** * Edit address page. * * @param string $load_address Type of address to load. */ public static function edit_address( $load_address = 'billing' ) { $current_user = wp_get_current_user(); $load_address = sanitize_key( $load_address ); $country = get_user_meta( get_current_user_id(), $load_address . '_country', true ); if ( ! $country ) { $country = WC()->countries->get_base_country(); } if ( 'billing' === $load_address ) { $allowed_countries = WC()->countries->get_allowed_countries(); if ( ! array_key_exists( $country, $allowed_countries ) ) { $country = current( array_keys( $allowed_countries ) ); } } if ( 'shipping' === $load_address ) { $allowed_countries = WC()->countries->get_shipping_countries(); if ( ! array_key_exists( $country, $allowed_countries ) ) { $country = current( array_keys( $allowed_countries ) ); } } $address = WC()->countries->get_address_fields( $country, $load_address . '_' ); // Enqueue scripts. wp_enqueue_script( 'wc-country-select' ); wp_enqueue_script( 'wc-address-i18n' ); // Prepare values. foreach ( $address as $key => $field ) { $value = get_user_meta( get_current_user_id(), $key, true ); if ( ! $value ) { switch ( $key ) { case 'billing_email': case 'shipping_email': $value = $current_user->user_email; break; } } $address[ $key ]['value'] = apply_filters( 'woocommerce_my_account_edit_address_field_value', $value, $key, $load_address ); } wc_get_template( 'myaccount/form-edit-address.php', array( 'load_address' => $load_address, 'address' => apply_filters( 'woocommerce_address_to_edit', $address, $load_address ), ) ); } /** * Lost password page handling. */ public static function lost_password() { /** * After sending the reset link, don't show the form again. */ if ( ! empty( $_GET['reset-link-sent'] ) ) { // WPCS: input var ok, CSRF ok. return wc_get_template( 'myaccount/lost-password-confirmation.php' ); /** * Process reset key / login from email confirmation link */ } elseif ( ! empty( $_GET['show-reset-form'] ) ) { // WPCS: input var ok, CSRF ok. if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 0 < strpos( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ], ':' ) ) { // @codingStandardsIgnoreLine list( $rp_id, $rp_key ) = array_map( 'wc_clean', explode( ':', wp_unslash( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ), 2 ) ); // @codingStandardsIgnoreLine $userdata = get_userdata( absint( $rp_id ) ); $rp_login = $userdata ? $userdata->user_login : ''; $user = self::check_password_reset_key( $rp_key, $rp_login ); // Reset key / login is correct, display reset password form with hidden key / login values. if ( is_object( $user ) ) { return wc_get_template( 'myaccount/form-reset-password.php', array( 'key' => $rp_key, 'login' => $rp_login, ) ); } } } // Show lost password form by default. wc_get_template( 'myaccount/form-lost-password.php', array( 'form' => 'lost_password', ) ); } /** * Handles sending password retrieval email to customer. * * Based on retrieve_password() in core wp-login.php. * * @uses $wpdb WordPress Database object * @return bool True: when finish. False: on error */ public static function retrieve_password() { $login = isset( $_POST['user_login'] ) ? sanitize_user( wp_unslash( $_POST['user_login'] ) ) : ''; // WPCS: input var ok, CSRF ok. if ( empty( $login ) ) { wc_add_notice( __( 'Enter a username or email address.', 'woocommerce' ), 'error' ); return false; } else { // Check on username first, as customers can use emails as usernames. $user_data = get_user_by( 'login', $login ); } // If no user found, check if it login is email and lookup user based on email. if ( ! $user_data && is_email( $login ) && apply_filters( 'woocommerce_get_username_from_email', true ) ) { $user_data = get_user_by( 'email', $login ); } $errors = new WP_Error(); do_action( 'lostpassword_post', $errors, $user_data ); if ( $errors->get_error_code() ) { wc_add_notice( $errors->get_error_message(), 'error' ); return false; } if ( ! $user_data ) { wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' ); return false; } if ( is_multisite() && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' ); return false; } // Redefining user_login ensures we return the right case in the email. $user_login = $user_data->user_login; do_action( 'retrieve_password', $user_login ); $allow = apply_filters( 'allow_password_reset', true, $user_data->ID ); if ( ! $allow ) { wc_add_notice( __( 'Password reset is not allowed for this user', 'woocommerce' ), 'error' ); return false; } elseif ( is_wp_error( $allow ) ) { wc_add_notice( $allow->get_error_message(), 'error' ); return false; } // Get password reset key (function introduced in WordPress 4.4). $key = get_password_reset_key( $user_data ); // Send email notification. WC()->mailer(); // Load email classes. do_action( 'woocommerce_reset_password_notification', $user_login, $key ); return true; } /** * Retrieves a user row based on password reset key and login. * * @uses $wpdb WordPress Database object. * @param string $key Hash to validate sending user's password. * @param string $login The user login. * @return WP_User|bool User's database row on success, false for invalid keys */ public static function check_password_reset_key( $key, $login ) { // Check for the password reset key. // Get user data or an error message in case of invalid or expired key. $user = check_password_reset_key( $key, $login ); if ( is_wp_error( $user ) ) { wc_add_notice( __( 'This key is invalid or has already been used. Please reset your password again if needed.', 'woocommerce' ), 'error' ); return false; } return $user; } /** * Handles resetting the user's password. * * @param object $user The user. * @param string $new_pass New password for the user in plaintext. */ public static function reset_password( $user, $new_pass ) { do_action( 'password_reset', $user, $new_pass ); wp_set_password( $new_pass, $user->ID ); self::set_reset_password_cookie(); if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) { wp_password_change_notification( $user ); } } /** * Set or unset the cookie. * * @param string $value Cookie value. */ public static function set_reset_password_cookie( $value = '' ) { $rp_cookie = 'wp-resetpass-' . COOKIEHASH; $rp_path = isset( $_SERVER['REQUEST_URI'] ) ? current( explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : ''; // WPCS: input var ok, sanitization ok. if ( $value ) { setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); } else { setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); } } /** * Show the add payment method page. */ public static function add_payment_method() { if ( ! is_user_logged_in() ) { wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit(); } else { do_action( 'before_woocommerce_add_payment_method' ); wc_get_template( 'myaccount/form-add-payment-method.php' ); do_action( 'after_woocommerce_add_payment_method' ); } } } includes/shortcodes/class-wc-shortcode-order-tracking.php 0000644 00000004372 15132754524 0017673 0 ustar 00 <?php /** * Order Tracking Shortcode * * Lets a user see the status of an order by entering their order details. * * @package WooCommerce\Shortcodes\Order_Tracking * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Shortcode order tracking class. */ class WC_Shortcode_Order_Tracking { /** * Get the shortcode content. * * @param array $atts Shortcode attributes. * @return string */ public static function get( $atts ) { return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts ); } /** * Output the shortcode. * * @param array $atts Shortcode attributes. */ public static function output( $atts ) { // Check cart class is loaded or abort. if ( is_null( WC()->cart ) ) { return; } $atts = shortcode_atts( array(), $atts, 'woocommerce_order_tracking' ); $nonce_value = wc_get_var( $_REQUEST['woocommerce-order-tracking-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( isset( $_REQUEST['orderid'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-order_tracking' ) ) { // WPCS: input var ok. $order_id = empty( $_REQUEST['orderid'] ) ? 0 : ltrim( wc_clean( wp_unslash( $_REQUEST['orderid'] ) ), '#' ); // WPCS: input var ok. $order_email = empty( $_REQUEST['order_email'] ) ? '' : sanitize_email( wp_unslash( $_REQUEST['order_email'] ) ); // WPCS: input var ok. if ( ! $order_id ) { wc_print_notice( __( 'Please enter a valid order ID', 'woocommerce' ), 'error' ); } elseif ( ! $order_email ) { wc_print_notice( __( 'Please enter a valid email address', 'woocommerce' ), 'error' ); } else { $order = wc_get_order( apply_filters( 'woocommerce_shortcode_order_tracking_order_id', $order_id ) ); if ( $order && $order->get_id() && strtolower( $order->get_billing_email() ) === strtolower( $order_email ) ) { do_action( 'woocommerce_track_order', $order->get_id() ); wc_get_template( 'order/tracking.php', array( 'order' => $order, ) ); return; } else { wc_print_notice( __( 'Sorry, the order could not be found. Please contact us if you are having difficulty finding your order details.', 'woocommerce' ), 'error' ); } } } wc_get_template( 'order/form-tracking.php' ); } } includes/class-wc-cache-helper.php 0000644 00000027431 15132754524 0013134 0 ustar 00 <?php /** * WC_Cache_Helper class. * * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Cache_Helper. */ class WC_Cache_Helper { /** * Transients to delete on shutdown. * * @var array Array of transient keys. */ private static $delete_transients = array(); /** * Hook in methods. */ public static function init() { add_filter( 'nocache_headers', array( __CLASS__, 'additional_nocache_headers' ), 10 ); add_action( 'shutdown', array( __CLASS__, 'delete_transients_on_shutdown' ), 10 ); add_action( 'template_redirect', array( __CLASS__, 'geolocation_ajax_redirect' ) ); add_action( 'wc_ajax_update_order_review', array( __CLASS__, 'update_geolocation_hash' ), 5 ); add_action( 'admin_notices', array( __CLASS__, 'notices' ) ); add_action( 'delete_version_transients', array( __CLASS__, 'delete_version_transients' ), 10 ); add_action( 'wp', array( __CLASS__, 'prevent_caching' ) ); add_action( 'clean_term_cache', array( __CLASS__, 'clean_term_cache' ), 10, 2 ); add_action( 'edit_terms', array( __CLASS__, 'clean_term_cache' ), 10, 2 ); } /** * Set additional nocache headers. * * @param array $headers Header names and field values. * @since 3.6.0 */ public static function additional_nocache_headers( $headers ) { $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $set_cache = false; /** * Allow plugins to enable nocache headers. Enabled for Google weblight. * * @param bool $enable_nocache_headers Flag indicating whether to add nocache headers. Default: false. */ if ( apply_filters( 'woocommerce_enable_nocache_headers', false ) ) { $set_cache = true; } /** * Enabled for Google weblight. * * @see https://support.google.com/webmasters/answer/1061943?hl=en */ if ( false !== strpos( $agent, 'googleweblight' ) ) { // no-transform: Opt-out of Google weblight. https://support.google.com/webmasters/answer/6211428?hl=en. $set_cache = true; } if ( false !== strpos( $agent, 'Chrome' ) && is_cart() ) { $set_cache = true; } if ( $set_cache ) { $headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate'; } return $headers; } /** * Add a transient to delete on shutdown. * * @since 3.6.0 * @param string|array $keys Transient key or keys. */ public static function queue_delete_transient( $keys ) { self::$delete_transients = array_unique( array_merge( is_array( $keys ) ? $keys : array( $keys ), self::$delete_transients ) ); } /** * Transients that don't need to be cleaned right away can be deleted on shutdown to avoid repetition. * * @since 3.6.0 */ public static function delete_transients_on_shutdown() { if ( self::$delete_transients ) { foreach ( self::$delete_transients as $key ) { delete_transient( $key ); } self::$delete_transients = array(); } } /** * Used to clear layered nav counts based on passed attribute names. * * @since 3.6.0 * @param array $attribute_keys Attribute keys. */ public static function invalidate_attribute_count( $attribute_keys ) { if ( $attribute_keys ) { foreach ( $attribute_keys as $attribute_key ) { self::queue_delete_transient( 'wc_layered_nav_counts_' . $attribute_key ); } } } /** * Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once. * * @param string $group Group of cache to get. * @return string */ public static function get_cache_prefix( $group ) { // Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed. $prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group ); if ( false === $prefix ) { $prefix = microtime(); wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group ); } return 'wc_cache_' . $prefix . '_'; } /** * Increment group cache prefix (invalidates cache). * * @param string $group Group of cache to clear. */ public static function incr_cache_prefix( $group ) { wc_deprecated_function( 'WC_Cache_Helper::incr_cache_prefix', '3.9.0', 'WC_Cache_Helper::invalidate_cache_group' ); self::invalidate_cache_group( $group ); } /** * Invalidate cache group. * * @param string $group Group of cache to clear. * @since 3.9.0 */ public static function invalidate_cache_group( $group ) { wp_cache_set( 'wc_' . $group . '_cache_prefix', microtime(), $group ); } /** * Get a hash of the customer location. * * @return string */ public static function geolocation_ajax_get_location_hash() { $customer = new WC_Customer( 0, true ); $location = array(); $location['country'] = $customer->get_billing_country(); $location['state'] = $customer->get_billing_state(); $location['postcode'] = $customer->get_billing_postcode(); $location['city'] = $customer->get_billing_city(); return apply_filters( 'woocommerce_geolocation_ajax_get_location_hash', substr( md5( implode( '', $location ) ), 0, 12 ), $location, $customer ); } /** * Prevent caching on certain pages */ public static function prevent_caching() { if ( ! is_blog_installed() ) { return; } $page_ids = array_filter( array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ) ) ); if ( is_page( $page_ids ) ) { self::set_nocache_constants(); nocache_headers(); } } /** * When using geolocation via ajax, to bust cache, redirect if the location hash does not equal the querystring. * * This prevents caching of the wrong data for this request. */ public static function geolocation_ajax_redirect() { if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) && ! is_checkout() && ! is_cart() && ! is_account_page() && ! is_ajax() && empty( $_POST ) ) { // WPCS: CSRF ok, input var ok. $location_hash = self::geolocation_ajax_get_location_hash(); $current_hash = isset( $_GET['v'] ) ? wc_clean( wp_unslash( $_GET['v'] ) ) : ''; // WPCS: sanitization ok, input var ok, CSRF ok. if ( empty( $current_hash ) || $current_hash !== $location_hash ) { global $wp; $redirect_url = trailingslashit( home_url( $wp->request ) ); if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { // WPCS: Input var ok. $redirect_url = add_query_arg( wp_unslash( $_SERVER['QUERY_STRING'] ), '', $redirect_url ); // WPCS: sanitization ok, Input var ok. } if ( ! get_option( 'permalink_structure' ) ) { $redirect_url = add_query_arg( $wp->query_string, '', $redirect_url ); } $redirect_url = add_query_arg( 'v', $location_hash, remove_query_arg( 'v', $redirect_url ) ); wp_safe_redirect( esc_url_raw( $redirect_url ), 307 ); exit; } } } /** * Updates the `woocommerce_geo_hash` cookie, which is used to help ensure we display * the correct pricing etc to customers, according to their billing country. * * Note that: * * A) This only sets the cookie if the default customer address is set to "Geolocate (with * Page Caching Support)". * * B) It is hooked into the `wc_ajax_update_order_review` action, which has the benefit of * ensuring we update the cookie any time the billing country is changed. */ public static function update_geolocation_hash() { if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { wc_setcookie( 'woocommerce_geo_hash', static::geolocation_ajax_get_location_hash(), time() + HOUR_IN_SECONDS ); } } /** * Get transient version. * * When using transients with unpredictable names, e.g. those containing an md5 * hash in the name, we need a way to invalidate them all at once. * * When using default WP transients we're able to do this with a DB query to * delete transients manually. * * With external cache however, this isn't possible. Instead, this function is used * to append a unique string (based on time()) to each transient. When transients * are invalidated, the transient version will increment and data will be regenerated. * * Raised in issue https://github.com/woocommerce/woocommerce/issues/5777. * Adapted from ideas in http://tollmanz.com/invalidation-schemes/. * * @param string $group Name for the group of transients we need to invalidate. * @param boolean $refresh true to force a new version. * @return string transient version based on time(), 10 digits. */ public static function get_transient_version( $group, $refresh = false ) { $transient_name = $group . '-transient-version'; $transient_value = get_transient( $transient_name ); if ( false === $transient_value || true === $refresh ) { $transient_value = (string) time(); set_transient( $transient_name, $transient_value ); } return $transient_value; } /** * Set constants to prevent caching by some plugins. * * @param mixed $return Value to return. Previously hooked into a filter. * @return mixed */ public static function set_nocache_constants( $return = true ) { wc_maybe_define_constant( 'DONOTCACHEPAGE', true ); wc_maybe_define_constant( 'DONOTCACHEOBJECT', true ); wc_maybe_define_constant( 'DONOTCACHEDB', true ); return $return; } /** * Notices function. */ public static function notices() { if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) { return; } $config = w3_instance( 'W3_Config' ); $enabled = $config->get_integer( 'dbcache.enabled' ); $settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) ); if ( $enabled && ! in_array( '_wc_session_', $settings, true ) ) { ?> <div class="error"> <p> <?php /* translators: 1: key 2: URL */ echo wp_kses_post( sprintf( __( 'In order for <strong>database caching</strong> to work with WooCommerce you must add %1$s to the "Ignored Query Strings" option in <a href="%2$s">W3 Total Cache settings</a>.', 'woocommerce' ), '<code>_wc_session_</code>', esc_url( admin_url( 'admin.php?page=w3tc_dbcache' ) ) ) ); ?> </p> </div> <?php } } /** * Clean term caches added by WooCommerce. * * @since 3.3.4 * @param array|int $ids Array of ids or single ID to clear cache for. * @param string $taxonomy Taxonomy name. */ public static function clean_term_cache( $ids, $taxonomy ) { if ( 'product_cat' === $taxonomy ) { $ids = is_array( $ids ) ? $ids : array( $ids ); $clear_ids = array( 0 ); foreach ( $ids as $id ) { $clear_ids[] = $id; $clear_ids = array_merge( $clear_ids, get_ancestors( $id, 'product_cat', 'taxonomy' ) ); } $clear_ids = array_unique( $clear_ids ); foreach ( $clear_ids as $id ) { wp_cache_delete( 'product-category-hierarchy-' . $id, 'product_cat' ); } } } /** * When the transient version increases, this is used to remove all past transients to avoid filling the DB. * * Note; this only works on transients appended with the transient version, and when object caching is not being used. * * @deprecated 3.6.0 Adjusted transient usage to include versions within the transient values, making this cleanup obsolete. * @since 2.3.10 * @param string $version Version of the transient to remove. */ public static function delete_version_transients( $version = '' ) { if ( ! wp_using_ext_object_cache() && ! empty( $version ) ) { global $wpdb; $limit = apply_filters( 'woocommerce_delete_version_transients_limit', 1000 ); if ( ! $limit ) { return; } $affected = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d;", '\_transient\_%' . $version, $limit ) ); // WPCS: cache ok, db call ok. // If affected rows is equal to limit, there are more rows to delete. Delete in 30 secs. if ( $affected === $limit ) { wp_schedule_single_event( time() + 30, 'delete_version_transients', array( $version ) ); } } } } WC_Cache_Helper::init(); includes/class-wc-logger.php 0000644 00000021417 15132754524 0012071 0 ustar 00 <?php /** * Provides logging capabilities for debugging purposes. * * @class WC_Logger * @version 2.0.0 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Logger class. */ class WC_Logger implements WC_Logger_Interface { /** * Stores registered log handlers. * * @var array */ protected $handlers; /** * Minimum log level this handler will process. * * @var int Integer representation of minimum log level to handle. */ protected $threshold; /** * Constructor for the logger. * * @param array $handlers Optional. Array of log handlers. If $handlers is not provided, the filter 'woocommerce_register_log_handlers' will be used to define the handlers. If $handlers is provided, the filter will not be applied and the handlers will be used directly. * @param string $threshold Optional. Define an explicit threshold. May be configured via WC_LOG_THRESHOLD. By default, all logs will be processed. */ public function __construct( $handlers = null, $threshold = null ) { if ( null === $handlers ) { $handlers = apply_filters( 'woocommerce_register_log_handlers', array() ); } $register_handlers = array(); if ( ! empty( $handlers ) && is_array( $handlers ) ) { foreach ( $handlers as $handler ) { $implements = class_implements( $handler ); if ( is_object( $handler ) && is_array( $implements ) && in_array( 'WC_Log_Handler_Interface', $implements, true ) ) { $register_handlers[] = $handler; } else { wc_doing_it_wrong( __METHOD__, sprintf( /* translators: 1: class name 2: WC_Log_Handler_Interface */ __( 'The provided handler %1$s does not implement %2$s.', 'woocommerce' ), '<code>' . esc_html( is_object( $handler ) ? get_class( $handler ) : $handler ) . '</code>', '<code>WC_Log_Handler_Interface</code>' ), '3.0' ); } } } // Support the constant as long as a valid log level has been set for it. if ( null === $threshold ) { $threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' ); if ( null !== $threshold && ! WC_Log_Levels::is_valid_level( $threshold ) ) { $threshold = null; } } if ( null !== $threshold ) { $threshold = WC_Log_Levels::get_level_severity( $threshold ); } $this->handlers = $register_handlers; $this->threshold = $threshold; } /** * Determine whether to handle or ignore log. * * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @return bool True if the log should be handled. */ protected function should_handle( $level ) { if ( null === $this->threshold ) { return true; } return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); } /** * Add a log entry. * * This is not the preferred method for adding log messages. Please use log() or any one of * the level methods (debug(), info(), etc.). This method may be deprecated in the future. * * @param string $handle File handle. * @param string $message Message to log. * @param string $level Logging level. * @return bool */ public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ) { $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); $this->log( $level, $message, array( 'source' => $handle, '_legacy' => true, ) ); wc_do_deprecated_action( 'woocommerce_log_add', array( $handle, $message ), '3.0', 'This action has been deprecated with no alternative.' ); return true; } /** * Add a log entry. * * @param string $level One of the following: * 'emergency': System is unusable. * 'alert': Action must be taken immediately. * 'critical': Critical conditions. * 'error': Error conditions. * 'warning': Warning conditions. * 'notice': Normal but significant condition. * 'info': Informational messages. * 'debug': Debug-level messages. * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function log( $level, $message, $context = array() ) { if ( ! WC_Log_Levels::is_valid_level( $level ) ) { /* translators: 1: WC_Logger::log 2: level */ wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid level "%2$s".', 'woocommerce' ), '<code>WC_Logger::log</code>', $level ), '3.0' ); } if ( $this->should_handle( $level ) ) { $timestamp = time(); foreach ( $this->handlers as $handler ) { /** * Filter the logging message. Returning null will prevent logging from occuring since 5.3. * * @since 3.1 * @param string $message Log message. * @param string $level One of: emergency, alert, critical, error, warning, notice, info, or debug. * @param array $context Additional information for log handlers. * @param object $handler The handler object, such as WC_Log_Handler_File. Available since 5.3. */ $message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context, $handler ); if ( null !== $message ) { $handler->handle( $timestamp, $level, $message, $context ); } } } } /** * Adds an emergency level message. * * System is unusable. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function emergency( $message, $context = array() ) { $this->log( WC_Log_Levels::EMERGENCY, $message, $context ); } /** * Adds an alert level message. * * Action must be taken immediately. * Example: Entire website down, database unavailable, etc. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function alert( $message, $context = array() ) { $this->log( WC_Log_Levels::ALERT, $message, $context ); } /** * Adds a critical level message. * * Critical conditions. * Example: Application component unavailable, unexpected exception. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function critical( $message, $context = array() ) { $this->log( WC_Log_Levels::CRITICAL, $message, $context ); } /** * Adds an error level message. * * Runtime errors that do not require immediate action but should typically be logged * and monitored. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function error( $message, $context = array() ) { $this->log( WC_Log_Levels::ERROR, $message, $context ); } /** * Adds a warning level message. * * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not * necessarily wrong. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function warning( $message, $context = array() ) { $this->log( WC_Log_Levels::WARNING, $message, $context ); } /** * Adds a notice level message. * * Normal but significant events. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function notice( $message, $context = array() ) { $this->log( WC_Log_Levels::NOTICE, $message, $context ); } /** * Adds a info level message. * * Interesting events. * Example: User logs in, SQL logs. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function info( $message, $context = array() ) { $this->log( WC_Log_Levels::INFO, $message, $context ); } /** * Adds a debug level message. * * Detailed debug information. * * @see WC_Logger::log * * @param string $message Message to log. * @param array $context Log context. */ public function debug( $message, $context = array() ) { $this->log( WC_Log_Levels::DEBUG, $message, $context ); } /** * Clear entries for a chosen file/source. * * @param string $source Source/handle to clear. * @return bool */ public function clear( $source = '' ) { if ( ! $source ) { return false; } foreach ( $this->handlers as $handler ) { if ( is_callable( array( $handler, 'clear' ) ) ) { $handler->clear( $source ); } } return true; } /** * Clear all logs older than a defined number of days. Defaults to 30 days. * * @since 3.4.0 */ public function clear_expired_logs() { $days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) ); $timestamp = strtotime( "-{$days} days" ); foreach ( $this->handlers as $handler ) { if ( is_callable( array( $handler, 'delete_logs_before_timestamp' ) ) ) { $handler->delete_logs_before_timestamp( $timestamp ); } } } } includes/wc-core-functions.php 0000644 00000234177 15132754524 0012456 0 ustar 00 <?php /** * WooCommerce Core Functions * * General core functions available on both the front-end and admin. * * @package WooCommerce\Functions * @version 3.3.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } // Include core functions (available in both admin and frontend). require WC_ABSPATH . 'includes/wc-conditional-functions.php'; require WC_ABSPATH . 'includes/wc-coupon-functions.php'; require WC_ABSPATH . 'includes/wc-user-functions.php'; require WC_ABSPATH . 'includes/wc-deprecated-functions.php'; require WC_ABSPATH . 'includes/wc-formatting-functions.php'; require WC_ABSPATH . 'includes/wc-order-functions.php'; require WC_ABSPATH . 'includes/wc-order-item-functions.php'; require WC_ABSPATH . 'includes/wc-page-functions.php'; require WC_ABSPATH . 'includes/wc-product-functions.php'; require WC_ABSPATH . 'includes/wc-stock-functions.php'; require WC_ABSPATH . 'includes/wc-account-functions.php'; require WC_ABSPATH . 'includes/wc-term-functions.php'; require WC_ABSPATH . 'includes/wc-attribute-functions.php'; require WC_ABSPATH . 'includes/wc-rest-functions.php'; require WC_ABSPATH . 'includes/wc-widget-functions.php'; require WC_ABSPATH . 'includes/wc-webhook-functions.php'; /** * Filters on data used in admin and frontend. */ add_filter( 'woocommerce_coupon_code', 'html_entity_decode' ); add_filter( 'woocommerce_coupon_code', 'wc_sanitize_coupon_code' ); add_filter( 'woocommerce_coupon_code', 'wc_strtolower' ); add_filter( 'woocommerce_stock_amount', 'intval' ); // Stock amounts are integers by default. add_filter( 'woocommerce_shipping_rate_label', 'sanitize_text_field' ); // Shipping rate label. add_filter( 'woocommerce_attribute_label', 'wp_kses_post', 100 ); /** * Short Description (excerpt). */ if ( function_exists( 'do_blocks' ) ) { add_filter( 'woocommerce_short_description', 'do_blocks', 9 ); } add_filter( 'woocommerce_short_description', 'wptexturize' ); add_filter( 'woocommerce_short_description', 'convert_smilies' ); add_filter( 'woocommerce_short_description', 'convert_chars' ); add_filter( 'woocommerce_short_description', 'wpautop' ); add_filter( 'woocommerce_short_description', 'shortcode_unautop' ); add_filter( 'woocommerce_short_description', 'prepend_attachment' ); add_filter( 'woocommerce_short_description', 'do_shortcode', 11 ); // After wpautop(). add_filter( 'woocommerce_short_description', 'wc_format_product_short_description', 9999999 ); add_filter( 'woocommerce_short_description', 'wc_do_oembeds' ); add_filter( 'woocommerce_short_description', array( $GLOBALS['wp_embed'], 'run_shortcode' ), 8 ); // Before wpautop(). /** * Define a constant if it is not already defined. * * @since 3.0.0 * @param string $name Constant name. * @param mixed $value Value. */ function wc_maybe_define_constant( $name, $value ) { if ( ! defined( $name ) ) { define( $name, $value ); } } /** * Create a new order programmatically. * * Returns a new order object on success which can then be used to add additional data. * * @param array $args Order arguments. * @return WC_Order|WP_Error */ function wc_create_order( $args = array() ) { $default_args = array( 'status' => null, 'customer_id' => null, 'customer_note' => null, 'parent' => null, 'created_via' => null, 'cart_hash' => null, 'order_id' => 0, ); try { $args = wp_parse_args( $args, $default_args ); $order = new WC_Order( $args['order_id'] ); // Update props that were set (not null). if ( ! is_null( $args['parent'] ) ) { $order->set_parent_id( absint( $args['parent'] ) ); } if ( ! is_null( $args['status'] ) ) { $order->set_status( $args['status'] ); } if ( ! is_null( $args['customer_note'] ) ) { $order->set_customer_note( $args['customer_note'] ); } if ( ! is_null( $args['customer_id'] ) ) { $order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 ); } if ( ! is_null( $args['created_via'] ) ) { $order->set_created_via( sanitize_text_field( $args['created_via'] ) ); } if ( ! is_null( $args['cart_hash'] ) ) { $order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) ); } // Set these fields when creating a new order but not when updating an existing order. if ( ! $args['order_id'] ) { $order->set_currency( get_woocommerce_currency() ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); $order->set_customer_user_agent( wc_get_user_agent() ); } // Update other order props set automatically. $order->save(); } catch ( Exception $e ) { return new WP_Error( 'error', $e->getMessage() ); } return $order; } /** * Update an order. Uses wc_create_order. * * @param array $args Order arguments. * @return WC_Order|WP_Error */ function wc_update_order( $args ) { if ( empty( $args['order_id'] ) ) { return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) ); } return wc_create_order( $args ); } /** * Given a path, this will convert any of the subpaths into their corresponding tokens. * * @since 4.3.0 * @param string $path The absolute path to tokenize. * @param array $path_tokens An array keyed with the token, containing paths that should be replaced. * @return string The tokenized path. */ function wc_tokenize_path( $path, $path_tokens ) { // Order most to least specific so that the token can encompass as much of the path as possible. uasort( $path_tokens, function ( $a, $b ) { $a = strlen( $a ); $b = strlen( $b ); if ( $a > $b ) { return -1; } if ( $b > $a ) { return 1; } return 0; } ); foreach ( $path_tokens as $token => $token_path ) { if ( 0 !== strpos( $path, $token_path ) ) { continue; } $path = str_replace( $token_path, '{{' . $token . '}}', $path ); } return $path; } /** * Given a tokenized path, this will expand the tokens to their full path. * * @since 4.3.0 * @param string $path The absolute path to expand. * @param array $path_tokens An array keyed with the token, containing paths that should be expanded. * @return string The absolute path. */ function wc_untokenize_path( $path, $path_tokens ) { foreach ( $path_tokens as $token => $token_path ) { $path = str_replace( '{{' . $token . '}}', $token_path, $path ); } return $path; } /** * Fetches an array containing all of the configurable path constants to be used in tokenization. * * @return array The key is the define and the path is the constant. */ function wc_get_path_define_tokens() { $defines = array( 'ABSPATH', 'WP_CONTENT_DIR', 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'PLUGINDIR', 'WP_THEME_DIR', ); $path_tokens = array(); foreach ( $defines as $define ) { if ( defined( $define ) ) { $path_tokens[ $define ] = constant( $define ); } } return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens ); } /** * Get template part (for templates like the shop-loop). * * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority. * * @param mixed $slug Template slug. * @param string $name Template name (default: ''). */ function wc_get_template_part( $slug, $name = '' ) { $cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) ); $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); if ( ! $template ) { if ( $name ) { $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( array( "{$slug}-{$name}.php", WC()->template_path() . "{$slug}-{$name}.php", ) ); if ( ! $template ) { $fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php"; $template = file_exists( $fallback ) ? $fallback : ''; } } if ( ! $template ) { // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php. $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( array( "{$slug}.php", WC()->template_path() . "{$slug}.php", ) ); } // Don't cache the absolute path so that it can be shared between web servers with different paths. $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); wc_set_template_cache( $cache_key, $cache_path ); } else { // Make sure that the absolute path to the template is resolved. $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); } // Allow 3rd party plugins to filter template file from their plugin. $template = apply_filters( 'wc_get_template_part', $template, $slug, $name ); if ( $template ) { load_template( $template, false ); } } /** * Get other templates (e.g. product attributes) passing attributes and including the file. * * @param string $template_name Template name. * @param array $args Arguments. (default: array). * @param string $template_path Template path. (default: ''). * @param string $default_path Default path. (default: ''). */ function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { $cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) ); $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); if ( ! $template ) { $template = wc_locate_template( $template_name, $template_path, $default_path ); // Don't cache the absolute path so that it can be shared between web servers with different paths. $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); wc_set_template_cache( $cache_key, $cache_path ); } else { // Make sure that the absolute path to the template is resolved. $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); } // Allow 3rd party plugin filter template file from their plugin. $filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path ); if ( $filter_template !== $template ) { if ( ! file_exists( $filter_template ) ) { /* translators: %s template */ wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '<code>' . $filter_template . '</code>' ), '2.1' ); return; } $template = $filter_template; } $action_args = array( 'template_name' => $template_name, 'template_path' => $template_path, 'located' => $template, 'args' => $args, ); if ( ! empty( $args ) && is_array( $args ) ) { if ( isset( $args['action_args'] ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ), '3.6.0' ); unset( $args['action_args'] ); } extract( $args ); // @codingStandardsIgnoreLine } do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); include $action_args['located']; do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); } /** * Like wc_get_template, but returns the HTML instead of outputting. * * @see wc_get_template * @since 2.5.0 * @param string $template_name Template name. * @param array $args Arguments. (default: array). * @param string $template_path Template path. (default: ''). * @param string $default_path Default path. (default: ''). * * @return string */ function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) { ob_start(); wc_get_template( $template_name, $args, $template_path, $default_path ); return ob_get_clean(); } /** * Locate a template and return the path for inclusion. * * This is the load order: * * yourtheme/$template_path/$template_name * yourtheme/$template_name * $default_path/$template_name * * @param string $template_name Template name. * @param string $template_path Template path. (default: ''). * @param string $default_path Default path. (default: ''). * @return string */ function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) { if ( ! $template_path ) { $template_path = WC()->template_path(); } if ( ! $default_path ) { $default_path = WC()->plugin_path() . '/templates/'; } // Look within passed path within the theme - this is priority. if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) { $cs_template = str_replace( '_', '-', $template_name ); $template = locate_template( array( trailingslashit( $template_path ) . $cs_template, $cs_template, ) ); } if ( empty( $template ) ) { $template = locate_template( array( trailingslashit( $template_path ) . $template_name, $template_name, ) ); } // Get default template/. if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { if ( empty( $cs_template ) ) { $template = $default_path . $template_name; } else { $template = $default_path . $cs_template; } } // Return what we found. return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path ); } /** * Add a template to the template cache. * * @since 4.3.0 * @param string $cache_key Object cache key. * @param string $template Located template. */ function wc_set_template_cache( $cache_key, $template ) { wp_cache_set( $cache_key, $template, 'woocommerce' ); $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); if ( is_array( $cached_templates ) ) { $cached_templates[] = $cache_key; } else { $cached_templates = array( $cache_key ); } wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' ); } /** * Clear the template cache. * * @since 4.3.0 */ function wc_clear_template_cache() { $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); if ( is_array( $cached_templates ) ) { foreach ( $cached_templates as $cache_key ) { wp_cache_delete( $cache_key, 'woocommerce' ); } wp_cache_delete( 'cached_templates', 'woocommerce' ); } } /** * Get Base Currency Code. * * @return string */ function get_woocommerce_currency() { return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) ); } /** * Get full list of currency codes. * * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) * * @return array */ function get_woocommerce_currencies() { static $currencies; if ( ! isset( $currencies ) ) { $currencies = array_unique( apply_filters( 'woocommerce_currencies', array( 'AED' => __( 'United Arab Emirates dirham', 'woocommerce' ), 'AFN' => __( 'Afghan afghani', 'woocommerce' ), 'ALL' => __( 'Albanian lek', 'woocommerce' ), 'AMD' => __( 'Armenian dram', 'woocommerce' ), 'ANG' => __( 'Netherlands Antillean guilder', 'woocommerce' ), 'AOA' => __( 'Angolan kwanza', 'woocommerce' ), 'ARS' => __( 'Argentine peso', 'woocommerce' ), 'AUD' => __( 'Australian dollar', 'woocommerce' ), 'AWG' => __( 'Aruban florin', 'woocommerce' ), 'AZN' => __( 'Azerbaijani manat', 'woocommerce' ), 'BAM' => __( 'Bosnia and Herzegovina convertible mark', 'woocommerce' ), 'BBD' => __( 'Barbadian dollar', 'woocommerce' ), 'BDT' => __( 'Bangladeshi taka', 'woocommerce' ), 'BGN' => __( 'Bulgarian lev', 'woocommerce' ), 'BHD' => __( 'Bahraini dinar', 'woocommerce' ), 'BIF' => __( 'Burundian franc', 'woocommerce' ), 'BMD' => __( 'Bermudian dollar', 'woocommerce' ), 'BND' => __( 'Brunei dollar', 'woocommerce' ), 'BOB' => __( 'Bolivian boliviano', 'woocommerce' ), 'BRL' => __( 'Brazilian real', 'woocommerce' ), 'BSD' => __( 'Bahamian dollar', 'woocommerce' ), 'BTC' => __( 'Bitcoin', 'woocommerce' ), 'BTN' => __( 'Bhutanese ngultrum', 'woocommerce' ), 'BWP' => __( 'Botswana pula', 'woocommerce' ), 'BYR' => __( 'Belarusian ruble (old)', 'woocommerce' ), 'BYN' => __( 'Belarusian ruble', 'woocommerce' ), 'BZD' => __( 'Belize dollar', 'woocommerce' ), 'CAD' => __( 'Canadian dollar', 'woocommerce' ), 'CDF' => __( 'Congolese franc', 'woocommerce' ), 'CHF' => __( 'Swiss franc', 'woocommerce' ), 'CLP' => __( 'Chilean peso', 'woocommerce' ), 'CNY' => __( 'Chinese yuan', 'woocommerce' ), 'COP' => __( 'Colombian peso', 'woocommerce' ), 'CRC' => __( 'Costa Rican colón', 'woocommerce' ), 'CUC' => __( 'Cuban convertible peso', 'woocommerce' ), 'CUP' => __( 'Cuban peso', 'woocommerce' ), 'CVE' => __( 'Cape Verdean escudo', 'woocommerce' ), 'CZK' => __( 'Czech koruna', 'woocommerce' ), 'DJF' => __( 'Djiboutian franc', 'woocommerce' ), 'DKK' => __( 'Danish krone', 'woocommerce' ), 'DOP' => __( 'Dominican peso', 'woocommerce' ), 'DZD' => __( 'Algerian dinar', 'woocommerce' ), 'EGP' => __( 'Egyptian pound', 'woocommerce' ), 'ERN' => __( 'Eritrean nakfa', 'woocommerce' ), 'ETB' => __( 'Ethiopian birr', 'woocommerce' ), 'EUR' => __( 'Euro', 'woocommerce' ), 'FJD' => __( 'Fijian dollar', 'woocommerce' ), 'FKP' => __( 'Falkland Islands pound', 'woocommerce' ), 'GBP' => __( 'Pound sterling', 'woocommerce' ), 'GEL' => __( 'Georgian lari', 'woocommerce' ), 'GGP' => __( 'Guernsey pound', 'woocommerce' ), 'GHS' => __( 'Ghana cedi', 'woocommerce' ), 'GIP' => __( 'Gibraltar pound', 'woocommerce' ), 'GMD' => __( 'Gambian dalasi', 'woocommerce' ), 'GNF' => __( 'Guinean franc', 'woocommerce' ), 'GTQ' => __( 'Guatemalan quetzal', 'woocommerce' ), 'GYD' => __( 'Guyanese dollar', 'woocommerce' ), 'HKD' => __( 'Hong Kong dollar', 'woocommerce' ), 'HNL' => __( 'Honduran lempira', 'woocommerce' ), 'HRK' => __( 'Croatian kuna', 'woocommerce' ), 'HTG' => __( 'Haitian gourde', 'woocommerce' ), 'HUF' => __( 'Hungarian forint', 'woocommerce' ), 'IDR' => __( 'Indonesian rupiah', 'woocommerce' ), 'ILS' => __( 'Israeli new shekel', 'woocommerce' ), 'IMP' => __( 'Manx pound', 'woocommerce' ), 'INR' => __( 'Indian rupee', 'woocommerce' ), 'IQD' => __( 'Iraqi dinar', 'woocommerce' ), 'IRR' => __( 'Iranian rial', 'woocommerce' ), 'IRT' => __( 'Iranian toman', 'woocommerce' ), 'ISK' => __( 'Icelandic króna', 'woocommerce' ), 'JEP' => __( 'Jersey pound', 'woocommerce' ), 'JMD' => __( 'Jamaican dollar', 'woocommerce' ), 'JOD' => __( 'Jordanian dinar', 'woocommerce' ), 'JPY' => __( 'Japanese yen', 'woocommerce' ), 'KES' => __( 'Kenyan shilling', 'woocommerce' ), 'KGS' => __( 'Kyrgyzstani som', 'woocommerce' ), 'KHR' => __( 'Cambodian riel', 'woocommerce' ), 'KMF' => __( 'Comorian franc', 'woocommerce' ), 'KPW' => __( 'North Korean won', 'woocommerce' ), 'KRW' => __( 'South Korean won', 'woocommerce' ), 'KWD' => __( 'Kuwaiti dinar', 'woocommerce' ), 'KYD' => __( 'Cayman Islands dollar', 'woocommerce' ), 'KZT' => __( 'Kazakhstani tenge', 'woocommerce' ), 'LAK' => __( 'Lao kip', 'woocommerce' ), 'LBP' => __( 'Lebanese pound', 'woocommerce' ), 'LKR' => __( 'Sri Lankan rupee', 'woocommerce' ), 'LRD' => __( 'Liberian dollar', 'woocommerce' ), 'LSL' => __( 'Lesotho loti', 'woocommerce' ), 'LYD' => __( 'Libyan dinar', 'woocommerce' ), 'MAD' => __( 'Moroccan dirham', 'woocommerce' ), 'MDL' => __( 'Moldovan leu', 'woocommerce' ), 'MGA' => __( 'Malagasy ariary', 'woocommerce' ), 'MKD' => __( 'Macedonian denar', 'woocommerce' ), 'MMK' => __( 'Burmese kyat', 'woocommerce' ), 'MNT' => __( 'Mongolian tögrög', 'woocommerce' ), 'MOP' => __( 'Macanese pataca', 'woocommerce' ), 'MRU' => __( 'Mauritanian ouguiya', 'woocommerce' ), 'MUR' => __( 'Mauritian rupee', 'woocommerce' ), 'MVR' => __( 'Maldivian rufiyaa', 'woocommerce' ), 'MWK' => __( 'Malawian kwacha', 'woocommerce' ), 'MXN' => __( 'Mexican peso', 'woocommerce' ), 'MYR' => __( 'Malaysian ringgit', 'woocommerce' ), 'MZN' => __( 'Mozambican metical', 'woocommerce' ), 'NAD' => __( 'Namibian dollar', 'woocommerce' ), 'NGN' => __( 'Nigerian naira', 'woocommerce' ), 'NIO' => __( 'Nicaraguan córdoba', 'woocommerce' ), 'NOK' => __( 'Norwegian krone', 'woocommerce' ), 'NPR' => __( 'Nepalese rupee', 'woocommerce' ), 'NZD' => __( 'New Zealand dollar', 'woocommerce' ), 'OMR' => __( 'Omani rial', 'woocommerce' ), 'PAB' => __( 'Panamanian balboa', 'woocommerce' ), 'PEN' => __( 'Sol', 'woocommerce' ), 'PGK' => __( 'Papua New Guinean kina', 'woocommerce' ), 'PHP' => __( 'Philippine peso', 'woocommerce' ), 'PKR' => __( 'Pakistani rupee', 'woocommerce' ), 'PLN' => __( 'Polish złoty', 'woocommerce' ), 'PRB' => __( 'Transnistrian ruble', 'woocommerce' ), 'PYG' => __( 'Paraguayan guaraní', 'woocommerce' ), 'QAR' => __( 'Qatari riyal', 'woocommerce' ), 'RON' => __( 'Romanian leu', 'woocommerce' ), 'RSD' => __( 'Serbian dinar', 'woocommerce' ), 'RUB' => __( 'Russian ruble', 'woocommerce' ), 'RWF' => __( 'Rwandan franc', 'woocommerce' ), 'SAR' => __( 'Saudi riyal', 'woocommerce' ), 'SBD' => __( 'Solomon Islands dollar', 'woocommerce' ), 'SCR' => __( 'Seychellois rupee', 'woocommerce' ), 'SDG' => __( 'Sudanese pound', 'woocommerce' ), 'SEK' => __( 'Swedish krona', 'woocommerce' ), 'SGD' => __( 'Singapore dollar', 'woocommerce' ), 'SHP' => __( 'Saint Helena pound', 'woocommerce' ), 'SLL' => __( 'Sierra Leonean leone', 'woocommerce' ), 'SOS' => __( 'Somali shilling', 'woocommerce' ), 'SRD' => __( 'Surinamese dollar', 'woocommerce' ), 'SSP' => __( 'South Sudanese pound', 'woocommerce' ), 'STN' => __( 'São Tomé and Príncipe dobra', 'woocommerce' ), 'SYP' => __( 'Syrian pound', 'woocommerce' ), 'SZL' => __( 'Swazi lilangeni', 'woocommerce' ), 'THB' => __( 'Thai baht', 'woocommerce' ), 'TJS' => __( 'Tajikistani somoni', 'woocommerce' ), 'TMT' => __( 'Turkmenistan manat', 'woocommerce' ), 'TND' => __( 'Tunisian dinar', 'woocommerce' ), 'TOP' => __( 'Tongan paʻanga', 'woocommerce' ), 'TRY' => __( 'Turkish lira', 'woocommerce' ), 'TTD' => __( 'Trinidad and Tobago dollar', 'woocommerce' ), 'TWD' => __( 'New Taiwan dollar', 'woocommerce' ), 'TZS' => __( 'Tanzanian shilling', 'woocommerce' ), 'UAH' => __( 'Ukrainian hryvnia', 'woocommerce' ), 'UGX' => __( 'Ugandan shilling', 'woocommerce' ), 'USD' => __( 'United States (US) dollar', 'woocommerce' ), 'UYU' => __( 'Uruguayan peso', 'woocommerce' ), 'UZS' => __( 'Uzbekistani som', 'woocommerce' ), 'VEF' => __( 'Venezuelan bolívar', 'woocommerce' ), 'VES' => __( 'Bolívar soberano', 'woocommerce' ), 'VND' => __( 'Vietnamese đồng', 'woocommerce' ), 'VUV' => __( 'Vanuatu vatu', 'woocommerce' ), 'WST' => __( 'Samoan tālā', 'woocommerce' ), 'XAF' => __( 'Central African CFA franc', 'woocommerce' ), 'XCD' => __( 'East Caribbean dollar', 'woocommerce' ), 'XOF' => __( 'West African CFA franc', 'woocommerce' ), 'XPF' => __( 'CFP franc', 'woocommerce' ), 'YER' => __( 'Yemeni rial', 'woocommerce' ), 'ZAR' => __( 'South African rand', 'woocommerce' ), 'ZMW' => __( 'Zambian kwacha', 'woocommerce' ), ) ) ); } return $currencies; } /** * Get all available Currency symbols. * * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) * * @since 4.1.0 * @return array */ function get_woocommerce_currency_symbols() { $symbols = apply_filters( 'woocommerce_currency_symbols', array( 'AED' => 'د.إ', 'AFN' => '؋', 'ALL' => 'L', 'AMD' => 'AMD', 'ANG' => 'ƒ', 'AOA' => 'Kz', 'ARS' => '$', 'AUD' => '$', 'AWG' => 'Afl.', 'AZN' => 'AZN', 'BAM' => 'KM', 'BBD' => '$', 'BDT' => '৳ ', 'BGN' => 'лв.', 'BHD' => '.د.ب', 'BIF' => 'Fr', 'BMD' => '$', 'BND' => '$', 'BOB' => 'Bs.', 'BRL' => 'R$', 'BSD' => '$', 'BTC' => '฿', 'BTN' => 'Nu.', 'BWP' => 'P', 'BYR' => 'Br', 'BYN' => 'Br', 'BZD' => '$', 'CAD' => '$', 'CDF' => 'Fr', 'CHF' => 'CHF', 'CLP' => '$', 'CNY' => '¥', 'COP' => '$', 'CRC' => '₡', 'CUC' => '$', 'CUP' => '$', 'CVE' => '$', 'CZK' => 'Kč', 'DJF' => 'Fr', 'DKK' => 'DKK', 'DOP' => 'RD$', 'DZD' => 'د.ج', 'EGP' => 'EGP', 'ERN' => 'Nfk', 'ETB' => 'Br', 'EUR' => '€', 'FJD' => '$', 'FKP' => '£', 'GBP' => '£', 'GEL' => '₾', 'GGP' => '£', 'GHS' => '₵', 'GIP' => '£', 'GMD' => 'D', 'GNF' => 'Fr', 'GTQ' => 'Q', 'GYD' => '$', 'HKD' => '$', 'HNL' => 'L', 'HRK' => 'kn', 'HTG' => 'G', 'HUF' => 'Ft', 'IDR' => 'Rp', 'ILS' => '₪', 'IMP' => '£', 'INR' => '₹', 'IQD' => 'ع.د', 'IRR' => '﷼', 'IRT' => 'تومان', 'ISK' => 'kr.', 'JEP' => '£', 'JMD' => '$', 'JOD' => 'د.ا', 'JPY' => '¥', 'KES' => 'KSh', 'KGS' => 'сом', 'KHR' => '៛', 'KMF' => 'Fr', 'KPW' => '₩', 'KRW' => '₩', 'KWD' => 'د.ك', 'KYD' => '$', 'KZT' => '₸', 'LAK' => '₭', 'LBP' => 'ل.ل', 'LKR' => 'රු', 'LRD' => '$', 'LSL' => 'L', 'LYD' => 'ل.د', 'MAD' => 'د.م.', 'MDL' => 'MDL', 'MGA' => 'Ar', 'MKD' => 'ден', 'MMK' => 'Ks', 'MNT' => '₮', 'MOP' => 'P', 'MRU' => 'UM', 'MUR' => '₨', 'MVR' => '.ރ', 'MWK' => 'MK', 'MXN' => '$', 'MYR' => 'RM', 'MZN' => 'MT', 'NAD' => 'N$', 'NGN' => '₦', 'NIO' => 'C$', 'NOK' => 'kr', 'NPR' => '₨', 'NZD' => '$', 'OMR' => 'ر.ع.', 'PAB' => 'B/.', 'PEN' => 'S/', 'PGK' => 'K', 'PHP' => '₱', 'PKR' => '₨', 'PLN' => 'zł', 'PRB' => 'р.', 'PYG' => '₲', 'QAR' => 'ر.ق', 'RMB' => '¥', 'RON' => 'lei', 'RSD' => 'рсд', 'RUB' => '₽', 'RWF' => 'Fr', 'SAR' => 'ر.س', 'SBD' => '$', 'SCR' => '₨', 'SDG' => 'ج.س.', 'SEK' => 'kr', 'SGD' => '$', 'SHP' => '£', 'SLL' => 'Le', 'SOS' => 'Sh', 'SRD' => '$', 'SSP' => '£', 'STN' => 'Db', 'SYP' => 'ل.س', 'SZL' => 'L', 'THB' => '฿', 'TJS' => 'ЅМ', 'TMT' => 'm', 'TND' => 'د.ت', 'TOP' => 'T$', 'TRY' => '₺', 'TTD' => '$', 'TWD' => 'NT$', 'TZS' => 'Sh', 'UAH' => '₴', 'UGX' => 'UGX', 'USD' => '$', 'UYU' => '$', 'UZS' => 'UZS', 'VEF' => 'Bs F', 'VES' => 'Bs.S', 'VND' => '₫', 'VUV' => 'Vt', 'WST' => 'T', 'XAF' => 'CFA', 'XCD' => '$', 'XOF' => 'CFA', 'XPF' => 'Fr', 'YER' => '﷼', 'ZAR' => 'R', 'ZMW' => 'ZK', ) ); return $symbols; } /** * Get Currency symbol. * * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) * * @param string $currency Currency. (default: ''). * @return string */ function get_woocommerce_currency_symbol( $currency = '' ) { if ( ! $currency ) { $currency = get_woocommerce_currency(); } $symbols = get_woocommerce_currency_symbols(); $currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : ''; return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency ); } /** * Send HTML emails from WooCommerce. * * @param mixed $to Receiver. * @param mixed $subject Subject. * @param mixed $message Message. * @param string $headers Headers. (default: "Content-Type: text/html\r\n"). * @param string $attachments Attachments. (default: ""). * @return bool */ function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { $mailer = WC()->mailer(); return $mailer->send( $to, $subject, $message, $headers, $attachments ); } /** * Return "theme support" values from the current theme, if set. * * @since 3.3.0 * @param string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array. * @param mixed $default Optional value to return if the theme does not declare support for a prop. * @return mixed Value of prop(s). */ function wc_get_theme_support( $prop = '', $default = null ) { $theme_support = get_theme_support( 'woocommerce' ); $theme_support = is_array( $theme_support ) ? $theme_support[0] : false; if ( ! $theme_support ) { return $default; } if ( $prop ) { $prop_stack = explode( '::', $prop ); $prop_key = array_shift( $prop_stack ); if ( isset( $theme_support[ $prop_key ] ) ) { $value = $theme_support[ $prop_key ]; if ( count( $prop_stack ) ) { foreach ( $prop_stack as $prop_key ) { if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) { $value = $value[ $prop_key ]; } else { $value = $default; break; } } } } else { $value = $default; } return $value; } return $theme_support; } /** * Get an image size by name or defined dimensions. * * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to * allow 3rd party customisation. * * Sizes defined by the theme take priority over settings. Settings are hidden when a theme * defines sizes. * * @param array|string $image_size Name of the image size to get, or an array of dimensions. * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop. */ function wc_get_image_size( $image_size ) { $cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size ); $size = wp_cache_get( $cache_key, 'woocommerce' ); if ( $size ) { return $size; } $size = array( 'width' => 600, 'height' => 600, 'crop' => 1, ); if ( is_array( $image_size ) ) { $size = array( 'width' => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600, 'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600, 'crop' => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1, ); $image_size = $size['width'] . '_' . $size['height']; } else { $image_size = str_replace( 'woocommerce_', '', $image_size ); // Legacy size mapping. if ( 'shop_single' === $image_size ) { $image_size = 'single'; } elseif ( 'shop_catalog' === $image_size ) { $image_size = 'thumbnail'; } elseif ( 'shop_thumbnail' === $image_size ) { $image_size = 'gallery_thumbnail'; } if ( 'single' === $image_size ) { $size['width'] = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) ); $size['height'] = ''; $size['crop'] = 0; } elseif ( 'gallery_thumbnail' === $image_size ) { $size['width'] = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) ); $size['height'] = $size['width']; $size['crop'] = 1; } elseif ( 'thumbnail' === $image_size ) { $size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) ); $cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' ); if ( 'uncropped' === $cropping ) { $size['height'] = ''; $size['crop'] = 0; } elseif ( 'custom' === $cropping ) { $width = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) ); $height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) ); $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); $size['crop'] = 1; } else { $cropping_split = explode( ':', $cropping ); $width = max( 1, (float) current( $cropping_split ) ); $height = max( 1, (float) end( $cropping_split ) ); $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); $size['crop'] = 1; } } } $size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size ); wp_cache_set( $cache_key, $size, 'woocommerce' ); return $size; } /** * Queue some JavaScript code to be output in the footer. * * @param string $code Code. */ function wc_enqueue_js( $code ) { global $wc_queued_js; if ( empty( $wc_queued_js ) ) { $wc_queued_js = ''; } $wc_queued_js .= "\n" . $code . "\n"; } /** * Output any queued javascript code in the footer. */ function wc_print_js() { global $wc_queued_js; if ( ! empty( $wc_queued_js ) ) { // Sanitize. $wc_queued_js = wp_check_invalid_utf8( $wc_queued_js ); $wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js ); $wc_queued_js = str_replace( "\r", '', $wc_queued_js ); $js = "<!-- WooCommerce JavaScript -->\n<script type=\"text/javascript\">\njQuery(function($) { $wc_queued_js });\n</script>\n"; /** * Queued jsfilter. * * @since 2.6.0 * @param string $js JavaScript code. */ echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped unset( $wc_queued_js ); } } /** * Set a cookie - wrapper for setcookie using WP constants. * * @param string $name Name of the cookie being set. * @param string $value Value of the cookie. * @param integer $expire Expiry of the cookie. * @param bool $secure Whether the cookie should be served only over https. * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0. */ function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) { if ( ! headers_sent() ) { setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ) ); } elseif ( Constants::is_true( 'WP_DEBUG' ) ) { headers_sent( $file, $line ); trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine } } /** * Get the URL to the WooCommerce REST API. * * @since 2.1 * @param string $path an endpoint to include in the URL. * @return string the URL. */ function get_woocommerce_api_url( $path ) { if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) { $version = Constants::get_constant( 'WC_API_REQUEST_VERSION' ); } else { $version = substr( WC_API::VERSION, 0, 1 ); } $url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' ); if ( ! empty( $path ) && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } return $url; } /** * Get a log file path. * * @since 2.2 * * @param string $handle name. * @return string the log file path. */ function wc_get_log_file_path( $handle ) { return WC_Log_Handler_File::get_log_file_path( $handle ); } /** * Get a log file name. * * @since 3.3 * * @param string $handle Name. * @return string The log file name. */ function wc_get_log_file_name( $handle ) { return WC_Log_Handler_File::get_log_file_name( $handle ); } /** * Recursively get page children. * * @param int $page_id Page ID. * @return int[] */ function wc_get_page_children( $page_id ) { $page_ids = get_posts( array( 'post_parent' => $page_id, 'post_type' => 'page', 'numberposts' => -1, // @codingStandardsIgnoreLine 'post_status' => 'any', 'fields' => 'ids', ) ); if ( ! empty( $page_ids ) ) { foreach ( $page_ids as $page_id ) { $page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) ); } } return $page_ids; } /** * Flushes rewrite rules when the shop page (or it's children) gets saved. */ function flush_rewrite_rules_on_shop_page_save() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; // Check if this is the edit page. if ( 'page' !== $screen_id ) { return; } // Check if page is edited. if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $shop_page_id = wc_get_page_id( 'shop' ); if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) { do_action( 'woocommerce_flush_rewrite_rules' ); } } add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' ); /** * Various rewrite rule fixes. * * @since 2.2 * @param array $rules Rules. * @return array */ function wc_fix_rewrite_rules( $rules ) { global $wp_rewrite; $permalinks = wc_get_permalink_structure(); // Fix the rewrite rules when the product permalink have %product_cat% flag. if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) { foreach ( $rules as $rule => $rewrite ) { if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) { unset( $rules[ $rule ] ); } } } // If the shop page is used as the base, we need to handle shop page subpages to avoid 404s. if ( ! $permalinks['use_verbose_page_rules'] ) { return $rules; } $shop_page_id = wc_get_page_id( 'shop' ); if ( $shop_page_id ) { $page_rewrite_rules = array(); $subpages = wc_get_page_children( $shop_page_id ); // Subpage rules. foreach ( $subpages as $subpage ) { $uri = get_page_uri( $subpage ); $page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri; $wp_generated_rewrite_rules = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false ); foreach ( $wp_generated_rewrite_rules as $key => $value ) { $wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri; } $page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules ); } // Merge with rules. $rules = array_merge( $page_rewrite_rules, $rules ); } return $rules; } add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' ); /** * Prevent product attachment links from breaking when using complex rewrite structures. * * @param string $link Link. * @param int $post_id Post ID. * @return string */ function wc_fix_product_attachment_link( $link, $post_id ) { $parent_type = get_post_type( wp_get_post_parent_id( $post_id ) ); if ( 'product' === $parent_type || 'product_variation' === $parent_type ) { $link = home_url( '/?attachment_id=' . $post_id ); } return $link; } add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 ); /** * Protect downloads from ms-files.php in multisite. * * @param string $rewrite rewrite rules. * @return string */ function wc_ms_protect_download_rewite_rules( $rewrite ) { if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { return $rewrite; } $rule = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n"; $rule .= "<IfModule mod_rewrite.c>\n"; $rule .= "RewriteEngine On\n"; $rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n"; $rule .= "RewriteRule /ms-files.php$ - [F]\n"; $rule .= "</IfModule>\n\n"; return $rule . $rewrite; } add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' ); /** * Formats a string in the format COUNTRY:STATE into an array. * * @since 2.3.0 * @param string $country_string Country string. * @return array */ function wc_format_country_state_string( $country_string ) { if ( strstr( $country_string, ':' ) ) { list( $country, $state ) = explode( ':', $country_string ); } else { $country = $country_string; $state = ''; } return array( 'country' => $country, 'state' => $state, ); } /** * Get the store's base location. * * @since 2.3.0 * @return array */ function wc_get_base_location() { $default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country', 'US:CA' ) ); return wc_format_country_state_string( $default ); } /** * Get the customer's default location. * * Filtered, and set to base location or left blank. If cache-busting, * this should only be used when 'location' is set in the querystring. * * @since 2.3.0 * @return array */ function wc_get_customer_default_location() { $set_default_location_to = get_option( 'woocommerce_default_customer_address', 'base' ); $default_location = '' === $set_default_location_to ? '' : get_option( 'woocommerce_default_country', 'US:CA' ); $location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) ); // Geolocation takes priority if used and if geolocation is possible. if ( 'geolocation' === $set_default_location_to || 'geolocation_ajax' === $set_default_location_to ) { $ua = wc_get_user_agent(); // Exclude common bots from geolocation by user agent. if ( ! stristr( $ua, 'bot' ) && ! stristr( $ua, 'spider' ) && ! stristr( $ua, 'crawl' ) ) { $geolocation = WC_Geolocation::geolocate_ip( '', true, false ); if ( ! empty( $geolocation['country'] ) ) { $location = $geolocation; } } } // Once we have a location, ensure it's valid, otherwise fallback to a valid location. $allowed_country_codes = WC()->countries->get_allowed_countries(); if ( ! empty( $location['country'] ) && ! array_key_exists( $location['country'], $allowed_country_codes ) ) { $location['country'] = current( array_keys( $allowed_country_codes ) ); $location['state'] = ''; } return apply_filters( 'woocommerce_customer_default_location_array', $location ); } /** * Get user agent string. * * @since 3.0.0 * @return string */ function wc_get_user_agent() { return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine } /** * Generate a rand hash. * * @since 2.4.0 * @return string */ function wc_rand_hash() { if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) { return sha1( wp_rand() ); } return bin2hex( openssl_random_pseudo_bytes( 20 ) ); // @codingStandardsIgnoreLine } /** * WC API - Hash. * * @since 2.4.0 * @param string $data Message to be hashed. * @return string */ function wc_api_hash( $data ) { return hash_hmac( 'sha256', $data, 'wc-api' ); } /** * Find all possible combinations of values from the input array and return in a logical order. * * @since 2.5.0 * @param array $input Input. * @return array */ function wc_array_cartesian( $input ) { $input = array_filter( $input ); $results = array(); $indexes = array(); $index = 0; // Generate indexes from keys and values so we have a logical sort order. foreach ( $input as $key => $values ) { foreach ( $values as $value ) { $indexes[ $key ][ $value ] = $index++; } } // Loop over the 2D array of indexes and generate all combinations. foreach ( $indexes as $key => $values ) { // When result is empty, fill with the values of the first looped array. if ( empty( $results ) ) { foreach ( $values as $value ) { $results[] = array( $key => $value ); } } else { // Second and subsequent input sub-array merging. foreach ( $results as $result_key => $result ) { foreach ( $values as $value ) { // If the key is not set, we can set it. if ( ! isset( $results[ $result_key ][ $key ] ) ) { $results[ $result_key ][ $key ] = $value; } else { // If the key is set, we can add a new combination to the results array. $new_combination = $results[ $result_key ]; $new_combination[ $key ] = $value; $results[] = $new_combination; } } } } } // Sort the indexes. arsort( $results ); // Convert indexes back to values. foreach ( $results as $result_key => $result ) { $converted_values = array(); // Sort the values. arsort( $results[ $result_key ] ); // Convert the values. foreach ( $results[ $result_key ] as $key => $value ) { $converted_values[ $key ] = array_search( $value, $indexes[ $key ], true ); } $results[ $result_key ] = $converted_values; } return $results; } /** * Run a MySQL transaction query, if supported. * * @since 2.5.0 * @param string $type Types: start (default), commit, rollback. * @param bool $force use of transactions. */ function wc_transaction_query( $type = 'start', $force = false ) { global $wpdb; $wpdb->hide_errors(); wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true ); if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) { switch ( $type ) { case 'commit': $wpdb->query( 'COMMIT' ); break; case 'rollback': $wpdb->query( 'ROLLBACK' ); break; default: $wpdb->query( 'START TRANSACTION' ); break; } } } /** * Gets the url to the cart page. * * @since 2.5.0 * * @return string Url to cart page */ function wc_get_cart_url() { return apply_filters( 'woocommerce_get_cart_url', wc_get_page_permalink( 'cart' ) ); } /** * Gets the url to the checkout page. * * @since 2.5.0 * * @return string Url to checkout page */ function wc_get_checkout_url() { $checkout_url = wc_get_page_permalink( 'checkout' ); if ( $checkout_url ) { // Force SSL if needed. if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); } } return apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); } /** * Register a shipping method. * * @since 1.5.7 * @param string|object $shipping_method class name (string) or a class object. */ function woocommerce_register_shipping_method( $shipping_method ) { WC()->shipping()->register_shipping_method( $shipping_method ); } if ( ! function_exists( 'wc_get_shipping_zone' ) ) { /** * Get the shipping zone matching a given package from the cart. * * @since 2.6.0 * @uses WC_Shipping_Zones::get_zone_matching_package * @param array $package Shipping package. * @return WC_Shipping_Zone */ function wc_get_shipping_zone( $package ) { return WC_Shipping_Zones::get_zone_matching_package( $package ); } } /** * Get a nice name for credit card providers. * * @since 2.6.0 * @param string $type Provider Slug/Type. * @return string */ function wc_get_credit_card_type_label( $type ) { // Normalize. $type = strtolower( $type ); $type = str_replace( '-', ' ', $type ); $type = str_replace( '_', ' ', $type ); $labels = apply_filters( 'woocommerce_credit_card_type_labels', array( 'mastercard' => __( 'MasterCard', 'woocommerce' ), 'visa' => __( 'Visa', 'woocommerce' ), 'discover' => __( 'Discover', 'woocommerce' ), 'american express' => __( 'American Express', 'woocommerce' ), 'diners' => __( 'Diners', 'woocommerce' ), 'jcb' => __( 'JCB', 'woocommerce' ), ) ); return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) ); } /** * Outputs a "back" link so admin screens can easily jump back a page. * * @param string $label Title of the page to return to. * @param string $url URL of the page to return to. */ function wc_back_link( $label, $url ) { echo '<small class="wc-admin-breadcrumb"><a href="' . esc_url( $url ) . '" aria-label="' . esc_attr( $label ) . '">⤴</a></small>'; } /** * Display a WooCommerce help tip. * * @since 2.5.0 * * @param string $tip Help tip text. * @param bool $allow_html Allow sanitized HTML if true or escape. * @return string */ function wc_help_tip( $tip, $allow_html = false ) { if ( $allow_html ) { $tip = wc_sanitize_tooltip( $tip ); } else { $tip = esc_attr( $tip ); } return '<span class="woocommerce-help-tip" data-tip="' . $tip . '"></span>'; } /** * Return a list of potential postcodes for wildcard searching. * * @since 2.6.0 * @param string $postcode Postcode. * @param string $country Country to format postcode for matching. * @return string[] */ function wc_get_wildcard_postcodes( $postcode, $country = '' ) { $formatted_postcode = wc_format_postcode( $postcode, $country ); $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode ); $postcodes = array( $postcode, $formatted_postcode, $formatted_postcode . '*', ); for ( $i = 0; $i < $length; $i ++ ) { $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*'; } return $postcodes; } /** * Used by shipping zones and taxes to compare a given $postcode to stored * postcodes to find matches for numerical ranges, and wildcards. * * @since 2.6.0 * @param string $postcode Postcode you want to match against stored postcodes. * @param array $objects Array of postcode objects from Database. * @param string $object_id_key DB column name for the ID. * @param string $object_compare_key DB column name for the value. * @param string $country Country from which this postcode belongs. Allows for formatting. * @return array Array of matching object ID and matching values. */ function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) { $postcode = wc_normalize_postcode( $postcode ); $wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) ); $matches = array(); foreach ( $objects as $object ) { $object_id = $object->$object_id_key; $compare_against = $object->$object_compare_key; // Handle postcodes containing ranges. if ( strstr( $compare_against, '...' ) ) { $range = array_map( 'trim', explode( '...', $compare_against ) ); if ( 2 !== count( $range ) ) { continue; } list( $min, $max ) = $range; // If the postcode is non-numeric, make it numeric. if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) { $compare = wc_make_numeric_postcode( $postcode ); $min = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' ); $max = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' ); } else { $compare = $postcode; } if ( $compare >= $min && $compare <= $max ) { $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); $matches[ $object_id ][] = $compare_against; } } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) { // Wildcard and standard comparison. $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); $matches[ $object_id ][] = $compare_against; } } return $matches; } /** * Gets number of shipping methods currently enabled. Used to identify if * shipping is configured. * * @since 2.6.0 * @param bool $include_legacy Count legacy shipping methods too. * @param bool $enabled_only Whether non-legacy shipping methods should be * restricted to enabled ones. It doesn't affect * legacy shipping methods. @since 4.3.0. * @return int */ function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) { global $wpdb; $transient_name = $include_legacy ? 'wc_shipping_method_count_legacy' : 'wc_shipping_method_count'; $transient_version = WC_Cache_Helper::get_transient_version( 'shipping' ); $transient_value = get_transient( $transient_name ); if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { return absint( $transient_value['value'] ); } $where_clause = $enabled_only ? 'WHERE is_enabled=1' : ''; $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) ); if ( $include_legacy ) { // Count activated methods that don't support shipping zones. $methods = WC()->shipping()->get_shipping_methods(); foreach ( $methods as $method ) { if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) { $method_count++; } } } $transient_value = array( 'version' => $transient_version, 'value' => $method_count, ); set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); return $method_count; } /** * Wrapper for set_time_limit to see if it is enabled. * * @since 2.6.0 * @param int $limit Time limit. */ function wc_set_time_limit( $limit = 0 ) { if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved @set_time_limit( $limit ); // @codingStandardsIgnoreLine } } /** * Wrapper for nocache_headers which also disables page caching. * * @since 3.2.4 */ function wc_nocache_headers() { WC_Cache_Helper::set_nocache_constants(); nocache_headers(); } /** * Used to sort products attributes with uasort. * * @since 2.6.0 * @param array $a First attribute to compare. * @param array $b Second attribute to compare. * @return int */ function wc_product_attribute_uasort_comparison( $a, $b ) { $a_position = is_null( $a ) ? null : $a['position']; $b_position = is_null( $b ) ? null : $b['position']; return wc_uasort_comparison( $a_position, $b_position ); } /** * Used to sort shipping zone methods with uasort. * * @since 3.0.0 * @param array $a First shipping zone method to compare. * @param array $b Second shipping zone method to compare. * @return int */ function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) { return wc_uasort_comparison( $a->method_order, $b->method_order ); } /** * User to sort checkout fields based on priority with uasort. * * @since 3.5.1 * @param array $a First field to compare. * @param array $b Second field to compare. * @return int */ function wc_checkout_fields_uasort_comparison( $a, $b ) { /* * We are not guaranteed to get a priority * setting. So don't compare if they don't * exist. */ if ( ! isset( $a['priority'], $b['priority'] ) ) { return 0; } return wc_uasort_comparison( $a['priority'], $b['priority'] ); } /** * User to sort two values with ausort. * * @since 3.5.1 * @param int $a First value to compare. * @param int $b Second value to compare. * @return int */ function wc_uasort_comparison( $a, $b ) { if ( $a === $b ) { return 0; } return ( $a < $b ) ? -1 : 1; } /** * Sort values based on ascii, usefull for special chars in strings. * * @param string $a First value. * @param string $b Second value. * @return int */ function wc_ascii_uasort_comparison( $a, $b ) { // 'setlocale' is required for compatibility with PHP 8. // Without it, 'iconv' will return '?'s instead of transliterated characters. $prev_locale = setlocale( LC_CTYPE, 0 ); setlocale( LC_ALL, 'C.UTF-8' ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) { $a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a ); $b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b ); } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged setlocale( LC_ALL, $prev_locale ); return strcmp( $a, $b ); } /** * Sort array according to current locale rules and maintaining index association. * By default tries to use Collator from PHP Internationalization Functions if available. * If PHP Collator class doesn't exists it fallback to removing accepts from a array * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values. * * @since 4.6.0 * @param array $data List of values to sort. * @param string $locale Locale. * @return array */ function wc_asort_by_locale( &$data, $locale = '' ) { // Use Collator if PHP Internationalization Functions (php-intl) is available. if ( class_exists( 'Collator' ) ) { try { $locale = $locale ? $locale : get_locale(); $collator = new Collator( $locale ); $collator->asort( $data, Collator::SORT_STRING ); return $data; } catch ( IntlException $e ) { /* * Just skip if some error got caused. * It may be caused in installations that doesn't include ICU TZData. */ if ( Constants::is_true( 'WP_DEBUG' ) ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log sprintf( 'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstallaing PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s', 'https://www.php.net/manual/en/intl.installation.php', $e->getMessage() ) ); } } } $raw_data = $data; array_walk( $data, function ( &$value ) { $value = remove_accents( html_entity_decode( $value ) ); } ); uasort( $data, 'strcmp' ); foreach ( $data as $key => $val ) { $data[ $key ] = $raw_data[ $key ]; } return $data; } /** * Get rounding mode for internal tax calculations. * * @since 3.2.4 * @return int */ function wc_get_tax_rounding_mode() { $constant = WC_TAX_ROUNDING_MODE; if ( 'auto' === $constant ) { return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? PHP_ROUND_HALF_DOWN : PHP_ROUND_HALF_UP; } return intval( $constant ); } /** * Get rounding precision for internal WC calculations. * Will increase the precision of wc_get_price_decimals by 2 decimals, unless WC_ROUNDING_PRECISION is set to a higher number. * * @since 2.6.3 * @return int */ function wc_get_rounding_precision() { $precision = wc_get_price_decimals() + 2; if ( absint( WC_ROUNDING_PRECISION ) > $precision ) { $precision = absint( WC_ROUNDING_PRECISION ); } return $precision; } /** * Add precision to a number and return a number. * * @since 3.2.0 * @param float $value Number to add precision to. * @param bool $round If should round after adding precision. * @return int|float */ function wc_add_number_precision( $value, $round = true ) { $cent_precision = pow( 10, wc_get_price_decimals() ); $value = $value * $cent_precision; return $round ? NumberUtil::round( $value, wc_get_rounding_precision() - wc_get_price_decimals() ) : $value; } /** * Remove precision from a number and return a float. * * @since 3.2.0 * @param float $value Number to add precision to. * @return float */ function wc_remove_number_precision( $value ) { $cent_precision = pow( 10, wc_get_price_decimals() ); return $value / $cent_precision; } /** * Add precision to an array of number and return an array of int. * * @since 3.2.0 * @param array $value Number to add precision to. * @param bool $round Should we round after adding precision?. * @return int|array */ function wc_add_number_precision_deep( $value, $round = true ) { if ( ! is_array( $value ) ) { return wc_add_number_precision( $value, $round ); } foreach ( $value as $key => $sub_value ) { $value[ $key ] = wc_add_number_precision_deep( $sub_value, $round ); } return $value; } /** * Remove precision from an array of number and return an array of int. * * @since 3.2.0 * @param array $value Number to add precision to. * @return int|array */ function wc_remove_number_precision_deep( $value ) { if ( ! is_array( $value ) ) { return wc_remove_number_precision( $value ); } foreach ( $value as $key => $sub_value ) { $value[ $key ] = wc_remove_number_precision_deep( $sub_value ); } return $value; } /** * Get a shared logger instance. * * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following: * - a class name which will be instantiated as `new $class` with no arguments * - an instance which will be used directly as the logger * In either case, the class or instance *must* implement WC_Logger_Interface. * * @see WC_Logger_Interface * * @return WC_Logger */ function wc_get_logger() { static $logger = null; $class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' ); if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) { return $logger; } $implements = class_implements( $class ); if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) { $logger = is_object( $class ) ? $class : new $class(); } else { wc_doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */ __( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ), '<code>' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '</code>', '<code>woocommerce_logging_class</code>', '<code>WC_Logger_Interface</code>' ), '3.0' ); $logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger(); } return $logger; } /** * Trigger logging cleanup using the logging class. * * @since 3.4.0 */ function wc_cleanup_logs() { $logger = wc_get_logger(); if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) { $logger->clear_expired_logs(); } } add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' ); /** * Prints human-readable information about a variable. * * Some server environments block some debugging functions. This function provides a safe way to * turn an expression into a printable, readable form without calling blocked functions. * * @since 3.0 * * @param mixed $expression The expression to be printed. * @param bool $return Optional. Default false. Set to true to return the human-readable string. * @return string|bool False if expression could not be printed. True if the expression was printed. * If $return is true, a string representation will be returned. */ function wc_print_r( $expression, $return = false ) { $alternatives = array( array( 'func' => 'print_r', 'args' => array( $expression, true ), ), array( 'func' => 'var_export', 'args' => array( $expression, true ), ), array( 'func' => 'json_encode', 'args' => array( $expression ), ), array( 'func' => 'serialize', 'args' => array( $expression ), ), ); $alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression ); foreach ( $alternatives as $alternative ) { if ( function_exists( $alternative['func'] ) ) { $res = $alternative['func']( ...$alternative['args'] ); if ( $return ) { return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return true; } } return false; } /** * Registers the default log handler. * * @since 3.0 * @param array $handlers Handlers. * @return array */ function wc_register_default_log_handler( $handlers ) { $handler_class = Constants::get_constant( 'WC_LOG_HANDLER' ); if ( ! class_exists( $handler_class ) ) { $handler_class = WC_Log_Handler_File::class; } array_push( $handlers, new $handler_class() ); return $handlers; } add_filter( 'woocommerce_register_log_handlers', 'wc_register_default_log_handler' ); /** * Based on wp_list_pluck, this calls a method instead of returning a property. * * @since 3.0.0 * @param array $list List of objects or arrays. * @param int|string $callback_or_field Callback method from the object to place instead of the entire object. * @param int|string $index_key Optional. Field from the object to use as keys for the new array. * Default null. * @return array Array of values. */ function wc_list_pluck( $list, $callback_or_field, $index_key = null ) { // Use wp_list_pluck if this isn't a callback. $first_el = current( $list ); if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) { return wp_list_pluck( $list, $callback_or_field, $index_key ); } if ( ! $index_key ) { /* * This is simple. Could at some point wrap array_column() * if we knew we had an array of arrays. */ foreach ( $list as $key => $value ) { $list[ $key ] = $value->{$callback_or_field}(); } return $list; } /* * When index_key is not set for a particular item, push the value * to the end of the stack. This is how array_column() behaves. */ $newlist = array(); foreach ( $list as $value ) { // Get index. @since 3.2.0 this supports a callback. if ( is_callable( array( $value, $index_key ) ) ) { $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}(); } elseif ( isset( $value->$index_key ) ) { $newlist[ $value->$index_key ] = $value->{$callback_or_field}(); } else { $newlist[] = $value->{$callback_or_field}(); } } return $newlist; } /** * Get permalink settings for things like products and taxonomies. * * As of 3.3.0, the permalink settings are stored to the option instead of * being blank and inheritting from the locale. This speeds up page loading * times by negating the need to switch locales on each page load. * * This is more inline with WP core behavior which does not localize slugs. * * @since 3.0.0 * @return array */ function wc_get_permalink_structure() { $saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() ); $permalinks = wp_parse_args( array_filter( $saved_permalinks ), array( 'product_base' => _x( 'product', 'slug', 'woocommerce' ), 'category_base' => _x( 'product-category', 'slug', 'woocommerce' ), 'tag_base' => _x( 'product-tag', 'slug', 'woocommerce' ), 'attribute_base' => '', 'use_verbose_page_rules' => false, ) ); if ( $saved_permalinks !== $permalinks ) { update_option( 'woocommerce_permalinks', $permalinks ); } $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] ); $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] ); $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] ); $permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] ); return $permalinks; } /** * Switch WooCommerce to site language. * * @since 3.1.0 */ function wc_switch_to_site_locale() { if ( function_exists( 'switch_to_locale' ) ) { switch_to_locale( get_locale() ); // Filter on plugin_locale so load_plugin_textdomain loads the correct locale. add_filter( 'plugin_locale', 'get_locale' ); // Init WC locale. WC()->load_plugin_textdomain(); } } /** * Switch WooCommerce language to original. * * @since 3.1.0 */ function wc_restore_locale() { if ( function_exists( 'restore_previous_locale' ) ) { restore_previous_locale(); // Remove filter. remove_filter( 'plugin_locale', 'get_locale' ); // Init WC locale. WC()->load_plugin_textdomain(); } } /** * Convert plaintext phone number to clickable phone number. * * Remove formatting and allow "+". * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link * * @since 3.1.0 * * @param string $phone Content to convert phone number. * @return string Content with converted phone number. */ function wc_make_phone_clickable( $phone ) { $number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) ); return $number ? '<a href="tel:' . esc_attr( $number ) . '">' . esc_html( $phone ) . '</a>' : ''; } /** * Get an item of post data if set, otherwise return a default value. * * @since 3.0.9 * @param string $key Meta key. * @param string $default Default value. * @return mixed Value sanitized by wc_clean. */ function wc_get_post_data_by_key( $key, $default = '' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) ); } /** * Get data if set, otherwise return a default value or null. Prevents notices when data is not set. * * @since 3.2.0 * @param mixed $var Variable. * @param string $default Default value. * @return mixed */ function wc_get_var( &$var, $default = null ) { return isset( $var ) ? $var : $default; } /** * Read in WooCommerce headers when reading plugin headers. * * @since 3.2.0 * @param array $headers Headers. * @return array */ function wc_enable_wc_plugin_headers( $headers ) { if ( ! class_exists( 'WC_Plugin_Updates' ) ) { include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; } // WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run. $headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER; // WC tested up to - allows developers to define which version of WooCommerce they have tested up to. $headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER; // Woo - This is used in WooCommerce extensions and is picked up by the helper. $headers[] = 'Woo'; return $headers; } add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' ); add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' ); /** * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active. * * @since 3.2.0 * @param bool $should_update If should update. * @param object $plugin Plugin data. * @return bool */ function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) { if ( ! isset( $plugin->plugin, $plugin->new_version ) ) { return $should_update; } if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) { return $should_update; } if ( ! class_exists( 'WC_Plugin_Updates' ) ) { include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; } $new_version = wc_clean( $plugin->new_version ); $plugin_updates = new WC_Plugin_Updates(); $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ); if ( ! is_string( $version_type ) ) { $version_type = 'none'; } $untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type ); if ( ! empty( $untested_plugins ) ) { return false; } return $should_update; } add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 ); /** * Delete expired transients. * * Deletes all expired transients. The multi-table delete syntax is used. * to delete the transient record from table a, and the corresponding. * transient_timeout record from table b. * * Based on code inside core's upgrade_network() function. * * @since 3.2.0 * @return int Number of transients that were cleared. */ function wc_delete_expired_transients() { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) AND b.option_value < %d"; $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) AND b.option_value < %d"; $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared return absint( $rows + $rows2 ); } add_action( 'woocommerce_installed', 'wc_delete_expired_transients' ); /** * Make a URL relative, if possible. * * @since 3.2.0 * @param string $url URL to make relative. * @return string */ function wc_get_relative_url( $url ) { return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url ); } /** * See if a resource is remote. * * @since 3.2.0 * @param string $url URL to check. * @return bool */ function wc_is_external_resource( $url ) { $wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) ); return strstr( $url, '://' ) && ! strstr( $url, $wp_base ); } /** * See if theme/s is activate or not. * * @since 3.3.0 * @param string|array $theme Theme name or array of theme names to check. * @return boolean */ function wc_is_active_theme( $theme ) { return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme; } /** * Is the site using a default WP theme? * * @return boolean */ function wc_is_wp_default_theme_active() { return wc_is_active_theme( array( 'twentytwentyone', 'twentytwenty', 'twentynineteen', 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten', ) ); } /** * Cleans up session data - cron callback. * * @since 3.3.0 */ function wc_cleanup_session_data() { $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); $session = new $session_class(); if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) { $session->cleanup_sessions(); } } add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' ); /** * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2). * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/ * * @param float $decimal the decimal number. * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string). */ function wc_decimal_to_fraction( $decimal ) { if ( 0 > $decimal || ! is_numeric( $decimal ) ) { // Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded. return false; } if ( 0 === $decimal ) { return array( 0, 1 ); } $tolerance = 1.e-4; $numerator = 1; $h2 = 0; $denominator = 0; $k2 = 1; $b = 1 / $decimal; do { $b = 1 / $b; $a = floor( $b ); $aux = $numerator; $numerator = $a * $numerator + $h2; $h2 = $aux; $aux = $denominator; $denominator = $a * $denominator + $k2; $k2 = $aux; $b = $b - $a; } while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance ); return array( $numerator, $denominator ); } /** * Round discount. * * @param double $value Amount to round. * @param int $precision DP to round. * @return float */ function wc_round_discount( $value, $precision ) { if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound } if ( PHP_ROUND_HALF_DOWN === WC_DISCOUNT_ROUNDING_MODE ) { return wc_legacy_round_half_down( $value, $precision ); } return NumberUtil::round( $value, $precision ); } /** * Return the html selected attribute if stringified $value is found in array of stringified $options * or if stringified $value is the same as scalar stringified $options. * * @param string|int $value Value to find within options. * @param string|int|array $options Options to go through when looking for value. * @return string */ function wc_selected( $value, $options ) { if ( is_array( $options ) ) { $options = array_map( 'strval', $options ); return selected( in_array( (string) $value, $options, true ), true, false ); } return selected( $value, $options, false ); } /** * Retrieves the MySQL server version. Based on $wpdb. * * @since 3.4.1 * @return array Vesion information. */ function wc_get_server_database_version() { global $wpdb; if ( empty( $wpdb->is_mysql ) ) { return array( 'string' => '', 'number' => '', ); } // phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved if ( $wpdb->use_mysqli ) { $server_info = mysqli_get_server_info( $wpdb->dbh ); } else { $server_info = mysql_get_server_info( $wpdb->dbh ); } // phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved return array( 'string' => $server_info, 'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ), ); } /** * Initialize and load the cart functionality. * * @since 3.6.4 * @return void */ function wc_load_cart() { if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) { /* translators: 1: wc_load_cart 2: woocommerce_init */ wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' ); return; } // Ensure dependencies are loaded in all contexts. include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; WC()->initialize_session(); WC()->initialize_cart(); } /** * Test whether the context of execution comes from async action scheduler. * * @since 4.0.0 * @return bool */ function wc_is_running_from_async_action_scheduler() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action']; } /** * Polyfill for wp_cache_get_multiple for WP versions before 5.5. * * @param array $keys Array of keys to get from group. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param bool $force Optional. Whether to force an update of the local cache from the persistent * cache. Default false. * @return array|bool Array of values. */ function wc_cache_get_multiple( $keys, $group = '', $force = false ) { if ( function_exists( 'wp_cache_get_multiple' ) ) { return wp_cache_get_multiple( $keys, $group, $force ); } $values = array(); foreach ( $keys as $key ) { $values[ $key ] = wp_cache_get( $key, $group, $force ); } return $values; } includes/class-wc-template-loader.php 0000644 00000045156 15132754524 0013677 0 ustar 00 <?php /** * Template Loader * * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * Template loader class. */ class WC_Template_Loader { /** * Store the shop page ID. * * @var integer */ private static $shop_page_id = 0; /** * Store whether we're processing a product inside the_content filter. * * @var boolean */ private static $in_content_filter = false; /** * Is WooCommerce support defined? * * @var boolean */ private static $theme_support = false; /** * Hook in methods. */ public static function init() { self::$theme_support = current_theme_supports( 'woocommerce' ); self::$shop_page_id = wc_get_page_id( 'shop' ); // Supported themes. if ( self::$theme_support ) { add_filter( 'template_include', array( __CLASS__, 'template_loader' ) ); add_filter( 'comments_template', array( __CLASS__, 'comments_template_loader' ) ); } else { // Unsupported themes. add_action( 'template_redirect', array( __CLASS__, 'unsupported_theme_init' ) ); } } /** * Load a template. * * Handles template usage so that we can use our own templates instead of the theme's. * * Templates are in the 'templates' folder. WooCommerce looks for theme * overrides in /theme/woocommerce/ by default. * * For beginners, it also looks for a woocommerce.php template first. If the user adds * this to the theme (containing a woocommerce() inside) this will be used for all * WooCommerce templates. * * @param string $template Template to load. * @return string */ public static function template_loader( $template ) { if ( is_embed() ) { return $template; } $default_file = self::get_template_loader_default_file(); if ( $default_file ) { /** * Filter hook to choose which files to find before WooCommerce does it's own logic. * * @since 3.0.0 * @var array */ $search_files = self::get_template_loader_files( $default_file ); $template = locate_template( $search_files ); if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { if ( false !== strpos( $default_file, 'product_cat' ) || false !== strpos( $default_file, 'product_tag' ) ) { $cs_template = str_replace( '_', '-', $default_file ); $template = WC()->plugin_path() . '/templates/' . $cs_template; } else { $template = WC()->plugin_path() . '/templates/' . $default_file; } } } return $template; } /** * Checks whether a block template with that name exists. * * @since 5.5.0 * @param string $template_name Template to check. * @return boolean */ private static function has_block_template( $template_name ) { if ( ! $template_name ) { return false; } return is_readable( get_stylesheet_directory() . '/block-templates/' . $template_name . '.html' ); } /** * Get the default filename for a template except if a block template with * the same name exists. * * @since 3.0.0 * @since 5.5.0 If a block template with the same name exists, return an * empty string. * @return string */ private static function get_template_loader_default_file() { if ( is_singular( 'product' ) && ! self::has_block_template( 'single-product' ) ) { $default_file = 'single-product.php'; } elseif ( is_product_taxonomy() ) { $object = get_queried_object(); if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { if ( self::has_block_template( 'taxonomy-' . $object->taxonomy ) ) { $default_file = ''; } else { $default_file = 'taxonomy-' . $object->taxonomy . '.php'; } } elseif ( ! self::has_block_template( 'archive-product' ) ) { $default_file = 'archive-product.php'; } } elseif ( ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) && ! self::has_block_template( 'archive-product' ) ) { $default_file = self::$theme_support ? 'archive-product.php' : ''; } else { $default_file = ''; } return $default_file; } /** * Get an array of filenames to search for a given template. * * @since 3.0.0 * @param string $default_file The default file name. * @return string[] */ private static function get_template_loader_files( $default_file ) { $templates = apply_filters( 'woocommerce_template_loader_files', array(), $default_file ); $templates[] = 'woocommerce.php'; if ( is_page_template() ) { $page_template = get_page_template_slug(); if ( $page_template ) { $validated_file = validate_file( $page_template ); if ( 0 === $validated_file ) { $templates[] = $page_template; } else { error_log( "WooCommerce: Unable to validate template path: \"$page_template\". Error Code: $validated_file." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } } if ( is_singular( 'product' ) ) { $object = get_queried_object(); $name_decoded = urldecode( $object->post_name ); if ( $name_decoded !== $object->post_name ) { $templates[] = "single-product-{$name_decoded}.php"; } $templates[] = "single-product-{$object->post_name}.php"; } if ( is_product_taxonomy() ) { $object = get_queried_object(); $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '.php'; if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { $cs_taxonomy = str_replace( '_', '-', $object->taxonomy ); $cs_default = str_replace( '_', '-', $default_file ); $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '-' . $object->slug . '.php'; $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '.php'; $templates[] = $cs_default; } } $templates[] = $default_file; if ( isset( $cs_default ) ) { $templates[] = WC()->template_path() . $cs_default; } $templates[] = WC()->template_path() . $default_file; return array_unique( $templates ); } /** * Load comments template. * * @param string $template template to load. * @return string */ public static function comments_template_loader( $template ) { if ( get_post_type() !== 'product' ) { return $template; } $check_dirs = array( trailingslashit( get_stylesheet_directory() ) . WC()->template_path(), trailingslashit( get_template_directory() ) . WC()->template_path(), trailingslashit( get_stylesheet_directory() ), trailingslashit( get_template_directory() ), trailingslashit( WC()->plugin_path() ) . 'templates/', ); if ( WC_TEMPLATE_DEBUG_MODE ) { $check_dirs = array( array_pop( $check_dirs ) ); } foreach ( $check_dirs as $dir ) { if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) { return trailingslashit( $dir ) . 'single-product-reviews.php'; } } } /** * Unsupported theme compatibility methods. */ /** * Hook in methods to enhance the unsupported theme experience on pages. * * @since 3.3.0 */ public static function unsupported_theme_init() { if ( 0 < self::$shop_page_id ) { if ( is_product_taxonomy() ) { self::unsupported_theme_tax_archive_init(); } elseif ( is_product() ) { self::unsupported_theme_product_page_init(); } else { self::unsupported_theme_shop_page_init(); } } } /** * Hook in methods to enhance the unsupported theme experience on the Shop page. * * @since 3.3.0 */ private static function unsupported_theme_shop_page_init() { add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ), 10 ); add_filter( 'the_title', array( __CLASS__, 'unsupported_theme_title_filter' ), 10, 2 ); add_filter( 'comments_number', array( __CLASS__, 'unsupported_theme_comments_number_filter' ) ); } /** * Hook in methods to enhance the unsupported theme experience on Product pages. * * @since 3.3.0 */ private static function unsupported_theme_product_page_init() { add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ), 10 ); add_filter( 'post_thumbnail_html', array( __CLASS__, 'unsupported_theme_single_featured_image_filter' ) ); add_filter( 'woocommerce_product_tabs', array( __CLASS__, 'unsupported_theme_remove_review_tab' ) ); remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); } /** * Enhance the unsupported theme experience on Product Category and Attribute pages by rendering * those pages using the single template and shortcode-based content. To do this we make a dummy * post and set a shortcode as the post content. This approach is adapted from bbPress. * * @since 3.3.0 */ private static function unsupported_theme_tax_archive_init() { global $wp_query, $post; $queried_object = get_queried_object(); $args = self::get_current_shop_view_args(); $shortcode_args = array( 'page' => $args->page, 'columns' => $args->columns, 'rows' => $args->rows, 'orderby' => '', 'order' => '', 'paginate' => true, 'cache' => false, ); if ( is_product_category() ) { $shortcode_args['category'] = sanitize_title( $queried_object->slug ); } elseif ( taxonomy_is_product_attribute( $queried_object->taxonomy ) ) { $shortcode_args['attribute'] = sanitize_title( $queried_object->taxonomy ); $shortcode_args['terms'] = sanitize_title( $queried_object->slug ); } elseif ( is_product_tag() ) { $shortcode_args['tag'] = sanitize_title( $queried_object->slug ); } else { // Default theme archive for all other taxonomies. return; } // Description handling. if ( ! empty( $queried_object->description ) && ( empty( $_GET['product-page'] ) || 1 === absint( $_GET['product-page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $prefix = '<div class="term-description">' . wc_format_content( wp_kses_post( $queried_object->description ) ) . '</div>'; } else { $prefix = ''; } add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); $shortcode = new WC_Shortcode_Products( $shortcode_args ); remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); $shop_page = get_post( self::$shop_page_id ); $dummy_post_properties = array( 'ID' => 0, 'post_status' => 'publish', 'post_author' => $shop_page->post_author, 'post_parent' => 0, 'post_type' => 'page', 'post_date' => $shop_page->post_date, 'post_date_gmt' => $shop_page->post_date_gmt, 'post_modified' => $shop_page->post_modified, 'post_modified_gmt' => $shop_page->post_modified_gmt, 'post_content' => $prefix . $shortcode->get_content(), 'post_title' => wc_clean( $queried_object->name ), 'post_excerpt' => '', 'post_content_filtered' => '', 'post_mime_type' => '', 'post_password' => '', 'post_name' => $queried_object->slug, 'guid' => '', 'menu_order' => 0, 'pinged' => '', 'to_ping' => '', 'ping_status' => '', 'comment_status' => 'closed', 'comment_count' => 0, 'filter' => 'raw', ); // Set the $post global. $post = new WP_Post( (object) $dummy_post_properties ); // @codingStandardsIgnoreLine. // Copy the new post global into the main $wp_query. $wp_query->post = $post; $wp_query->posts = array( $post ); // Prevent comments form from appearing. $wp_query->post_count = 1; $wp_query->is_404 = false; $wp_query->is_page = true; $wp_query->is_single = true; $wp_query->is_archive = false; $wp_query->is_tax = true; $wp_query->max_num_pages = 0; // Prepare everything for rendering. setup_postdata( $post ); remove_all_filters( 'the_content' ); remove_all_filters( 'the_excerpt' ); add_filter( 'template_include', array( __CLASS__, 'force_single_template_filter' ) ); } /** * Add layered nav args to WP_Query args generated by the 'products' shortcode. * * @since 3.3.4 * @param array $query WP_Query args. * @return array */ public static function unsupported_archive_layered_nav_compatibility( $query ) { foreach ( WC()->query->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { $query['tax_query'][] = array( 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => $data['terms'], 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', 'include_children' => false, ); } return $query; } /** * Force the loading of one of the single templates instead of whatever template was about to be loaded. * * @since 3.3.0 * @param string $template Path to template. * @return string */ public static function force_single_template_filter( $template ) { $possible_templates = array( 'page', 'single', 'singular', 'index', ); foreach ( $possible_templates as $possible_template ) { $path = get_query_template( $possible_template ); if ( $path ) { return $path; } } return $template; } /** * Get information about the current shop page view. * * @since 3.3.0 * @return array */ private static function get_current_shop_view_args() { return (object) array( 'page' => absint( max( 1, absint( get_query_var( 'paged' ) ) ) ), 'columns' => wc_get_default_products_per_row(), 'rows' => wc_get_default_product_rows_per_page(), ); } /** * Filter the title and insert WooCommerce content on the shop page. * * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. * * @since 3.3.0 * @param string $title Existing title. * @param int $id ID of the post being filtered. * @return string */ public static function unsupported_theme_title_filter( $title, $id ) { if ( self::$theme_support || ! $id !== self::$shop_page_id ) { return $title; } if ( is_page( self::$shop_page_id ) || ( is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) === self::$shop_page_id ) ) { $args = self::get_current_shop_view_args(); $title_suffix = array(); if ( $args->page > 1 ) { /* translators: %d: Page number. */ $title_suffix[] = sprintf( esc_html__( 'Page %d', 'woocommerce' ), $args->page ); } if ( $title_suffix ) { $title = $title . ' – ' . implode( ', ', $title_suffix ); } } return $title; } /** * Filter the content and insert WooCommerce content on the shop page. * * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. * * @since 3.3.0 * @param string $content Existing post content. * @return string */ public static function unsupported_theme_shop_content_filter( $content ) { global $wp_query; if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { return $content; } self::$in_content_filter = true; // Remove the filter we're in to avoid nested calls. remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ) ); // Unsupported theme shop page. if ( is_page( self::$shop_page_id ) ) { $args = self::get_current_shop_view_args(); $shortcode = new WC_Shortcode_Products( array_merge( WC()->query->get_catalog_ordering_args(), array( 'page' => $args->page, 'columns' => $args->columns, 'rows' => $args->rows, 'orderby' => '', 'order' => '', 'paginate' => true, 'cache' => false, ) ), 'products' ); // Allow queries to run e.g. layered nav. add_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); $content = $content . $shortcode->get_content(); // Remove actions and self to avoid nested calls. remove_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); WC()->query->remove_ordering_args(); } self::$in_content_filter = false; return $content; } /** * Filter the content and insert WooCommerce content on the shop page. * * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. * * @since 3.3.0 * @param string $content Existing post content. * @return string */ public static function unsupported_theme_product_content_filter( $content ) { global $wp_query; if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { return $content; } self::$in_content_filter = true; // Remove the filter we're in to avoid nested calls. remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ) ); if ( is_product() ) { $content = do_shortcode( '[product_page id="' . get_the_ID() . '" show_title=0 status="any"]' ); } self::$in_content_filter = false; return $content; } /** * Suppress the comments number on the Shop page for unsupported themes since there is no commenting on the Shop page. * * @since 3.4.5 * @param string $comments_number The comments number text. * @return string */ public static function unsupported_theme_comments_number_filter( $comments_number ) { if ( is_page( self::$shop_page_id ) ) { return ''; } return $comments_number; } /** * Are we filtering content for unsupported themes? * * @since 3.3.2 * @return bool */ public static function in_content_filter() { return (bool) self::$in_content_filter; } /** * Prevent the main featured image on product pages because there will be another featured image * in the gallery. * * @since 3.3.0 * @param string $html Img element HTML. * @return string */ public static function unsupported_theme_single_featured_image_filter( $html ) { if ( self::in_content_filter() || ! is_product() || ! is_main_query() ) { return $html; } return ''; } /** * Remove the Review tab and just use the regular comment form. * * @param array $tabs Tab info. * @return array */ public static function unsupported_theme_remove_review_tab( $tabs ) { unset( $tabs['reviews'] ); return $tabs; } } add_action( 'init', array( 'WC_Template_Loader', 'init' ) ); includes/class-wc-discounts.php 0000644 00000101243 15132754524 0012621 0 ustar 00 <?php /** * Discount calculation * * @package WooCommerce\Classes * @since 3.2.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * Discounts class. */ class WC_Discounts { /** * Reference to cart or order object. * * @since 3.2.0 * @var WC_Cart|WC_Order */ protected $object; /** * An array of items to discount. * * @var array */ protected $items = array(); /** * An array of discounts which have been applied to items. * * @var array[] Code => Item Key => Value */ protected $discounts = array(); /** * WC_Discounts Constructor. * * @param WC_Cart|WC_Order $object Cart or order object. */ public function __construct( $object = null ) { if ( is_a( $object, 'WC_Cart' ) ) { $this->set_items_from_cart( $object ); } elseif ( is_a( $object, 'WC_Order' ) ) { $this->set_items_from_order( $object ); } } /** * Set items directly. Used by WC_Cart_Totals. * * @since 3.2.3 * @param array $items Items to set. */ public function set_items( $items ) { $this->items = $items; $this->discounts = array(); uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Normalise cart items which will be discounted. * * @since 3.2.0 * @param WC_Cart $cart Cart object. */ public function set_items_from_cart( $cart ) { $this->items = array(); $this->discounts = array(); if ( ! is_a( $cart, 'WC_Cart' ) ) { return; } $this->object = $cart; foreach ( $cart->get_cart() as $key => $cart_item ) { $item = new stdClass(); $item->key = $key; $item->object = $cart_item; $item->product = $cart_item['data']; $item->quantity = $cart_item['quantity']; $item->price = wc_add_number_precision_deep( (float) $item->product->get_price() * (float) $item->quantity ); $this->items[ $key ] = $item; } uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Normalise order items which will be discounted. * * @since 3.2.0 * @param WC_Order $order Order object. */ public function set_items_from_order( $order ) { $this->items = array(); $this->discounts = array(); if ( ! is_a( $order, 'WC_Order' ) ) { return; } $this->object = $order; foreach ( $order->get_items() as $order_item ) { $item = new stdClass(); $item->key = $order_item->get_id(); $item->object = $order_item; $item->product = $order_item->get_product(); $item->quantity = $order_item->get_quantity(); $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); if ( $order->get_prices_include_tax() ) { $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); } $this->items[ $order_item->get_id() ] = $item; } uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Get the object concerned. * * @since 3.3.2 * @return object */ public function get_object() { return $this->object; } /** * Get items. * * @since 3.2.0 * @return object[] */ public function get_items() { return $this->items; } /** * Get items to validate. * * @since 3.3.2 * @return object[] */ public function get_items_to_validate() { return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); } /** * Get discount by key with or without precision. * * @since 3.2.0 * @param string $key name of discount row to return. * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return float */ public function get_discount( $key, $in_cents = false ) { $item_discount_totals = $this->get_discounts_by_item( $in_cents ); return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; } /** * Get all discount totals. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts( $in_cents = false ) { $discounts = $this->discounts; return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); } /** * Get all discount totals per item. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts_by_item( $in_cents = false ) { $discounts = $this->discounts; $item_discount_totals = (array) array_shift( $discounts ); foreach ( $discounts as $item_discounts ) { foreach ( $item_discounts as $item_key => $item_discount ) { $item_discount_totals[ $item_key ] += $item_discount; } } return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); } /** * Get all discount totals per coupon. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts_by_coupon( $in_cents = false ) { $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); } /** * Get discounted price of an item without precision. * * @since 3.2.0 * @param object $item Get data for this item. * @return float */ public function get_discounted_price( $item ) { return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); } /** * Get discounted price of an item to precision (in cents). * * @since 3.2.0 * @param object $item Get data for this item. * @return int */ public function get_discounted_price_in_cents( $item ) { return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) ); } /** * Apply a discount to all items using a coupon. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object being applied to the items. * @param bool $validate Set to false to skip coupon validation. * @throws Exception Error message when coupon isn't valid. * @return bool|WP_Error True if applied or WP_Error instance in failure. */ public function apply_coupon( $coupon, $validate = true ) { if ( ! is_a( $coupon, 'WC_Coupon' ) ) { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); } $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; if ( is_wp_error( $is_coupon_valid ) ) { return $is_coupon_valid; } if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) { $this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 ); } $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); // Core discounts are handled here as of 3.2. switch ( $coupon->get_discount_type() ) { case 'percent': $this->apply_coupon_percent( $coupon, $items_to_apply ); break; case 'fixed_product': $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); break; case 'fixed_cart': $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); break; default: $this->apply_coupon_custom( $coupon, $items_to_apply ); break; } return true; } /** * Sort by price. * * @since 3.2.0 * @param array $a First element. * @param array $b Second element. * @return int */ protected function sort_by_price( $a, $b ) { $price_1 = $a->price * $a->quantity; $price_2 = $b->price * $b->quantity; if ( $price_1 === $price_2 ) { return 0; } return ( $price_1 < $price_2 ) ? 1 : -1; } /** * Filter out all products which have been fully discounted to 0. * Used as array_filter callback. * * @since 3.2.0 * @param object $item Get data for this item. * @return bool */ protected function filter_products_with_price( $item ) { return $this->get_discounted_price_in_cents( $item ) > 0; } /** * Get items which the coupon should be applied to. * * @since 3.2.0 * @param object $coupon Coupon object. * @return array */ protected function get_items_to_apply_coupon( $coupon ) { $items_to_apply = array(); foreach ( $this->get_items_to_validate() as $item ) { $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { continue; } if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { continue; } $items_to_apply[] = $item_to_apply; } return $items_to_apply; } /** * Apply percent discount to items and return an array of discounts granted. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @return int Total discounted. */ protected function apply_coupon_percent( $coupon, $items_to_apply ) { $total_discount = 0; $cart_total = 0; $limit_usage_qty = 0; $applied_count = 0; $adjust_final_discount = true; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } $coupon_amount = $coupon->get_amount(); foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price ); // See how many and what price to apply to. $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; // Run coupon calculations. $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { // Send through the legacy filter, but not as cents. $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); if ( $filtered_discount !== $discount ) { $discount = $filtered_discount; $adjust_final_discount = false; } } $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); $cart_total = $cart_total + $price_to_discount; $total_discount = $total_discount + $discount; $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); } return $total_discount; } /** * Apply fixed product discount to items. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. * @return int Total discounted. */ protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { $total_discount = 0; $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); $limit_usage_qty = 0; $applied_count = 0; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; // Run coupon calculations. if ( $limit_usage_qty ) { $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; } else { $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); $discount = $amount * $apply_quantity; } if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { // Send through the legacy filter, but not as cents. $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); } $discount = min( $discounted_price, $discount ); $total_discount = $total_discount + $discount; $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } return $total_discount; } /** * Apply fixed cart discount to items. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. * @return int Total discounted. */ protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { $total_discount = 0; $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); if ( ! $item_count ) { return $total_discount; } if ( ! $amount ) { // If there is no amount we still send it through so filters are fired. $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); } else { $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. if ( $per_item_discount > 0 ) { $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); /** * If there is still discount remaining, repeat the process. */ if ( $total_discount > 0 && $total_discount < $amount ) { $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); } } elseif ( $amount > 0 ) { $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); } } return $total_discount; } /** * Apply custom coupon discount to items. * * @since 3.3 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @return int Total discounted. */ protected function apply_coupon_custom( $coupon, $items_to_apply ) { $limit_usage_qty = 0; $applied_count = 0; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } // Apply the coupon to each item. foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); // See how many and what price to apply to. $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); // Run coupon calculations. $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); return array_sum( $this->discounts[ $coupon->get_code() ] ); } /** * Deal with remaining fractional discounts by splitting it over items * until the amount is expired, discounting 1 cent at a time. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply. * @return int Total discounted. */ protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { $total_discount = 0; foreach ( $items_to_apply as $item ) { for ( $i = 0; $i < $item->quantity; $i ++ ) { // Find out how much price is available to discount for the item. $price_to_discount = $this->get_discounted_price_in_cents( $item ); // Run coupon calculations. $discount = min( $price_to_discount, 1 ); // Store totals. $total_discount += $discount; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; if ( $total_discount >= $amount ) { break 2; } } if ( $total_discount >= $amount ) { break; } } return $total_discount; } /** * Ensure coupon exists or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_exists( $coupon ) { if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) { /* translators: %s: coupon code */ throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 ); } return true; } /** * Ensure coupon usage limit is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_usage_limit( $coupon ) { if ( ! $coupon->get_usage_limit() ) { return true; } $usage_count = $coupon->get_usage_count(); $data_store = $coupon->get_data_store(); $tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0; if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) { // All good. return true; } // Coupon usage limit is reached. Let's show as informative error message as we can. if ( 0 === $tentative_usage_count ) { // No held coupon, usage limit is indeed reached. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } elseif ( is_user_logged_in() ) { $recent_pending_orders = wc_get_orders( array( 'limit' => 1, 'post_status' => array( 'wc-failed', 'wc-pending' ), 'customer' => get_current_user_id(), 'return' => 'ids', ) ); if ( count( $recent_pending_orders ) > 0 ) { // User logged in and have a pending order, maybe they are trying to use the coupon. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; } else { $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } } else { // Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST; } throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code ); } /** * Ensure coupon user usage limit is valid or throw exception. * * Per user usage limit - check here if user is logged in (against user IDs). * Checked again for emails later on in WC_Cart::check_customer_coupons(). * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @param int $user_id User ID. * @return bool */ protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { if ( empty( $user_id ) ) { if ( $this->object instanceof WC_Order ) { $user_id = $this->object->get_customer_id(); } else { $user_id = get_current_user_id(); } } if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { $data_store = $coupon->get_data_store(); $usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id ); if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) { $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ); $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; } else { $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } throw new Exception( $error_message, $error_code ); } } return true; } /** * Ensure coupon date is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_expiry_date( $coupon ) { if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); } return true; } /** * Ensure coupon amount is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_minimum_amount( $coupon ) { $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { /* translators: %s: coupon minimum amount */ throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); } return true; } /** * Ensure coupon amount is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_maximum_amount( $coupon ) { $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { /* translators: %s: coupon maximum amount */ throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); } return true; } /** * Ensure coupon is valid for products in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_product_ids( $coupon ) { if ( count( $coupon->get_product_ids() ) > 0 ) { $valid = false; foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Ensure coupon is valid for product categories in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_product_categories( $coupon ) { if ( count( $coupon->get_product_categories() ) > 0 ) { $valid = false; foreach ( $this->get_items_to_validate() as $item ) { if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { continue; } $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); if ( $item->product->get_parent_id() ) { $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); } // If we find an item with a cat in our allowed cat list, the coupon is valid. if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Ensure coupon is valid for sale items in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_sale_items( $coupon ) { if ( $coupon->get_exclude_sale_items() ) { $valid = true; foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && $item->product->is_on_sale() ) { $valid = false; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); } } return true; } /** * All exclusion rules must pass at the same time for a product coupon to be valid. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_items( $coupon ) { $items = $this->get_items_to_validate(); if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { $valid = false; foreach ( $items as $item ) { if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Cart discounts cannot be added if non-eligible product is found. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_eligible_items( $coupon ) { if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { $this->validate_coupon_sale_items( $coupon ); $this->validate_coupon_excluded_product_ids( $coupon ); $this->validate_coupon_excluded_product_categories( $coupon ); } return true; } /** * Exclude products. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_product_ids( $coupon ) { // Exclude Products. if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { $products = array(); foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { $products[] = $item->product->get_name(); } } if ( ! empty( $products ) ) { /* translators: %s: products list */ throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); } } return true; } /** * Exclude categories from product list. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_product_categories( $coupon ) { if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { $categories = array(); foreach ( $this->get_items_to_validate() as $item ) { if ( ! $item->product ) { continue; } $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); if ( $item->product->get_parent_id() ) { $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); } $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); if ( count( $cat_id_list ) > 0 ) { foreach ( $cat_id_list as $cat_id ) { $cat = get_term( $cat_id, 'product_cat' ); $categories[] = $cat->name; } } } if ( ! empty( $categories ) ) { /* translators: %s: categories list */ throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); } } return true; } /** * Get the object subtotal * * @return int */ protected function get_object_subtotal() { if ( is_a( $this->object, 'WC_Cart' ) ) { return wc_add_number_precision( $this->object->get_displayed_subtotal() ); } elseif ( is_a( $this->object, 'WC_Order' ) ) { $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); if ( $this->object->get_prices_include_tax() ) { // Add tax to tax-exclusive subtotal. $subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); } return $subtotal; } else { return array_sum( wp_list_pluck( $this->items, 'price' ) ); } } /** * Check if a coupon is valid. * * Error Codes: * - 100: Invalid filtered. * - 101: Invalid removed. * - 102: Not yours removed. * - 103: Already applied. * - 104: Individual use only. * - 105: Not exists. * - 106: Usage limit reached. * - 107: Expired. * - 108: Minimum spend limit not met. * - 109: Not applicable. * - 110: Not valid for sale items. * - 111: Missing coupon code. * - 112: Maximum spend limit met. * - 113: Excluded products. * - 114: Excluded categories. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool|WP_Error */ public function is_coupon_valid( $coupon ) { try { $this->validate_coupon_exists( $coupon ); $this->validate_coupon_usage_limit( $coupon ); $this->validate_coupon_user_usage_limit( $coupon ); $this->validate_coupon_expiry_date( $coupon ); $this->validate_coupon_minimum_amount( $coupon ); $this->validate_coupon_maximum_amount( $coupon ); $this->validate_coupon_product_ids( $coupon ); $this->validate_coupon_product_categories( $coupon ); $this->validate_coupon_excluded_items( $coupon ); $this->validate_coupon_eligible_items( $coupon ); if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); } } catch ( Exception $e ) { /** * Filter the coupon error message. * * @param string $error_message Error message. * @param int $error_code Error code. * @param WC_Coupon $coupon Coupon data. */ $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); return new WP_Error( 'invalid_coupon', $message, array( 'status' => 400, ) ); } return true; } } includes/class-wc-webhook.php 0000644 00000073742 15132754524 0012260 0 ustar 00 <?php /** * Webhook * * This class handles storing and retrieving webhook data from the associated. * * Webhooks are enqueued to their associated actions, delivered, and logged. * * @version 3.2.0 * @package WooCommerce\Webhooks * @since 2.2.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; require_once __DIR__ . '/legacy/class-wc-legacy-webhook.php'; /** * Webhook class. */ class WC_Webhook extends WC_Legacy_Webhook { /** * Store which object IDs this webhook has processed (ie scheduled to be delivered) * within the current page request. * * @var array */ protected $processed = array(); /** * Stores webhook data. * * @var array */ protected $data = array( 'date_created' => null, 'date_modified' => null, 'status' => 'disabled', 'delivery_url' => '', 'secret' => '', 'name' => '', 'topic' => '', 'hooks' => '', 'resource' => '', 'event' => '', 'failure_count' => 0, 'user_id' => 0, 'api_version' => 3, 'pending_delivery' => false, ); /** * Load webhook data based on how WC_Webhook is called. * * @param WC_Webhook|int $data Webhook ID or data. * @throws Exception If webhook cannot be read/found and $data is set. */ public function __construct( $data = 0 ) { parent::__construct( $data ); if ( $data instanceof WC_Webhook ) { $this->set_id( absint( $data->get_id() ) ); } elseif ( is_numeric( $data ) ) { $this->set_id( $data ); } $this->data_store = WC_Data_Store::load( 'webhook' ); // If we have an ID, load the webhook from the DB. if ( $this->get_id() ) { try { $this->data_store->read( $this ); } catch ( Exception $e ) { $this->set_id( 0 ); $this->set_object_read( true ); } } else { $this->set_object_read( true ); } } /** * Enqueue the hooks associated with the webhook. * * @since 2.2.0 */ public function enqueue() { $hooks = $this->get_hooks(); $url = $this->get_delivery_url(); if ( is_array( $hooks ) && ! empty( $url ) ) { foreach ( $hooks as $hook ) { add_action( $hook, array( $this, 'process' ) ); } } } /** * Process the webhook for delivery by verifying that it should be delivered. * and scheduling the delivery (in the background by default, or immediately). * * @since 2.2.0 * @param mixed $arg The first argument provided from the associated hooks. * @return mixed $arg Returns the argument in case the webhook was hooked into a filter. */ public function process( $arg ) { // Verify that webhook should be processed for delivery. if ( ! $this->should_deliver( $arg ) ) { return; } // Mark this $arg as processed to ensure it doesn't get processed again within the current request. $this->processed[] = $arg; /** * Process webhook delivery. * * @since 3.3.0 * @hooked wc_webhook_process_delivery - 10 */ do_action( 'woocommerce_webhook_process_delivery', $this, $arg ); return $arg; } /** * Helper to check if the webhook should be delivered, as some hooks. * (like `wp_trash_post`) will fire for every post type, not just ours. * * @since 2.2.0 * @param mixed $arg First hook argument. * @return bool True if webhook should be delivered, false otherwise. */ private function should_deliver( $arg ) { $should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg ) && ! $this->is_already_processed( $arg ); /** * Let other plugins intercept deliver for some messages queue like rabbit/zeromq. * * @param bool $should_deliver True if the webhook should be sent, or false to not send it. * @param WC_Webhook $this The current webhook class. * @param mixed $arg First hook argument. */ return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg ); } /** * Returns if webhook is active. * * @since 3.6.0 * @return bool True if validation passes. */ private function is_active() { return 'active' === $this->get_status(); } /** * Returns if topic is valid. * * @since 3.6.0 * @return bool True if validation passes. */ private function is_valid_topic() { return wc_is_webhook_valid_topic( $this->get_topic() ); } /** * Validates the criteria for certain actions. * * @since 3.6.0 * @param mixed $arg First hook argument. * @return bool True if validation passes. */ private function is_valid_action( $arg ) { $current_action = current_action(); $return = true; switch ( $current_action ) { case 'delete_post': case 'wp_trash_post': case 'untrashed_post': $return = $this->is_valid_post_action( $arg ); break; case 'delete_user': $return = $this->is_valid_user_action( $arg ); break; } if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { $return = $this->is_valid_processing_action( $arg ); } return $return; } /** * Validates post actions. * * @since 3.6.0 * @param mixed $arg First hook argument. * @return bool True if validation passes. */ private function is_valid_post_action( $arg ) { // Only deliver deleted/restored event for coupons, orders, and products. if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) { return false; } // Check if is delivering for the correct resource. if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { return false; } return true; } /** * Validates user actions. * * @since 3.6.0 * @param mixed $arg First hook argument. * @return bool True if validation passes. */ private function is_valid_user_action( $arg ) { $user = get_userdata( absint( $arg ) ); // Only deliver deleted customer event for users with customer role. if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) { return false; } return true; } /** * Validates WC processing actions. * * @since 3.6.0 * @param mixed $arg First hook argument. * @return bool True if validation passes. */ private function is_valid_processing_action( $arg ) { // The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks // fire for create and update of products and orders, so check the post // creation date to determine the actual event. $resource = get_post( absint( $arg ) ); // Drafts don't have post_date_gmt so calculate it here. $gmt_date = get_gmt_from_date( $resource->post_date ); // A resource is considered created when the hook is executed within 10 seconds of the post creation date. $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); if ( 'created' === $this->get_event() && ! $resource_created ) { return false; } elseif ( 'updated' === $this->get_event() && $resource_created ) { return false; } return true; } /** * Checks the resource for this webhook is valid e.g. valid post status. * * @since 3.6.0 * @param mixed $arg First hook argument. * @return bool True if validation passes. */ private function is_valid_resource( $arg ) { $resource = $this->get_resource(); if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) { $status = get_post_status( absint( $arg ) ); // Ignore auto drafts for all resources. if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) { return false; } // Ignore standard drafts for orders. if ( 'order' === $resource && 'draft' === $status ) { return false; } // Check registered order types for order types args. if ( 'order' === $resource && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) { return false; } } return true; } /** * Checks if the specified resource has already been queued for delivery within the current request. * * Helps avoid duplication of data being sent for topics that have more than one hook defined. * * @param mixed $arg First hook argument. * * @return bool */ protected function is_already_processed( $arg ) { return false !== array_search( $arg, $this->processed, true ); } /** * Deliver the webhook payload using wp_safe_remote_request(). * * @since 2.2.0 * @param mixed $arg First hook argument. */ public function deliver( $arg ) { $start_time = microtime( true ); $payload = $this->build_payload( $arg ); // Setup request args. $http_args = array( 'method' => 'POST', 'timeout' => MINUTE_IN_SECONDS, 'redirection' => 0, 'httpversion' => '1.0', 'blocking' => true, 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ), 'body' => trim( wp_json_encode( $payload ) ), 'headers' => array( 'Content-Type' => 'application/json', ), 'cookies' => array(), ); $http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() ); // Add custom headers. $delivery_id = $this->get_new_delivery_id(); $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0. $http_args['headers']['X-WC-Webhook-Topic'] = $this->get_topic(); $http_args['headers']['X-WC-Webhook-Resource'] = $this->get_resource(); $http_args['headers']['X-WC-Webhook-Event'] = $this->get_event(); $http_args['headers']['X-WC-Webhook-Signature'] = $this->generate_signature( $http_args['body'] ); $http_args['headers']['X-WC-Webhook-ID'] = $this->get_id(); $http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id; // Webhook away! $response = wp_safe_remote_request( $this->get_delivery_url(), $http_args ); $duration = NumberUtil::round( microtime( true ) - $start_time, 5 ); $this->log_delivery( $delivery_id, $http_args, $response, $duration ); do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() ); } /** * Get Legacy API payload. * * @since 3.0.0 * @param string $resource Resource type. * @param int $resource_id Resource ID. * @param string $event Event type. * @return array */ private function get_legacy_api_payload( $resource, $resource_id, $event ) { // Include & load API classes. WC()->api->includes(); WC()->api->register_resources( new WC_API_Server( '/' ) ); switch ( $resource ) { case 'coupon': $payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id ); break; case 'customer': $payload = WC()->api->WC_API_Customers->get_customer( $resource_id ); break; case 'order': $payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) ); break; case 'product': // Bulk and quick edit action hooks return a product object instead of an ID. if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { $resource_id = $resource_id->get_id(); } $payload = WC()->api->WC_API_Products->get_product( $resource_id ); break; // Custom topics include the first hook argument. case 'action': $payload = array( 'action' => current( $this->get_hooks() ), 'arg' => $resource_id, ); break; default: $payload = array(); break; } return $payload; } /** * Get WP API integration payload. * * @since 3.0.0 * @param string $resource Resource type. * @param int $resource_id Resource ID. * @param string $event Event type. * @return array */ private function get_wp_api_payload( $resource, $resource_id, $event ) { switch ( $resource ) { case 'coupon': case 'customer': case 'order': case 'product': // Bulk and quick edit action hooks return a product object instead of an ID. if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { $resource_id = $resource_id->get_id(); } $version = str_replace( 'wp_api_', '', $this->get_api_version() ); $payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" ); break; // Custom topics include the first hook argument. case 'action': $payload = array( 'action' => current( $this->get_hooks() ), 'arg' => $resource_id, ); break; default: $payload = array(); break; } return $payload; } /** * Build the payload data for the webhook. * * @since 2.2.0 * @param mixed $resource_id First hook argument, typically the resource ID. * @return mixed Payload data. */ public function build_payload( $resource_id ) { // Build the payload with the same user context as the user who created // the webhook -- this avoids permission errors as background processing // runs with no user context. $current_user = get_current_user_id(); wp_set_current_user( $this->get_user_id() ); $resource = $this->get_resource(); $event = $this->get_event(); // If a resource has been deleted, just include the ID. if ( 'deleted' === $event ) { $payload = array( 'id' => $resource_id, ); } else { if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) { $payload = $this->get_wp_api_payload( $resource, $resource_id, $event ); } else { $payload = $this->get_legacy_api_payload( $resource, $resource_id, $event ); } } // Restore the current user. wp_set_current_user( $current_user ); return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->get_id() ); } /** * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the * recipient can verify the authenticity of the webhook. Note that the signature * is calculated after the body has already been encoded (JSON by default). * * @since 2.2.0 * @param string $payload Payload data to hash. * @return string */ public function generate_signature( $payload ) { $hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) ); } /** * Generate a new unique hash as a delivery id based on current time and wehbook id. * Return the hash for inclusion in the webhook request. * * @since 2.2.0 * @return string */ public function get_new_delivery_id() { // Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID. return wp_hash( $this->get_id() . strtotime( 'now' ) ); } /** * Log the delivery request/response. * * @since 2.2.0 * @param string $delivery_id Previously created hash. * @param array $request Request data. * @param array|WP_Error $response Response data. * @param float $duration Request duration. */ public function log_delivery( $delivery_id, $request, $response, $duration ) { $logger = wc_get_logger(); $message = array( 'Webhook Delivery' => array( 'Delivery ID' => $delivery_id, 'Date' => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ), 'URL' => $this->get_delivery_url(), 'Duration' => $duration, 'Request' => array( 'Method' => $request['method'], 'Headers' => array_merge( array( 'User-Agent' => $request['user-agent'], ), $request['headers'] ), ), 'Body' => wp_slash( $request['body'] ), ), ); // Parse response. if ( is_wp_error( $response ) ) { $response_code = $response->get_error_code(); $response_message = $response->get_error_message(); $response_headers = array(); $response_body = ''; } else { $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); $response_headers = wp_remote_retrieve_headers( $response ); $response_body = wp_remote_retrieve_body( $response ); } $message['Webhook Delivery']['Response'] = array( 'Code' => $response_code, 'Message' => $response_message, 'Headers' => $response_headers, 'Body' => $response_body, ); if ( ! Constants::is_true( 'WP_DEBUG' ) ) { $message['Webhook Delivery']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.'; $message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.'; } $logger->info( wc_print_r( $message, true ), array( 'source' => 'webhooks-delivery', ) ); // Track failures. // Check for a success, which is a 2xx, 301 or 302 Response Code. if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) { $this->set_failure_count( 0 ); $this->save(); } else { $this->failed_delivery(); } } /** * Track consecutive delivery failures and automatically disable the webhook. * if more than 5 consecutive failures occur. A failure is defined as a. * non-2xx response. * * @since 2.2.0 */ private function failed_delivery() { $failures = $this->get_failure_count(); if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) { $this->set_status( 'disabled' ); do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() ); } else { $this->set_failure_count( ++$failures ); } $this->save(); } /** * Get the delivery logs for this webhook. * * @since 3.3.0 * @return string */ public function get_delivery_logs() { return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); } /** * Get the delivery log specified by the ID. The delivery log includes: * * + duration * + summary * + request method/url * + request headers/body * + response code/message/headers/body * * @since 2.2 * @deprecated 3.3.0 * @param int $delivery_id Delivery ID. * @return void */ public function get_delivery_log( $delivery_id ) { wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' ); } /** * Send a test ping to the delivery URL, sent when the webhook is first created. * * @since 2.2.0 * @return bool|WP_Error */ public function deliver_ping() { $args = array( 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ), 'body' => 'webhook_id=' . $this->get_id(), ); $test = wp_safe_remote_post( $this->get_delivery_url(), $args ); $response_code = wp_remote_retrieve_response_code( $test ); if ( is_wp_error( $test ) ) { /* translators: error message */ return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) ); } if ( 200 !== $response_code ) { /* translators: error message */ return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) ); } $this->set_pending_delivery( false ); $this->save(); return true; } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get the friendly name for the webhook. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() ); } /** * Get the webhook status. * * - 'active' - delivers payload. * - 'paused' - does not deliver payload, paused by admin. * - 'disabled' - does not delivery payload, paused automatically due to consecutive failures. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string status */ public function get_status( $context = 'view' ) { return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() ); } /** * Get webhook created date. * * @since 3.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return WC_DateTime|null Object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { return $this->get_prop( 'date_created', $context ); } /** * Get webhook modified date. * * @since 3.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return WC_DateTime|null Object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { return $this->get_prop( 'date_modified', $context ); } /** * Get the secret used for generating the HMAC-SHA256 signature. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string */ public function get_secret( $context = 'view' ) { return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() ); } /** * Get the webhook topic, e.g. `order.created`. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string */ public function get_topic( $context = 'view' ) { return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() ); } /** * Get the delivery URL. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string */ public function get_delivery_url( $context = 'view' ) { return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() ); } /** * Get the user ID for this webhook. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return int */ public function get_user_id( $context = 'view' ) { return $this->get_prop( 'user_id', $context ); } /** * API version. * * @since 3.0.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return string */ public function get_api_version( $context = 'view' ) { $version = $this->get_prop( 'api_version', $context ); return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3'; } /** * Get the failure count. * * @since 2.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return int */ public function get_failure_count( $context = 'view' ) { return $this->get_prop( 'failure_count', $context ); } /** * Get pending delivery. * * @since 3.2.0 * @param string $context What the value is for. * Valid values are 'view' and 'edit'. * @return bool */ public function get_pending_delivery( $context = 'view' ) { return $this->get_prop( 'pending_delivery', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set webhook name. * * @since 3.2.0 * @param string $name Webhook name. */ public function set_name( $name ) { $this->set_prop( 'name', $name ); } /** * Set webhook created date. * * @since 3.2.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. * If the DateTime string has no timezone or offset, * WordPress site timezone will be assumed. * Null if their is no date. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set webhook modified date. * * @since 3.2.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. * If the DateTime string has no timezone or offset, * WordPress site timezone will be assumed. * Null if their is no date. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set status. * * @since 3.2.0 * @param string $status Status. */ public function set_status( $status ) { if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { $status = 'disabled'; } $this->set_prop( 'status', $status ); } /** * Set the secret used for generating the HMAC-SHA256 signature. * * @since 2.2.0 * @param string $secret Secret. */ public function set_secret( $secret ) { $this->set_prop( 'secret', $secret ); } /** * Set the webhook topic and associated hooks. * The topic resource & event are also saved separately. * * @since 2.2.0 * @param string $topic Webhook topic. */ public function set_topic( $topic ) { $topic = wc_clean( $topic ); if ( ! wc_is_webhook_valid_topic( $topic ) ) { $topic = ''; } $this->set_prop( 'topic', $topic ); } /** * Set the delivery URL. * * @since 2.2.0 * @param string $url Delivery URL. */ public function set_delivery_url( $url ) { $this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) ); } /** * Set user ID. * * @since 3.2.0 * @param int $user_id User ID. */ public function set_user_id( $user_id ) { $this->set_prop( 'user_id', (int) $user_id ); } /** * Set API version. * * @since 3.0.0 * @param int|string $version REST API version. */ public function set_api_version( $version ) { if ( ! is_numeric( $version ) ) { $version = $this->data_store->get_api_version_number( $version ); } $this->set_prop( 'api_version', (int) $version ); } /** * Set pending delivery. * * @since 3.2.0 * @param bool $pending_delivery Set true if is pending for delivery. */ public function set_pending_delivery( $pending_delivery ) { $this->set_prop( 'pending_delivery', (bool) $pending_delivery ); } /** * Set failure count. * * @since 3.2.0 * @param bool $failure_count Total of failures. */ public function set_failure_count( $failure_count ) { $this->set_prop( 'failure_count', intval( $failure_count ) ); } /* |-------------------------------------------------------------------------- | Non-CRUD Getters |-------------------------------------------------------------------------- */ /** * Get the associated hook names for a topic. * * @since 2.2.0 * @param string $topic Topic name. * @return array */ private function get_topic_hooks( $topic ) { $topic_hooks = array( 'coupon.created' => array( 'woocommerce_process_shop_coupon_meta', 'woocommerce_new_coupon', ), 'coupon.updated' => array( 'woocommerce_process_shop_coupon_meta', 'woocommerce_update_coupon', ), 'coupon.deleted' => array( 'wp_trash_post', ), 'coupon.restored' => array( 'untrashed_post', ), 'customer.created' => array( 'user_register', 'woocommerce_created_customer', 'woocommerce_new_customer', ), 'customer.updated' => array( 'profile_update', 'woocommerce_update_customer', ), 'customer.deleted' => array( 'delete_user', ), 'order.created' => array( 'woocommerce_new_order', ), 'order.updated' => array( 'woocommerce_update_order', 'woocommerce_order_refunded', ), 'order.deleted' => array( 'wp_trash_post', ), 'order.restored' => array( 'untrashed_post', ), 'product.created' => array( 'woocommerce_process_product_meta', 'woocommerce_new_product', 'woocommerce_new_product_variation', ), 'product.updated' => array( 'woocommerce_process_product_meta', 'woocommerce_update_product', 'woocommerce_update_product_variation', ), 'product.deleted' => array( 'wp_trash_post', ), 'product.restored' => array( 'untrashed_post', ), ); $topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this ); return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array(); } /** * Get the hook names for the webhook. * * @since 2.2.0 * @return array */ public function get_hooks() { if ( 'action' === $this->get_resource() ) { $hooks = array( $this->get_event() ); } else { $hooks = $this->get_topic_hooks( $this->get_topic() ); } return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() ); } /** * Get the resource for the webhook, e.g. `order`. * * @since 2.2.0 * @return string */ public function get_resource() { $topic = explode( '.', $this->get_topic() ); return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() ); } /** * Get the event for the webhook, e.g. `created`. * * @since 2.2.0 * @return string */ public function get_event() { $topic = explode( '.', $this->get_topic() ); return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_id() ); } /** * Get the webhook i18n status. * * @return string */ public function get_i18n_status() { $status = $this->get_status(); $statuses = wc_get_webhook_statuses(); return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status; } } includes/class-wc-geo-ip.php 0000644 00000074645 15132754524 0012005 0 ustar 00 <?php /** * Geo IP class * * This class is a fork of GeoIP class from MaxMind LLC. * * @package WooCommerce\Classes * @version 2.4.0 * @deprecated 3.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Geo_IP Class. * * @deprecated 3.4.0 */ class WC_Geo_IP { const GEOIP_COUNTRY_BEGIN = 16776960; const GEOIP_STATE_BEGIN_REV0 = 16700000; const GEOIP_STATE_BEGIN_REV1 = 16000000; const GEOIP_MEMORY_CACHE = 1; const GEOIP_SHARED_MEMORY = 2; const STRUCTURE_INFO_MAX_SIZE = 20; const GEOIP_COUNTRY_EDITION = 1; const GEOIP_PROXY_EDITION = 8; const GEOIP_ASNUM_EDITION = 9; const GEOIP_NETSPEED_EDITION = 10; const GEOIP_REGION_EDITION_REV0 = 7; const GEOIP_REGION_EDITION_REV1 = 3; const GEOIP_CITY_EDITION_REV0 = 6; const GEOIP_CITY_EDITION_REV1 = 2; const GEOIP_ORG_EDITION = 5; const GEOIP_ISP_EDITION = 4; const SEGMENT_RECORD_LENGTH = 3; const STANDARD_RECORD_LENGTH = 3; const ORG_RECORD_LENGTH = 4; const GEOIP_SHM_KEY = 0x4f415401; const GEOIP_DOMAIN_EDITION = 11; const GEOIP_COUNTRY_EDITION_V6 = 12; const GEOIP_LOCATIONA_EDITION = 13; const GEOIP_ACCURACYRADIUS_EDITION = 14; const GEOIP_CITY_EDITION_REV1_V6 = 30; const GEOIP_CITY_EDITION_REV0_V6 = 31; const GEOIP_NETSPEED_EDITION_REV1 = 32; const GEOIP_NETSPEED_EDITION_REV1_V6 = 33; const GEOIP_USERTYPE_EDITION = 28; const GEOIP_USERTYPE_EDITION_V6 = 29; const GEOIP_ASNUM_EDITION_V6 = 21; const GEOIP_ISP_EDITION_V6 = 22; const GEOIP_ORG_EDITION_V6 = 23; const GEOIP_DOMAIN_EDITION_V6 = 24; /** * Flags. * * @var int */ public $flags; /** * File handler. * * @var resource */ public $filehandle; /** * Memory buffer. * * @var string */ public $memory_buffer; /** * Database type. * * @var int */ public $databaseType; /** * Database segments. * * @var int */ public $databaseSegments; /** * Record length. * * @var int */ public $record_length; /** * Shmid. * * @var string */ public $shmid; /** * Two letters country codes. * * @var array */ public $GEOIP_COUNTRY_CODES = array( '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'CW', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'SX', 'GA', 'GB', 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1', 'AX', 'GG', 'IM', 'JE', 'BL', 'MF', 'BQ', 'SS', 'O1', ); /** * 3 letters country codes. * * @var array */ public $GEOIP_COUNTRY_CODES3 = array( '', 'AP', 'EU', 'AND', 'ARE', 'AFG', 'ATG', 'AIA', 'ALB', 'ARM', 'CUW', 'AGO', 'ATA', 'ARG', 'ASM', 'AUT', 'AUS', 'ABW', 'AZE', 'BIH', 'BRB', 'BGD', 'BEL', 'BFA', 'BGR', 'BHR', 'BDI', 'BEN', 'BMU', 'BRN', 'BOL', 'BRA', 'BHS', 'BTN', 'BVT', 'BWA', 'BLR', 'BLZ', 'CAN', 'CCK', 'COD', 'CAF', 'COG', 'CHE', 'CIV', 'COK', 'CHL', 'CMR', 'CHN', 'COL', 'CRI', 'CUB', 'CPV', 'CXR', 'CYP', 'CZE', 'DEU', 'DJI', 'DNK', 'DMA', 'DOM', 'DZA', 'ECU', 'EST', 'EGY', 'ESH', 'ERI', 'ESP', 'ETH', 'FIN', 'FJI', 'FLK', 'FSM', 'FRO', 'FRA', 'SXM', 'GAB', 'GBR', 'GRD', 'GEO', 'GUF', 'GHA', 'GIB', 'GRL', 'GMB', 'GIN', 'GLP', 'GNQ', 'GRC', 'SGS', 'GTM', 'GUM', 'GNB', 'GUY', 'HKG', 'HMD', 'HND', 'HRV', 'HTI', 'HUN', 'IDN', 'IRL', 'ISR', 'IND', 'IOT', 'IRQ', 'IRN', 'ISL', 'ITA', 'JAM', 'JOR', 'JPN', 'KEN', 'KGZ', 'KHM', 'KIR', 'COM', 'KNA', 'PRK', 'KOR', 'KWT', 'CYM', 'KAZ', 'LAO', 'LBN', 'LCA', 'LIE', 'LKA', 'LBR', 'LSO', 'LTU', 'LUX', 'LVA', 'LBY', 'MAR', 'MCO', 'MDA', 'MDG', 'MHL', 'MKD', 'MLI', 'MMR', 'MNG', 'MAC', 'MNP', 'MTQ', 'MRT', 'MSR', 'MLT', 'MUS', 'MDV', 'MWI', 'MEX', 'MYS', 'MOZ', 'NAM', 'NCL', 'NER', 'NFK', 'NGA', 'NIC', 'NLD', 'NOR', 'NPL', 'NRU', 'NIU', 'NZL', 'OMN', 'PAN', 'PER', 'PYF', 'PNG', 'PHL', 'PAK', 'POL', 'SPM', 'PCN', 'PRI', 'PSE', 'PRT', 'PLW', 'PRY', 'QAT', 'REU', 'ROU', 'RUS', 'RWA', 'SAU', 'SLB', 'SYC', 'SDN', 'SWE', 'SGP', 'SHN', 'SVN', 'SJM', 'SVK', 'SLE', 'SMR', 'SEN', 'SOM', 'SUR', 'STP', 'SLV', 'SYR', 'SWZ', 'TCA', 'TCD', 'ATF', 'TGO', 'THA', 'TJK', 'TKL', 'TKM', 'TUN', 'TON', 'TLS', 'TUR', 'TTO', 'TUV', 'TWN', 'TZA', 'UKR', 'UGA', 'UMI', 'USA', 'URY', 'UZB', 'VAT', 'VCT', 'VEN', 'VGB', 'VIR', 'VNM', 'VUT', 'WLF', 'WSM', 'YEM', 'MYT', 'SRB', 'ZAF', 'ZMB', 'MNE', 'ZWE', 'A1', 'A2', 'O1', 'ALA', 'GGY', 'IMN', 'JEY', 'BLM', 'MAF', 'BES', 'SSD', 'O1', ); /** * Contry names. * * @var array */ public $GEOIP_COUNTRY_NAMES = array( '', 'Asia/Pacific Region', 'Europe', 'Andorra', 'United Arab Emirates', 'Afghanistan', 'Antigua and Barbuda', 'Anguilla', 'Albania', 'Armenia', 'Curacao', 'Angola', 'Antarctica', 'Argentina', 'American Samoa', 'Austria', 'Australia', 'Aruba', 'Azerbaijan', 'Bosnia and Herzegovina', 'Barbados', 'Bangladesh', 'Belgium', 'Burkina Faso', 'Bulgaria', 'Bahrain', 'Burundi', 'Benin', 'Bermuda', 'Brunei Darussalam', 'Bolivia', 'Brazil', 'Bahamas', 'Bhutan', 'Bouvet Island', 'Botswana', 'Belarus', 'Belize', 'Canada', 'Cocos (Keeling) Islands', 'Congo, The Democratic Republic of the', 'Central African Republic', 'Congo', 'Switzerland', "Cote D'Ivoire", 'Cook Islands', 'Chile', 'Cameroon', 'China', 'Colombia', 'Costa Rica', 'Cuba', 'Cape Verde', 'Christmas Island', 'Cyprus', 'Czech Republic', 'Germany', 'Djibouti', 'Denmark', 'Dominica', 'Dominican Republic', 'Algeria', 'Ecuador', 'Estonia', 'Egypt', 'Western Sahara', 'Eritrea', 'Spain', 'Ethiopia', 'Finland', 'Fiji', 'Falkland Islands (Malvinas)', 'Micronesia, Federated States of', 'Faroe Islands', 'France', 'Sint Maarten (Dutch part)', 'Gabon', 'United Kingdom', 'Grenada', 'Georgia', 'French Guiana', 'Ghana', 'Gibraltar', 'Greenland', 'Gambia', 'Guinea', 'Guadeloupe', 'Equatorial Guinea', 'Greece', 'South Georgia and the South Sandwich Islands', 'Guatemala', 'Guam', 'Guinea-Bissau', 'Guyana', 'Hong Kong', 'Heard Island and McDonald Islands', 'Honduras', 'Croatia', 'Haiti', 'Hungary', 'Indonesia', 'Ireland', 'Israel', 'India', 'British Indian Ocean Territory', 'Iraq', 'Iran, Islamic Republic of', 'Iceland', 'Italy', 'Jamaica', 'Jordan', 'Japan', 'Kenya', 'Kyrgyzstan', 'Cambodia', 'Kiribati', 'Comoros', 'Saint Kitts and Nevis', "Korea, Democratic People's Republic of", 'Korea, Republic of', 'Kuwait', 'Cayman Islands', 'Kazakhstan', "Lao People's Democratic Republic", 'Lebanon', 'Saint Lucia', 'Liechtenstein', 'Sri Lanka', 'Liberia', 'Lesotho', 'Lithuania', 'Luxembourg', 'Latvia', 'Libya', 'Morocco', 'Monaco', 'Moldova, Republic of', 'Madagascar', 'Marshall Islands', 'Macedonia', 'Mali', 'Myanmar', 'Mongolia', 'Macau', 'Northern Mariana Islands', 'Martinique', 'Mauritania', 'Montserrat', 'Malta', 'Mauritius', 'Maldives', 'Malawi', 'Mexico', 'Malaysia', 'Mozambique', 'Namibia', 'New Caledonia', 'Niger', 'Norfolk Island', 'Nigeria', 'Nicaragua', 'Netherlands', 'Norway', 'Nepal', 'Nauru', 'Niue', 'New Zealand', 'Oman', 'Panama', 'Peru', 'French Polynesia', 'Papua New Guinea', 'Philippines', 'Pakistan', 'Poland', 'Saint Pierre and Miquelon', 'Pitcairn Islands', 'Puerto Rico', 'Palestinian Territory', 'Portugal', 'Palau', 'Paraguay', 'Qatar', 'Reunion', 'Romania', 'Russian Federation', 'Rwanda', 'Saudi Arabia', 'Solomon Islands', 'Seychelles', 'Sudan', 'Sweden', 'Singapore', 'Saint Helena', 'Slovenia', 'Svalbard and Jan Mayen', 'Slovakia', 'Sierra Leone', 'San Marino', 'Senegal', 'Somalia', 'Suriname', 'Sao Tome and Principe', 'El Salvador', 'Syrian Arab Republic', 'Swaziland', 'Turks and Caicos Islands', 'Chad', 'French Southern Territories', 'Togo', 'Thailand', 'Tajikistan', 'Tokelau', 'Turkmenistan', 'Tunisia', 'Tonga', 'Timor-Leste', 'Turkey', 'Trinidad and Tobago', 'Tuvalu', 'Taiwan', 'Tanzania, United Republic of', 'Ukraine', 'Uganda', 'United States Minor Outlying Islands', 'United States', 'Uruguay', 'Uzbekistan', 'Holy See (Vatican City State)', 'Saint Vincent and the Grenadines', 'Venezuela', 'Virgin Islands, British', 'Virgin Islands, U.S.', 'Vietnam', 'Vanuatu', 'Wallis and Futuna', 'Samoa', 'Yemen', 'Mayotte', 'Serbia', 'South Africa', 'Zambia', 'Montenegro', 'Zimbabwe', 'Anonymous Proxy', 'Satellite Provider', 'Other', 'Aland Islands', 'Guernsey', 'Isle of Man', 'Jersey', 'Saint Barthelemy', 'Saint Martin', 'Bonaire, Saint Eustatius and Saba', 'South Sudan', 'Other', ); /** * 2 letters continent codes. * * @var array */ public $GEOIP_CONTINENT_CODES = array( '--', 'AS', 'EU', 'EU', 'AS', 'AS', 'NA', 'NA', 'EU', 'AS', 'NA', 'AF', 'AN', 'SA', 'OC', 'EU', 'OC', 'NA', 'AS', 'EU', 'NA', 'AS', 'EU', 'AF', 'EU', 'AS', 'AF', 'AF', 'NA', 'AS', 'SA', 'SA', 'NA', 'AS', 'AN', 'AF', 'EU', 'NA', 'NA', 'AS', 'AF', 'AF', 'AF', 'EU', 'AF', 'OC', 'SA', 'AF', 'AS', 'SA', 'NA', 'NA', 'AF', 'AS', 'AS', 'EU', 'EU', 'AF', 'EU', 'NA', 'NA', 'AF', 'SA', 'EU', 'AF', 'AF', 'AF', 'EU', 'AF', 'EU', 'OC', 'SA', 'OC', 'EU', 'EU', 'NA', 'AF', 'EU', 'NA', 'AS', 'SA', 'AF', 'EU', 'NA', 'AF', 'AF', 'NA', 'AF', 'EU', 'AN', 'NA', 'OC', 'AF', 'SA', 'AS', 'AN', 'NA', 'EU', 'NA', 'EU', 'AS', 'EU', 'AS', 'AS', 'AS', 'AS', 'AS', 'EU', 'EU', 'NA', 'AS', 'AS', 'AF', 'AS', 'AS', 'OC', 'AF', 'NA', 'AS', 'AS', 'AS', 'NA', 'AS', 'AS', 'AS', 'NA', 'EU', 'AS', 'AF', 'AF', 'EU', 'EU', 'EU', 'AF', 'AF', 'EU', 'EU', 'AF', 'OC', 'EU', 'AF', 'AS', 'AS', 'AS', 'OC', 'NA', 'AF', 'NA', 'EU', 'AF', 'AS', 'AF', 'NA', 'AS', 'AF', 'AF', 'OC', 'AF', 'OC', 'AF', 'NA', 'EU', 'EU', 'AS', 'OC', 'OC', 'OC', 'AS', 'NA', 'SA', 'OC', 'OC', 'AS', 'AS', 'EU', 'NA', 'OC', 'NA', 'AS', 'EU', 'OC', 'SA', 'AS', 'AF', 'EU', 'EU', 'AF', 'AS', 'OC', 'AF', 'AF', 'EU', 'AS', 'AF', 'EU', 'EU', 'EU', 'AF', 'EU', 'AF', 'AF', 'SA', 'AF', 'NA', 'AS', 'AF', 'NA', 'AF', 'AN', 'AF', 'AS', 'AS', 'OC', 'AS', 'AF', 'OC', 'AS', 'EU', 'NA', 'OC', 'AS', 'AF', 'EU', 'AF', 'OC', 'NA', 'SA', 'AS', 'EU', 'NA', 'SA', 'NA', 'NA', 'AS', 'OC', 'OC', 'OC', 'AS', 'AF', 'EU', 'AF', 'AF', 'EU', 'AF', '--', '--', '--', 'EU', 'EU', 'EU', 'EU', 'NA', 'NA', 'NA', 'AF', '--', ); /** @var WC_Logger Logger instance */ public static $log = false; /** * Logging method. * * @param string $message Log message. * @param string $level Optional. Default 'info'. * emergency|alert|critical|error|warning|notice|info|debug */ public static function log( $message, $level = 'info' ) { if ( empty( self::$log ) ) { self::$log = wc_get_logger(); } self::$log->log( $level, $message, array( 'source' => 'geoip' ) ); } /** * Open geoip file. * * @param string $filename * @param int $flags */ public function geoip_open( $filename, $flags ) { $this->flags = $flags; if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { $this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 ); } else { if ( $this->filehandle = fopen( $filename, 'rb' ) ) { if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { $s_array = fstat( $this->filehandle ); $this->memory_buffer = fread( $this->filehandle, $s_array['size'] ); } } else { $this->log( 'GeoIP API: Can not open ' . $filename, 'error' ); } } $this->_setup_segments(); } /** * Setup segments. * * @return WC_Geo_IP instance */ private function _setup_segments() { $this->databaseType = self::GEOIP_COUNTRY_EDITION; $this->record_length = self::STANDARD_RECORD_LENGTH; if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { $offset = @shmop_size( $this->shmid ) - 3; for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { $delim = @shmop_read( $this->shmid, $offset, 3 ); $offset += 3; if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { $this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) ); if ( $this->databaseType >= 106 ) { $this->databaseType -= 105; } $offset++; if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) || ( self::GEOIP_ORG_EDITION == $this->databaseType ) || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_ISP_EDITION == $this->databaseType ) || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) ) { $this->databaseSegments = 0; $buf = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH ); for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); } if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_ISP_EDITION == $this->databaseType ) || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) ) { $this->record_length = self::ORG_RECORD_LENGTH; } } break; } else { $offset -= 4; } } if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) ) { $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; } } else { $filepos = ftell( $this->filehandle ); fseek( $this->filehandle, -3, SEEK_END ); for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { $delim = fread( $this->filehandle, 3 ); if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { $this->databaseType = ord( fread( $this->filehandle, 1 ) ); if ( $this->databaseType >= 106 ) { $this->databaseType -= 105; } if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) || ( self::GEOIP_ORG_EDITION == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) || ( self::GEOIP_ISP_EDITION == $this->databaseType ) || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) ) { $this->databaseSegments = 0; $buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH ); for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); } if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) || ( self::GEOIP_ISP_EDITION == $this->databaseType ) || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) ) { $this->record_length = self::ORG_RECORD_LENGTH; } } break; } else { fseek( $this->filehandle, -4, SEEK_CUR ); } } if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) ) { $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; } fseek( $this->filehandle, $filepos, SEEK_SET ); } return $this; } /** * Close geoip file. * * @return bool */ public function geoip_close() { if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { return true; } return fclose( $this->filehandle ); } /** * Common get record. * * @param string $seek_country * @return WC_Geo_IP_Record instance */ private function _common_get_record( $seek_country ) { // workaround php's broken substr, strpos, etc handling with // mbstring.func_overload and mbstring.internal_encoding $mbExists = extension_loaded( 'mbstring' ); if ( $mbExists ) { $enc = mb_internal_encoding(); mb_internal_encoding( 'ISO-8859-1' ); } $record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments; if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { $record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH ); } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { $record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH ); } else { fseek( $this->filehandle, $record_pointer, SEEK_SET ); $record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH ); } $record = new WC_Geo_IP_Record(); $record_buf_pos = 0; $char = ord( substr( $record_buf, $record_buf_pos, 1 ) ); $record->country_code = $this->GEOIP_COUNTRY_CODES[ $char ]; $record->country_code3 = $this->GEOIP_COUNTRY_CODES3[ $char ]; $record->country_name = $this->GEOIP_COUNTRY_NAMES[ $char ]; $record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ]; $str_length = 0; $record_buf_pos++; // Get region $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); while ( 0 != $char ) { $str_length++; $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); } if ( $str_length > 0 ) { $record->region = substr( $record_buf, $record_buf_pos, $str_length ); } $record_buf_pos += $str_length + 1; $str_length = 0; // Get city $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); while ( 0 != $char ) { $str_length++; $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); } if ( $str_length > 0 ) { $record->city = substr( $record_buf, $record_buf_pos, $str_length ); } $record_buf_pos += $str_length + 1; $str_length = 0; // Get postal code $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); while ( 0 != $char ) { $str_length++; $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); } if ( $str_length > 0 ) { $record->postal_code = substr( $record_buf, $record_buf_pos, $str_length ); } $record_buf_pos += $str_length + 1; // Get latitude and longitude $latitude = 0; $longitude = 0; for ( $j = 0; $j < 3; ++$j ) { $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); $latitude += ( $char << ( $j * 8 ) ); } $record->latitude = ( $latitude / 10000 ) - 180; for ( $j = 0; $j < 3; ++$j ) { $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); $longitude += ( $char << ( $j * 8 ) ); } $record->longitude = ( $longitude / 10000 ) - 180; if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { $metroarea_combo = 0; if ( 'US' === $record->country_code ) { for ( $j = 0; $j < 3; ++$j ) { $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); $metroarea_combo += ( $char << ( $j * 8 ) ); } $record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 ); $record->area_code = $metroarea_combo % 1000; } } if ( $mbExists ) { mb_internal_encoding( $enc ); } return $record; } /** * Get record. * * @param int $ipnum * @return WC_Geo_IP_Record instance */ private function _get_record( $ipnum ) { $seek_country = $this->_geoip_seek_country( $ipnum ); if ( $seek_country == $this->databaseSegments ) { return null; } return $this->_common_get_record( $seek_country ); } /** * Seek country IPv6. * * @param int $ipnum * @return string */ public function _geoip_seek_country_v6( $ipnum ) { // arrays from unpack start with offset 1 // yet another php mystery. array_merge work around // this broken behaviour $v6vec = array_merge( unpack( 'C16', $ipnum ) ); $offset = 0; for ( $depth = 127; $depth >= 0; --$depth ) { if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { $buf = $this->_safe_substr( $this->memory_buffer, 2 * $this->record_length * $offset, 2 * $this->record_length ); } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { $buf = @shmop_read( $this->shmid, 2 * $this->record_length * $offset, 2 * $this->record_length ); } else { if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { break; } $buf = fread( $this->filehandle, 2 * $this->record_length ); } $x = array( 0, 0 ); for ( $i = 0; $i < 2; ++$i ) { for ( $j = 0; $j < $this->record_length; ++$j ) { $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); } } $bnum = 127 - $depth; $idx = $bnum >> 3; $b_mask = 1 << ( $bnum & 7 ^ 7 ); if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) { if ( $x[1] >= $this->databaseSegments ) { return $x[1]; } $offset = $x[1]; } else { if ( $x[0] >= $this->databaseSegments ) { return $x[0]; } $offset = $x[0]; } } $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); return false; } /** * Seek country. * * @param int $ipnum * @return string */ private function _geoip_seek_country( $ipnum ) { $offset = 0; for ( $depth = 31; $depth >= 0; --$depth ) { if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { $buf = $this->_safe_substr( $this->memory_buffer, 2 * $this->record_length * $offset, 2 * $this->record_length ); } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { $buf = @shmop_read( $this->shmid, 2 * $this->record_length * $offset, 2 * $this->record_length ); } else { if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { break; } $buf = fread( $this->filehandle, 2 * $this->record_length ); } $x = array( 0, 0 ); for ( $i = 0; $i < 2; ++$i ) { for ( $j = 0; $j < $this->record_length; ++$j ) { $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); } } if ( $ipnum & ( 1 << $depth ) ) { if ( $x[1] >= $this->databaseSegments ) { return $x[1]; } $offset = $x[1]; } else { if ( $x[0] >= $this->databaseSegments ) { return $x[0]; } $offset = $x[0]; } } $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); return false; } /** * Record by addr. * * @param string $addr * * @return WC_Geo_IP_Record */ public function geoip_record_by_addr( $addr ) { if ( null == $addr ) { return 0; } $ipnum = ip2long( $addr ); return $this->_get_record( $ipnum ); } /** * Country ID by addr IPv6. * * @param string $addr * @return int|bool */ public function geoip_country_id_by_addr_v6( $addr ) { if ( ! defined( 'AF_INET6' ) ) { $this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' ); return false; } $ipnum = inet_pton( $addr ); return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; } /** * Country ID by addr. * * @param string $addr * @return int */ public function geoip_country_id_by_addr( $addr ) { $ipnum = ip2long( $addr ); return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; } /** * Country code by addr IPv6. * * @param string $addr * @return string */ public function geoip_country_code_by_addr_v6( $addr ) { $country_id = $this->geoip_country_id_by_addr_v6( $addr ); if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { return $this->GEOIP_COUNTRY_CODES[ $country_id ]; } return false; } /** * Country code by addr. * * @param string $addr * @return string */ public function geoip_country_code_by_addr( $addr ) { if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { $record = $this->geoip_record_by_addr( $addr ); if ( false !== $record ) { return $record->country_code; } } else { $country_id = $this->geoip_country_id_by_addr( $addr ); if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { return $this->GEOIP_COUNTRY_CODES[ $country_id ]; } } return false; } /** * Encode string. * * @param string $string * @param int $start * @param int $length * @return string */ private function _safe_substr( $string, $start, $length ) { // workaround php's broken substr, strpos, etc handling with // mbstring.func_overload and mbstring.internal_encoding $mb_exists = extension_loaded( 'mbstring' ); if ( $mb_exists ) { $enc = mb_internal_encoding(); mb_internal_encoding( 'ISO-8859-1' ); } $buf = substr( $string, $start, $length ); if ( $mb_exists ) { mb_internal_encoding( $enc ); } return $buf; } } /** * Geo IP Record class. */ class WC_Geo_IP_Record { /** * Country code. * * @var string */ public $country_code; /** * 3 letters country code. * * @var string */ public $country_code3; /** * Country name. * * @var string */ public $country_name; /** * Region. * * @var string */ public $region; /** * City. * * @var string */ public $city; /** * Postal code. * * @var string */ public $postal_code; /** * Latitude * * @var int */ public $latitude; /** * Longitude. * * @var int */ public $longitude; /** * Area code. * * @var int */ public $area_code; /** * DMA Code. * * Metro and DMA code are the same. * Use metro code instead. * * @var float */ public $dma_code; /** * Metro code. * * @var float */ public $metro_code; /** * Continent code. * * @var string */ public $continent_code; } includes/class-wc-cart.php 0000644 00000201721 15132754524 0011541 0 ustar 00 <?php /** * WooCommerce cart * * The WooCommerce cart class stores cart data and active coupons as well as handling customer sessions and some cart related urls. * The cart class also has a price calculation function which calls upon other classes to calculate totals. * * @package WooCommerce\Classes * @version 2.1.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; require_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-cart.php'; require_once WC_ABSPATH . 'includes/class-wc-cart-fees.php'; require_once WC_ABSPATH . 'includes/class-wc-cart-session.php'; /** * WC_Cart class. */ class WC_Cart extends WC_Legacy_Cart { /** * Contains an array of cart items. * * @var array */ public $cart_contents = array(); /** * Contains an array of removed cart items so we can restore them if needed. * * @var array */ public $removed_cart_contents = array(); /** * Contains an array of coupon codes applied to the cart. * * @var array */ public $applied_coupons = array(); /** * This stores the chosen shipping methods for the cart item packages. * * @var array */ protected $shipping_methods; /** * Total defaults used to reset. * * @var array */ protected $default_totals = array( 'subtotal' => 0, 'subtotal_tax' => 0, 'shipping_total' => 0, 'shipping_tax' => 0, 'shipping_taxes' => array(), 'discount_total' => 0, 'discount_tax' => 0, 'cart_contents_total' => 0, 'cart_contents_tax' => 0, 'cart_contents_taxes' => array(), 'fee_total' => 0, 'fee_tax' => 0, 'fee_taxes' => array(), 'total' => 0, 'total_tax' => 0, ); /** * Store calculated totals. * * @var array */ protected $totals = array(); /** * Reference to the cart session handling class. * * @var WC_Cart_Session */ protected $session; /** * Reference to the cart fees API class. * * @var WC_Cart_Fees */ protected $fees_api; /** * Constructor for the cart class. Loads options and hooks in the init method. */ public function __construct() { $this->session = new WC_Cart_Session( $this ); $this->fees_api = new WC_Cart_Fees( $this ); // Register hooks for the objects. $this->session->init(); add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 ); add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 ); add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1, 2 ); } /** * When cloning, ensure object properties are handled. * * These properties store a reference to the cart, so we use new instead of clone. */ public function __clone() { $this->session = clone $this->session; $this->fees_api = clone $this->fees_api; } /* |-------------------------------------------------------------------------- | Getters. |-------------------------------------------------------------------------- | | Methods to retrieve class properties and avoid direct access. */ /** * Gets cart contents. * * @since 3.2.0 * @return array of cart items */ public function get_cart_contents() { return apply_filters( 'woocommerce_get_cart_contents', (array) $this->cart_contents ); } /** * Return items removed from the cart. * * @since 3.2.0 * @return array */ public function get_removed_cart_contents() { return (array) $this->removed_cart_contents; } /** * Gets the array of applied coupon codes. * * @return array of applied coupons */ public function get_applied_coupons() { return (array) $this->applied_coupons; } /** * Return all calculated coupon totals. * * @since 3.2.0 * @return array */ public function get_coupon_discount_totals() { return (array) $this->coupon_discount_totals; } /** * Return all calculated coupon tax totals. * * @since 3.2.0 * @return array */ public function get_coupon_discount_tax_totals() { return (array) $this->coupon_discount_tax_totals; } /** * Return all calculated totals. * * @since 3.2.0 * @return array */ public function get_totals() { return empty( $this->totals ) ? $this->default_totals : $this->totals; } /** * Get a total. * * @since 3.2.0 * @param string $key Key of element in $totals array. * @return mixed */ protected function get_totals_var( $key ) { return isset( $this->totals[ $key ] ) ? $this->totals[ $key ] : $this->default_totals[ $key ]; } /** * Get subtotal. * * @since 3.2.0 * @return float */ public function get_subtotal() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal' ) ); } /** * Get subtotal_tax. * * @since 3.2.0 * @return float */ public function get_subtotal_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal_tax' ) ); } /** * Get discount_total. * * @since 3.2.0 * @return float */ public function get_discount_total() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_total' ) ); } /** * Get discount_tax. * * @since 3.2.0 * @return float */ public function get_discount_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_tax' ) ); } /** * Get shipping_total. * * @since 3.2.0 * @return float */ public function get_shipping_total() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_total' ) ); } /** * Get shipping_tax. * * @since 3.2.0 * @return float */ public function get_shipping_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_tax' ) ); } /** * Gets cart total. This is the total of items in the cart, but after discounts. Subtotal is before discounts. * * @since 3.2.0 * @return float */ public function get_cart_contents_total() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_total' ) ); } /** * Gets cart tax amount. * * @since 3.2.0 * @return float */ public function get_cart_contents_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_tax' ) ); } /** * Gets cart total after calculation. * * @since 3.2.0 * @param string $context If the context is view, the value will be formatted for display. This keeps it compatible with pre-3.2 versions. * @return float */ public function get_total( $context = 'view' ) { $total = apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total' ) ); return 'view' === $context ? apply_filters( 'woocommerce_cart_total', wc_price( $total ) ) : $total; } /** * Get total tax amount. * * @since 3.2.0 * @return float */ public function get_total_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total_tax' ) ); } /** * Get total fee amount. * * @since 3.2.0 * @return float */ public function get_fee_total() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_total' ) ); } /** * Get total fee tax amount. * * @since 3.2.0 * @return float */ public function get_fee_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_tax' ) ); } /** * Get taxes. * * @since 3.2.0 */ public function get_shipping_taxes() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_taxes' ) ); } /** * Get taxes. * * @since 3.2.0 */ public function get_cart_contents_taxes() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_taxes' ) ); } /** * Get taxes. * * @since 3.2.0 */ public function get_fee_taxes() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_taxes' ) ); } /** * Return whether or not the cart is displaying prices including tax, rather than excluding tax. * * @since 3.3.0 * @return bool */ public function display_prices_including_tax() { return apply_filters( 'woocommerce_cart_' . __FUNCTION__, 'incl' === $this->get_tax_price_display_mode() ); } /* |-------------------------------------------------------------------------- | Setters. |-------------------------------------------------------------------------- | | Methods to set class properties and avoid direct access. */ /** * Sets the contents of the cart. * * @param array $value Cart array. */ public function set_cart_contents( $value ) { $this->cart_contents = (array) $value; } /** * Set items removed from the cart. * * @since 3.2.0 * @param array $value Item array. */ public function set_removed_cart_contents( $value = array() ) { $this->removed_cart_contents = (array) $value; } /** * Sets the array of applied coupon codes. * * @param array $value List of applied coupon codes. */ public function set_applied_coupons( $value = array() ) { $this->applied_coupons = (array) $value; } /** * Sets the array of calculated coupon totals. * * @since 3.2.0 * @param array $value Value to set. */ public function set_coupon_discount_totals( $value = array() ) { $this->coupon_discount_totals = (array) $value; } /** * Sets the array of calculated coupon tax totals. * * @since 3.2.0 * @param array $value Value to set. */ public function set_coupon_discount_tax_totals( $value = array() ) { $this->coupon_discount_tax_totals = (array) $value; } /** * Set all calculated totals. * * @since 3.2.0 * @param array $value Value to set. */ public function set_totals( $value = array() ) { $this->totals = wp_parse_args( $value, $this->default_totals ); } /** * Set subtotal. * * @since 3.2.0 * @param string $value Value to set. */ public function set_subtotal( $value ) { $this->totals['subtotal'] = wc_format_decimal( $value ); } /** * Set subtotal. * * @since 3.2.0 * @param string $value Value to set. */ public function set_subtotal_tax( $value ) { $this->totals['subtotal_tax'] = $value; } /** * Set discount_total. * * @since 3.2.0 * @param string $value Value to set. */ public function set_discount_total( $value ) { $this->totals['discount_total'] = $value; } /** * Set discount_tax. * * @since 3.2.0 * @param string $value Value to set. */ public function set_discount_tax( $value ) { $this->totals['discount_tax'] = $value; } /** * Set shipping_total. * * @since 3.2.0 * @param string $value Value to set. */ public function set_shipping_total( $value ) { $this->totals['shipping_total'] = wc_format_decimal( $value ); } /** * Set shipping_tax. * * @since 3.2.0 * @param string $value Value to set. */ public function set_shipping_tax( $value ) { $this->totals['shipping_tax'] = $value; } /** * Set cart_contents_total. * * @since 3.2.0 * @param string $value Value to set. */ public function set_cart_contents_total( $value ) { $this->totals['cart_contents_total'] = wc_format_decimal( $value ); } /** * Set cart tax amount. * * @since 3.2.0 * @param string $value Value to set. */ public function set_cart_contents_tax( $value ) { $this->totals['cart_contents_tax'] = $value; } /** * Set cart total. * * @since 3.2.0 * @param string $value Value to set. */ public function set_total( $value ) { $this->totals['total'] = wc_format_decimal( $value, wc_get_price_decimals() ); } /** * Set total tax amount. * * @since 3.2.0 * @param string $value Value to set. */ public function set_total_tax( $value ) { // We round here because this is a total entry, as opposed to line items in other setters. $this->totals['total_tax'] = wc_round_tax_total( $value ); } /** * Set fee amount. * * @since 3.2.0 * @param string $value Value to set. */ public function set_fee_total( $value ) { $this->totals['fee_total'] = wc_format_decimal( $value ); } /** * Set fee tax. * * @since 3.2.0 * @param string $value Value to set. */ public function set_fee_tax( $value ) { $this->totals['fee_tax'] = $value; } /** * Set taxes. * * @since 3.2.0 * @param array $value Tax values. */ public function set_shipping_taxes( $value ) { $this->totals['shipping_taxes'] = (array) $value; } /** * Set taxes. * * @since 3.2.0 * @param array $value Tax values. */ public function set_cart_contents_taxes( $value ) { $this->totals['cart_contents_taxes'] = (array) $value; } /** * Set taxes. * * @since 3.2.0 * @param array $value Tax values. */ public function set_fee_taxes( $value ) { $this->totals['fee_taxes'] = (array) $value; } /* |-------------------------------------------------------------------------- | Helper methods. |-------------------------------------------------------------------------- */ /** * Returns the cart and shipping taxes, merged. * * @return array merged taxes */ public function get_taxes() { return apply_filters( 'woocommerce_cart_get_taxes', wc_array_merge_recursive_numeric( $this->get_shipping_taxes(), $this->get_cart_contents_taxes(), $this->get_fee_taxes() ), $this ); } /** * Returns the contents of the cart in an array. * * @return array contents of the cart */ public function get_cart() { if ( ! did_action( 'wp_loaded' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' ); } if ( ! did_action( 'woocommerce_load_cart_from_session' ) ) { $this->session->get_cart_from_session(); } return array_filter( $this->get_cart_contents() ); } /** * Returns a specific item in the cart. * * @param string $item_key Cart item key. * @return array Item data */ public function get_cart_item( $item_key ) { return isset( $this->cart_contents[ $item_key ] ) ? $this->cart_contents[ $item_key ] : array(); } /** * Checks if the cart is empty. * * @return bool */ public function is_empty() { return 0 === count( $this->get_cart() ); } /** * Empties the cart and optionally the persistent cart too. * * @param bool $clear_persistent_cart Should the persistant cart be cleared too. Defaults to true. */ public function empty_cart( $clear_persistent_cart = true ) { do_action( 'woocommerce_before_cart_emptied', $clear_persistent_cart ); $this->cart_contents = array(); $this->removed_cart_contents = array(); $this->shipping_methods = array(); $this->coupon_discount_totals = array(); $this->coupon_discount_tax_totals = array(); $this->applied_coupons = array(); $this->totals = $this->default_totals; if ( $clear_persistent_cart ) { $this->session->persistent_cart_destroy(); } $this->fees_api->remove_all_fees(); do_action( 'woocommerce_cart_emptied', $clear_persistent_cart ); } /** * Get number of items in the cart. * * @return int */ public function get_cart_contents_count() { return apply_filters( 'woocommerce_cart_contents_count', array_sum( wp_list_pluck( $this->get_cart(), 'quantity' ) ) ); } /** * Get weight of items in the cart. * * @since 2.5.0 * @return float */ public function get_cart_contents_weight() { $weight = 0.0; foreach ( $this->get_cart() as $cart_item_key => $values ) { if ( $values['data']->has_weight() ) { $weight += (float) $values['data']->get_weight() * $values['quantity']; } } return apply_filters( 'woocommerce_cart_contents_weight', $weight ); } /** * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines. * * @return array */ public function get_cart_item_quantities() { $quantities = array(); foreach ( $this->get_cart() as $cart_item_key => $values ) { $product = $values['data']; $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $values['quantity'] : $values['quantity']; } return $quantities; } /** * Check all cart items for errors. */ public function check_cart_items() { $return = true; $result = $this->check_cart_item_validity(); if ( is_wp_error( $result ) ) { wc_add_notice( $result->get_error_message(), 'error' ); $return = false; } $result = $this->check_cart_item_stock(); if ( is_wp_error( $result ) ) { wc_add_notice( $result->get_error_message(), 'error' ); $return = false; } return $return; } /** * Check cart coupons for errors. */ public function check_cart_coupons() { foreach ( $this->get_applied_coupons() as $code ) { $coupon = new WC_Coupon( $code ); if ( ! $coupon->is_valid() ) { $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED ); $this->remove_coupon( $code ); } } } /** * Looks through cart items and checks the posts are not trashed or deleted. * * @return bool|WP_Error */ public function check_cart_item_validity() { $return = true; foreach ( $this->get_cart() as $cart_item_key => $values ) { $product = $values['data']; if ( ! $product || ! $product->exists() || 'trash' === $product->get_status() ) { $this->set_quantity( $cart_item_key, 0 ); $return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) ); } } return $return; } /** * Looks through the cart to check each item is in stock. If not, add an error. * * @return bool|WP_Error */ public function check_cart_item_stock() { $error = new WP_Error(); $product_qty_in_cart = $this->get_cart_item_quantities(); $current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0; foreach ( $this->get_cart() as $cart_item_key => $values ) { $product = $values['data']; // Check stock based on stock-status. if ( ! $product->is_in_stock() ) { /* translators: %s: product name */ $error->add( 'out-of-stock', sprintf( __( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) ); return $error; } // We only need to check products managing stock, with a limited stock qty. if ( ! $product->managing_stock() || $product->backorders_allowed() ) { continue; } // Check stock based on all items in the cart and consider any held stock within pending orders. $held_stock = wc_get_held_stock_quantity( $product, $current_session_order_id ); $required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ]; /** * Allows filter if product have enough stock to get added to the cart. * * @since 4.6.0 * @param bool $has_stock If have enough stock. * @param WC_Product $product Product instance. * @param array $values Cart item values. */ if ( apply_filters( 'woocommerce_cart_item_required_stock_is_not_enough', $product->get_stock_quantity() < ( $held_stock + $required_stock ), $product, $values ) ) { /* translators: 1: product name 2: quantity in stock */ $error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) ); return $error; } } return true; } /** * Gets and formats a list of cart item data + variations for display on the frontend. * * @param array $cart_item Cart item object. * @param bool $flat Should the data be returned flat or in a list. * @return string */ public function get_item_data( $cart_item, $flat = false ) { wc_deprecated_function( 'WC_Cart::get_item_data', '3.3', 'wc_get_formatted_cart_item_data' ); return wc_get_formatted_cart_item_data( $cart_item, $flat ); } /** * Gets cross sells based on the items in the cart. * * @return array cross_sells (item ids) */ public function get_cross_sells() { $cross_sells = array(); $in_cart = array(); if ( ! $this->is_empty() ) { foreach ( $this->get_cart() as $cart_item_key => $values ) { if ( $values['quantity'] > 0 ) { $cross_sells = array_merge( $values['data']->get_cross_sell_ids(), $cross_sells ); $in_cart[] = $values['product_id']; } } } $cross_sells = array_diff( $cross_sells, $in_cart ); return apply_filters( 'woocommerce_cart_crosssell_ids', wp_parse_id_list( $cross_sells ), $this ); } /** * Gets the url to remove an item from the cart. * * @param string $cart_item_key contains the id of the cart item. * @return string url to page */ public function get_remove_url( $cart_item_key ) { wc_deprecated_function( 'WC_Cart::get_remove_url', '3.3', 'wc_get_cart_remove_url' ); return wc_get_cart_remove_url( $cart_item_key ); } /** * Gets the url to re-add an item into the cart. * * @param string $cart_item_key Cart item key to undo. * @return string url to page */ public function get_undo_url( $cart_item_key ) { wc_deprecated_function( 'WC_Cart::get_undo_url', '3.3', 'wc_get_cart_undo_url' ); return wc_get_cart_undo_url( $cart_item_key ); } /** * Get taxes, merged by code, formatted ready for output. * * @return array */ public function get_tax_totals() { $shipping_taxes = $this->get_shipping_taxes(); // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back. $taxes = $this->get_taxes(); $tax_totals = array(); foreach ( $taxes as $key => $tax ) { $code = WC_Tax::get_rate_code( $key ); if ( $code || apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) === $key ) { if ( ! isset( $tax_totals[ $code ] ) ) { $tax_totals[ $code ] = new stdClass(); $tax_totals[ $code ]->amount = 0; } $tax_totals[ $code ]->tax_rate_id = $key; $tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key ); $tax_totals[ $code ]->label = WC_Tax::get_rate_label( $key ); if ( isset( $shipping_taxes[ $key ] ) ) { $tax -= $shipping_taxes[ $key ]; $tax = wc_round_tax_total( $tax ); $tax += NumberUtil::round( $shipping_taxes[ $key ], wc_get_price_decimals() ); unset( $shipping_taxes[ $key ] ); } $tax_totals[ $code ]->amount += wc_round_tax_total( $tax ); $tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount ); } } if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) { $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) ); $tax_totals = array_intersect_key( $tax_totals, $amounts ); } return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this ); } /** * Get all tax classes for items in the cart. * * @return array */ public function get_cart_item_tax_classes() { $found_tax_classes = array(); foreach ( WC()->cart->get_cart() as $item ) { if ( $item['data'] && ( $item['data']->is_taxable() || $item['data']->is_shipping_taxable() ) ) { $found_tax_classes[] = $item['data']->get_tax_class(); } } return array_unique( $found_tax_classes ); } /** * Get all tax classes for shipping based on the items in the cart. * * @return array */ public function get_cart_item_tax_classes_for_shipping() { $found_tax_classes = array(); foreach ( WC()->cart->get_cart() as $item ) { if ( $item['data'] && ( $item['data']->is_shipping_taxable() ) ) { $found_tax_classes[] = $item['data']->get_tax_class(); } } return array_unique( $found_tax_classes ); } /** * Determines the value that the customer spent and the subtotal * displayed, used for things like coupon validation. * * Since the coupon lines are displayed based on the TAX DISPLAY value * of cart, this is used to determine the spend. * * If cart totals are shown including tax, use the subtotal. * If cart totals are shown excluding tax, use the subtotal ex tax * (tax is shown after coupons). * * @since 2.6.0 * @return string */ public function get_displayed_subtotal() { return $this->display_prices_including_tax() ? $this->get_subtotal() + $this->get_subtotal_tax() : $this->get_subtotal(); } /** * Check if product is in the cart and return cart item key. * * Cart item key will be unique based on the item and its properties, such as variations. * * @param mixed $cart_id id of product to find in the cart. * @return string cart item key */ public function find_product_in_cart( $cart_id = false ) { if ( false !== $cart_id ) { if ( is_array( $this->cart_contents ) && isset( $this->cart_contents[ $cart_id ] ) ) { return $cart_id; } } return ''; } /** * Generate a unique ID for the cart item being added. * * @param int $product_id - id of the product the key is being generated for. * @param int $variation_id of the product the key is being generated for. * @param array $variation data for the cart item. * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart. * @return string cart item key */ public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) { $id_parts = array( $product_id ); if ( $variation_id && 0 !== $variation_id ) { $id_parts[] = $variation_id; } if ( is_array( $variation ) && ! empty( $variation ) ) { $variation_key = ''; foreach ( $variation as $key => $value ) { $variation_key .= trim( $key ) . trim( $value ); } $id_parts[] = $variation_key; } if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) { $cart_item_data_key = ''; foreach ( $cart_item_data as $key => $value ) { if ( is_array( $value ) || is_object( $value ) ) { $value = http_build_query( $value ); } $cart_item_data_key .= trim( $key ) . trim( $value ); } $id_parts[] = $cart_item_data_key; } return apply_filters( 'woocommerce_cart_id', md5( implode( '_', $id_parts ) ), $product_id, $variation_id, $variation, $cart_item_data ); } /** * Add a product to the cart. * * @throws Exception Plugins can throw an exception to prevent adding to cart. * @param int $product_id contains the id of the product to add to the cart. * @param int $quantity contains the quantity of the item to add. * @param int $variation_id ID of the variation being added to the cart. * @param array $variation attribute values. * @param array $cart_item_data extra cart item data we want to pass into the item. * @return string|bool $cart_item_key */ public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) { try { $product_id = absint( $product_id ); $variation_id = absint( $variation_id ); // Ensure we don't add a variation to the cart directly by variation ID. if ( 'product_variation' === get_post_type( $product_id ) ) { $variation_id = $product_id; $product_id = wp_get_post_parent_id( $variation_id ); } $product_data = wc_get_product( $variation_id ? $variation_id : $product_id ); $quantity = apply_filters( 'woocommerce_add_to_cart_quantity', $quantity, $product_id ); if ( $quantity <= 0 || ! $product_data || 'trash' === $product_data->get_status() ) { return false; } if ( $product_data->is_type( 'variation' ) ) { $missing_attributes = array(); $parent_data = wc_get_product( $product_data->get_parent_id() ); $variation_attributes = $product_data->get_variation_attributes(); // Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart. $variation_attributes = array_filter( $variation_attributes ); // Gather posted attributes. $posted_attributes = array(); foreach ( $parent_data->get_attributes() as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } $attribute_key = 'attribute_' . sanitize_title( $attribute['name'] ); if ( isset( $variation[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $attribute['is_taxonomy'] ) { // Don't use wc_clean as it destroys sanitized characters. $value = sanitize_title( wp_unslash( $variation[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } else { $value = html_entity_decode( wc_clean( wp_unslash( $variation[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } // Don't include if it's empty. if ( ! empty( $value ) || '0' === $value ) { $posted_attributes[ $attribute_key ] = $value; } } } // Merge variation attributes and posted attributes. $posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes ); // If no variation ID is set, attempt to get a variation ID from posted attributes. if ( empty( $variation_id ) ) { $data_store = WC_Data_Store::load( 'product' ); $variation_id = $data_store->find_matching_product_variation( $parent_data, $posted_attributes ); } // Do we have a variation ID? if ( empty( $variation_id ) ) { throw new Exception( __( 'Please choose product options…', 'woocommerce' ) ); } // Check the data we have is valid. $variation_data = wc_get_product_variation_attributes( $variation_id ); $attributes = array(); foreach ( $parent_data->get_attributes() as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } // Get valid value from variation data. $attribute_key = 'attribute_' . sanitize_title( $attribute['name'] ); $valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : ''; /** * If the attribute value was posted, check if it's valid. * * If no attribute was posted, only error if the variation has an 'any' attribute which requires a value. */ if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) { $value = $posted_and_variation_attributes[ $attribute_key ]; // Allow if valid or show error. if ( $valid_value === $value ) { $attributes[ $attribute_key ] = $value; } elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) { // If valid values are empty, this is an 'any' variation so get all possible values. $attributes[ $attribute_key ] = $value; } else { /* translators: %s: Attribute name. */ throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) ); } } elseif ( '' === $valid_value ) { $missing_attributes[] = wc_attribute_label( $attribute['name'] ); } $variation = $attributes; } if ( ! empty( $missing_attributes ) ) { /* translators: %s: Attribute name. */ throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) ); } } // Validate variation ID. if ( 0 < $variation_id && // Only check if there's any variation_id. ( ! $product_data->is_type( 'variation' ) || // Check if isn't a variation, it suppose to be a variation at this point. $product_data->get_parent_id() !== $product_id // Check if belongs to the selected variable product. ) ) { $product = wc_get_product( $product_id ); /* translators: 1: product link, 2: product name */ throw new Exception( sprintf( __( 'The selected product isn\'t a variation of %2$s, please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( $product->get_permalink() ), esc_html( $product->get_name() ) ) ); } // Load cart item data - may be added by other plugins. $cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity ); // Generate a ID based on product ID, variation ID, variation data, and other cart item data. $cart_id = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data ); // Find the cart item key in the existing cart. $cart_item_key = $this->find_product_in_cart( $cart_id ); // Force quantity to 1 if sold individually and check for existing item in cart. if ( $product_data->is_sold_individually() ) { $quantity = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data ); $found_in_cart = apply_filters( 'woocommerce_add_to_cart_sold_individually_found_in_cart', $cart_item_key && $this->cart_contents[ $cart_item_key ]['quantity'] > 0, $product_id, $variation_id, $cart_item_data, $cart_id ); if ( $found_in_cart ) { /* translators: %s: product name */ $message = sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_name() ); /** * Filters message about more than 1 product being added to cart. * * @since 4.5.0 * @param string $message Message. * @param WC_Product $product_data Product data. */ $message = apply_filters( 'woocommerce_cart_product_cannot_add_another_message', $message, $product_data ); throw new Exception( sprintf( '<a href="%s" class="button wc-forward">%s</a> %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), $message ) ); } } if ( ! $product_data->is_purchasable() ) { $message = __( 'Sorry, this product cannot be purchased.', 'woocommerce' ); /** * Filters message about product unable to be purchased. * * @since 3.8.0 * @param string $message Message. * @param WC_Product $product_data Product data. */ $message = apply_filters( 'woocommerce_cart_product_cannot_be_purchased_message', $message, $product_data ); throw new Exception( $message ); } // Stock check - only check if we're managing stock and backorders are not allowed. if ( ! $product_data->is_in_stock() ) { /* translators: %s: product name */ $message = sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_name() ); /** * Filters message about product being out of stock. * * @since 4.5.0 * @param string $message Message. * @param WC_Product $product_data Product data. */ $message = apply_filters( 'woocommerce_cart_product_out_of_stock_message', $message, $product_data ); throw new Exception( $message ); } if ( ! $product_data->has_enough_stock( $quantity ) ) { $stock_quantity = $product_data->get_stock_quantity(); /* translators: 1: product name 2: quantity in stock */ $message = sprintf( __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product_data->get_name(), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ) ); /** * Filters message about product not having enough stock. * * @since 4.5.0 * @param string $message Message. * @param WC_Product $product_data Product data. * @param int $stock_quantity Quantity remaining. */ $message = apply_filters( 'woocommerce_cart_product_not_enough_stock_message', $message, $product_data, $stock_quantity ); throw new Exception( $message ); } // Stock check - this time accounting for whats already in-cart. if ( $product_data->managing_stock() ) { $products_qty_in_cart = $this->get_cart_item_quantities(); if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) { $stock_quantity = $product_data->get_stock_quantity(); $stock_quantity_in_cart = $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ]; $message = sprintf( '<a href="%s" class="button wc-forward">%s</a> %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), /* translators: 1: quantity in stock 2: current quantity */ sprintf( __( 'You cannot add that amount to the cart — we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) ) ); /** * Filters message about product not having enough stock accounting for what's already in the cart. * * @param string $message Message. * @param WC_Product $product_data Product data. * @param int $stock_quantity Quantity remaining. * @param int $stock_quantity_in_cart * * @since 5.3.0 */ $message = apply_filters( 'woocommerce_cart_product_not_enough_stock_already_in_cart_message', $message, $product_data, $stock_quantity, $stock_quantity_in_cart ); throw new Exception( $message ); } } // If cart_item_key is set, the item is already in the cart. if ( $cart_item_key ) { $new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity']; $this->set_quantity( $cart_item_key, $new_quantity, false ); } else { $cart_item_key = $cart_id; // Add item after merging with $cart_item_data - hook to allow plugins to modify cart item. $this->cart_contents[ $cart_item_key ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array( 'key' => $cart_item_key, 'product_id' => $product_id, 'variation_id' => $variation_id, 'variation' => $variation, 'quantity' => $quantity, 'data' => $product_data, 'data_hash' => wc_get_cart_item_data_hash( $product_data ), ) ), $cart_item_key ); } $this->cart_contents = apply_filters( 'woocommerce_cart_contents_changed', $this->cart_contents ); do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ); return $cart_item_key; } catch ( Exception $e ) { if ( $e->getMessage() ) { wc_add_notice( $e->getMessage(), 'error' ); } return false; } } /** * Remove a cart item. * * @since 2.3.0 * @param string $cart_item_key Cart item key to remove from the cart. * @return bool */ public function remove_cart_item( $cart_item_key ) { if ( isset( $this->cart_contents[ $cart_item_key ] ) ) { $this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ]; unset( $this->removed_cart_contents[ $cart_item_key ]['data'] ); do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this ); unset( $this->cart_contents[ $cart_item_key ] ); do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this ); return true; } return false; } /** * Restore a cart item. * * @param string $cart_item_key Cart item key to restore to the cart. * @return bool */ public function restore_cart_item( $cart_item_key ) { if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) { $restore_item = $this->removed_cart_contents[ $cart_item_key ]; $this->cart_contents[ $cart_item_key ] = $restore_item; $this->cart_contents[ $cart_item_key ]['data'] = wc_get_product( $restore_item['variation_id'] ? $restore_item['variation_id'] : $restore_item['product_id'] ); do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this ); unset( $this->removed_cart_contents[ $cart_item_key ] ); do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this ); return true; } return false; } /** * Set the quantity for an item in the cart using it's key. * * @param string $cart_item_key contains the id of the cart item. * @param int $quantity contains the quantity of the item. * @param bool $refresh_totals whether or not to calculate totals after setting the new qty. Can be used to defer calculations if setting quantities in bulk. * @return bool */ public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) { if ( 0 === $quantity || $quantity < 0 ) { wc_do_deprecated_action( 'woocommerce_before_cart_item_quantity_zero', array( $cart_item_key, $this ), '3.7.0', 'woocommerce_remove_cart_item' ); // If we're setting qty to 0 we're removing the item from the cart. return $this->remove_cart_item( $cart_item_key ); } // Update qty. $old_quantity = $this->cart_contents[ $cart_item_key ]['quantity']; $this->cart_contents[ $cart_item_key ]['quantity'] = $quantity; do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity, $this ); if ( $refresh_totals ) { $this->calculate_totals(); } /** * Fired after qty has been changed. * * @since 3.6.0 * @param string $cart_item_key contains the id of the cart item. This may be empty if the cart item does not exist any more. * @param int $quantity contains the quantity of the item. * @param WC_Cart $this Cart class. */ do_action( 'woocommerce_cart_item_set_quantity', $cart_item_key, $quantity, $this ); return true; } /** * Get cart's owner. * * @since 3.2.0 * @return WC_Customer */ public function get_customer() { return WC()->customer; } /** * Calculate totals for the items in the cart. * * @uses WC_Cart_Totals */ public function calculate_totals() { $this->reset_totals(); if ( $this->is_empty() ) { $this->session->set_session(); return; } do_action( 'woocommerce_before_calculate_totals', $this ); new WC_Cart_Totals( $this ); do_action( 'woocommerce_after_calculate_totals', $this ); } /** * Looks at the totals to see if payment is actually required. * * @return bool */ public function needs_payment() { return apply_filters( 'woocommerce_cart_needs_payment', 0 < $this->get_total( 'edit' ), $this ); } /* * Shipping related functions. */ /** * Uses the shipping class to calculate shipping then gets the totals when its finished. */ public function calculate_shipping() { $this->shipping_methods = $this->needs_shipping() ? $this->get_chosen_shipping_methods( WC()->shipping()->calculate_shipping( $this->get_shipping_packages() ) ) : array(); $shipping_taxes = wp_list_pluck( $this->shipping_methods, 'taxes' ); $merged_taxes = array(); foreach ( $shipping_taxes as $taxes ) { foreach ( $taxes as $tax_id => $tax_amount ) { if ( ! isset( $merged_taxes[ $tax_id ] ) ) { $merged_taxes[ $tax_id ] = 0; } $merged_taxes[ $tax_id ] += $tax_amount; } } $this->set_shipping_total( array_sum( wp_list_pluck( $this->shipping_methods, 'cost' ) ) ); $this->set_shipping_tax( array_sum( $merged_taxes ) ); $this->set_shipping_taxes( $merged_taxes ); return $this->shipping_methods; } /** * Given a set of packages with rates, get the chosen ones only. * * @since 3.2.0 * @param array $calculated_shipping_packages Array of packages. * @return array */ protected function get_chosen_shipping_methods( $calculated_shipping_packages = array() ) { $chosen_methods = array(); // Get chosen methods for each package to get our totals. foreach ( $calculated_shipping_packages as $key => $package ) { $chosen_method = wc_get_chosen_shipping_method_for_package( $key, $package ); if ( $chosen_method ) { $chosen_methods[ $key ] = $package['rates'][ $chosen_method ]; } } return $chosen_methods; } /** * Filter items needing shipping callback. * * @since 3.0.0 * @param array $item Item to check for shipping. * @return bool */ protected function filter_items_needing_shipping( $item ) { $product = $item['data']; return $product && $product->needs_shipping(); } /** * Get only items that need shipping. * * @since 3.0.0 * @return array */ protected function get_items_needing_shipping() { return array_filter( $this->get_cart(), array( $this, 'filter_items_needing_shipping' ) ); } /** * Get packages to calculate shipping for. * * This lets us calculate costs for carts that are shipped to multiple locations. * * Shipping methods are responsible for looping through these packages. * * By default we pass the cart itself as a package - plugins can change this. * through the filter and break it up. * * @since 1.5.4 * @return array of cart items */ public function get_shipping_packages() { return apply_filters( 'woocommerce_cart_shipping_packages', array( array( 'contents' => $this->get_items_needing_shipping(), 'contents_cost' => array_sum( wp_list_pluck( $this->get_items_needing_shipping(), 'line_total' ) ), 'applied_coupons' => $this->get_applied_coupons(), 'user' => array( 'ID' => get_current_user_id(), ), 'destination' => array( 'country' => $this->get_customer()->get_shipping_country(), 'state' => $this->get_customer()->get_shipping_state(), 'postcode' => $this->get_customer()->get_shipping_postcode(), 'city' => $this->get_customer()->get_shipping_city(), 'address' => $this->get_customer()->get_shipping_address(), 'address_1' => $this->get_customer()->get_shipping_address(), // Provide both address and address_1 for backwards compatibility. 'address_2' => $this->get_customer()->get_shipping_address_2(), ), 'cart_subtotal' => $this->get_displayed_subtotal(), ), ) ); } /** * Looks through the cart to see if shipping is actually required. * * @return bool whether or not the cart needs shipping */ public function needs_shipping() { if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) { return false; } $needs_shipping = false; foreach ( $this->get_cart_contents() as $cart_item_key => $values ) { if ( $values['data']->needs_shipping() ) { $needs_shipping = true; break; } } return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); } /** * Should the shipping address form be shown. * * @return bool */ public function needs_shipping_address() { return apply_filters( 'woocommerce_cart_needs_shipping_address', true === $this->needs_shipping() && ! wc_ship_to_billing_address_only() ); } /** * Sees if the customer has entered enough data to calc the shipping yet. * * @return bool */ public function show_shipping() { if ( ! wc_shipping_enabled() || ! $this->get_cart_contents() ) { return false; } if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) { $country = $this->get_customer()->get_shipping_country(); if ( ! $country ) { return false; } $country_fields = WC()->countries->get_address_fields( $country, 'shipping_' ); if ( isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'] && ! $this->get_customer()->get_shipping_state() ) { return false; } if ( isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'] && ! $this->get_customer()->get_shipping_postcode() ) { return false; } } return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true ); } /** * Gets the shipping total (after calculation). * * @return string price or string for the shipping total */ public function get_cart_shipping_total() { // Default total assumes Free shipping. $total = __( 'Free!', 'woocommerce' ); if ( 0 < $this->get_shipping_total() ) { if ( $this->display_prices_including_tax() ) { $total = wc_price( $this->shipping_total + $this->shipping_tax_total ); if ( $this->shipping_tax_total > 0 && ! wc_prices_include_tax() ) { $total .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>'; } } else { $total = wc_price( $this->shipping_total ); if ( $this->shipping_tax_total > 0 && wc_prices_include_tax() ) { $total .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } } } return apply_filters( 'woocommerce_cart_shipping_total', $total, $this ); } /** * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error. * * Checks two types of coupons: * 1. Where a list of customer emails are set (limits coupon usage to those defined). * 2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email). * * @param array $posted Post data. */ public function check_customer_coupons( $posted ) { foreach ( $this->get_applied_coupons() as $code ) { $coupon = new WC_Coupon( $code ); if ( $coupon->is_valid() ) { // Get user and posted emails to compare. $current_user = wp_get_current_user(); $billing_email = isset( $posted['billing_email'] ) ? $posted['billing_email'] : ''; $check_emails = array_unique( array_filter( array_map( 'strtolower', array_map( 'sanitize_email', array( $billing_email, $current_user->user_email, ) ) ) ) ); // Limit to defined email addresses. $restrictions = $coupon->get_email_restrictions(); if ( is_array( $restrictions ) && 0 < count( $restrictions ) && ! $this->is_coupon_emails_allowed( $check_emails, $restrictions ) ) { $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ); $this->remove_coupon( $code ); } $coupon_usage_limit = $coupon->get_usage_limit_per_user(); if ( 0 < $coupon_usage_limit && 0 === get_current_user_id() ) { // For guest, usage per user has not been enforced yet. Enforce it now. $coupon_data_store = $coupon->get_data_store(); $billing_email = strtolower( sanitize_email( $billing_email ) ); if ( $coupon_data_store && $coupon_data_store->get_usage_by_email( $coupon, $billing_email ) >= $coupon_usage_limit ) { if ( $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $billing_email ) ) ) { $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST ); } else { $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); } } } } } } /** * Checks if the given email address(es) matches the ones specified on the coupon. * * @param array $check_emails Array of customer email addresses. * @param array $restrictions Array of allowed email addresses. * @return bool */ public function is_coupon_emails_allowed( $check_emails, $restrictions ) { foreach ( $check_emails as $check_email ) { // With a direct match we return true. if ( in_array( $check_email, $restrictions, true ) ) { return true; } // Go through the allowed emails and return true if the email matches a wildcard. foreach ( $restrictions as $restriction ) { // Convert to PHP-regex syntax. $regex = '/^' . str_replace( '*', '(.+)?', $restriction ) . '$/'; preg_match( $regex, $check_email, $match ); if ( ! empty( $match ) ) { return true; } } } // No matches, this one isn't allowed. return false; } /** * Returns whether or not a discount has been applied. * * @param string $coupon_code Coupon code to check. * @return bool */ public function has_discount( $coupon_code = '' ) { return $coupon_code ? in_array( wc_format_coupon_code( $coupon_code ), $this->applied_coupons, true ) : count( $this->applied_coupons ) > 0; } /** * Applies a coupon code passed to the method. * * @param string $coupon_code - The code to apply. * @return bool True if the coupon is applied, false if it does not exist or cannot be applied. */ public function apply_coupon( $coupon_code ) { // Coupons are globally disabled. if ( ! wc_coupons_enabled() ) { return false; } // Sanitize coupon code. $coupon_code = wc_format_coupon_code( $coupon_code ); // Get the coupon. $the_coupon = new WC_Coupon( $coupon_code ); // Prevent adding coupons by post ID. if ( $the_coupon->get_code() !== $coupon_code ) { $the_coupon->set_code( $coupon_code ); $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_EXIST ); return false; } // Check it can be used with cart. if ( ! $the_coupon->is_valid() ) { wc_add_notice( $the_coupon->get_error_message(), 'error' ); return false; } // Check if applied. if ( $this->has_discount( $coupon_code ) ) { $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED ); return false; } // If its individual use then remove other coupons. if ( $the_coupon->get_individual_use() ) { $coupons_to_keep = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons ); foreach ( $this->applied_coupons as $applied_coupon ) { $keep_key = array_search( $applied_coupon, $coupons_to_keep, true ); if ( false === $keep_key ) { $this->remove_coupon( $applied_coupon ); } else { unset( $coupons_to_keep[ $keep_key ] ); } } if ( ! empty( $coupons_to_keep ) ) { $this->applied_coupons += $coupons_to_keep; } } // Check to see if an individual use coupon is set. if ( $this->applied_coupons ) { foreach ( $this->applied_coupons as $code ) { $coupon = new WC_Coupon( $code ); if ( $coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) { // Reject new coupon. $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY ); return false; } } } $this->applied_coupons[] = $coupon_code; // Choose free shipping. if ( $the_coupon->get_free_shipping() ) { $packages = WC()->shipping()->get_packages(); $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); foreach ( $packages as $i => $package ) { $chosen_shipping_methods[ $i ] = 'free_shipping'; } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); } $the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS ); do_action( 'woocommerce_applied_coupon', $coupon_code ); return true; } /** * Get array of applied coupon objects and codes. * * @param null $deprecated No longer used. * @return array of applied coupons */ public function get_coupons( $deprecated = null ) { $coupons = array(); if ( 'order' === $deprecated ) { return $coupons; } foreach ( $this->get_applied_coupons() as $code ) { $coupon = new WC_Coupon( $code ); $coupons[ $code ] = $coupon; } return $coupons; } /** * Get the discount amount for a used coupon. * * @param string $code coupon code. * @param bool $ex_tax inc or ex tax. * @return float discount amount */ public function get_coupon_discount_amount( $code, $ex_tax = true ) { $totals = $this->get_coupon_discount_totals(); $discount_amount = isset( $totals[ $code ] ) ? $totals[ $code ] : 0; if ( ! $ex_tax ) { $discount_amount += $this->get_coupon_discount_tax_amount( $code ); } return wc_cart_round_discount( $discount_amount, wc_get_price_decimals() ); } /** * Get the discount tax amount for a used coupon (for tax inclusive prices). * * @param string $code coupon code. * @return float discount amount */ public function get_coupon_discount_tax_amount( $code ) { $totals = $this->get_coupon_discount_tax_totals(); return wc_cart_round_discount( isset( $totals[ $code ] ) ? $totals[ $code ] : 0, wc_get_price_decimals() ); } /** * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax. * * @param null $deprecated No longer used. */ public function remove_coupons( $deprecated = null ) { $this->set_coupon_discount_totals( array() ); $this->set_coupon_discount_tax_totals( array() ); $this->set_applied_coupons( array() ); $this->session->set_session(); } /** * Remove a single coupon by code. * * @param string $coupon_code Code of the coupon to remove. * @return bool */ public function remove_coupon( $coupon_code ) { $coupon_code = wc_format_coupon_code( $coupon_code ); $position = array_search( $coupon_code, array_map( 'wc_format_coupon_code', $this->get_applied_coupons() ), true ); if ( false !== $position ) { unset( $this->applied_coupons[ $position ] ); } WC()->session->set( 'refresh_totals', true ); do_action( 'woocommerce_removed_coupon', $coupon_code ); return true; } /** * Trigger an action so 3rd parties can add custom fees. * * @since 2.0.0 */ public function calculate_fees() { do_action( 'woocommerce_cart_calculate_fees', $this ); } /** * Return reference to fees API. * * @since 3.2.0 * @return WC_Cart_Fees */ public function fees_api() { return $this->fees_api; } /** * Add additional fee to the cart. * * This method should be called on a callback attached to the * woocommerce_cart_calculate_fees action during cart/checkout. Fees do not * persist. * * @uses WC_Cart_Fees::add_fee * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added. * @param float $amount Fee amount (do not enter negative amounts). * @param bool $taxable Is the fee taxable? (default: false). * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: ''). */ public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) { $this->fees_api()->add_fee( array( 'name' => $name, 'amount' => (float) $amount, 'taxable' => $taxable, 'tax_class' => $tax_class, ) ); } /** * Return all added fees from the Fees API. * * @uses WC_Cart_Fees::get_fees * @return array */ public function get_fees() { $fees = $this->fees_api()->get_fees(); if ( property_exists( $this, 'fees' ) ) { $fees = $fees + (array) $this->fees; } return $fees; } /** * Gets the total excluding taxes. * * @return string formatted price */ public function get_total_ex_tax() { return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( max( 0, $this->get_total( 'edit' ) - $this->get_total_tax() ) ) ); } /** * Gets the cart contents total (after calculation). * * @return string formatted price */ public function get_cart_total() { return apply_filters( 'woocommerce_cart_contents_total', wc_price( wc_prices_include_tax() ? $this->get_cart_contents_total() + $this->get_cart_contents_tax() : $this->get_cart_contents_total() ) ); } /** * Gets the sub total (after calculation). * * @param bool $compound whether to include compound taxes. * @return string formatted price */ public function get_cart_subtotal( $compound = false ) { /** * If the cart has compound tax, we want to show the subtotal as cart + shipping + non-compound taxes (after discount). */ if ( $compound ) { $cart_subtotal = wc_price( $this->get_cart_contents_total() + $this->get_shipping_total() + $this->get_taxes_total( false, false ) ); } elseif ( $this->display_prices_including_tax() ) { $cart_subtotal = wc_price( $this->get_subtotal() + $this->get_subtotal_tax() ); if ( $this->get_subtotal_tax() > 0 && ! wc_prices_include_tax() ) { $cart_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>'; } } else { $cart_subtotal = wc_price( $this->get_subtotal() ); if ( $this->get_subtotal_tax() > 0 && wc_prices_include_tax() ) { $cart_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } } return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this ); } /** * Get the product row price per item. * * @param WC_Product $product Product object. * @return string formatted price */ public function get_product_price( $product ) { if ( $this->display_prices_including_tax() ) { $product_price = wc_get_price_including_tax( $product ); } else { $product_price = wc_get_price_excluding_tax( $product ); } return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $product ); } /** * Get the product row subtotal. * * Gets the tax etc to avoid rounding issues. * * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate. * * @param WC_Product $product Product object. * @param int $quantity Quantity being purchased. * @return string formatted price */ public function get_product_subtotal( $product, $quantity ) { $price = $product->get_price(); if ( $product->is_taxable() ) { if ( $this->display_prices_including_tax() ) { $row_price = wc_get_price_including_tax( $product, array( 'qty' => $quantity ) ); $product_subtotal = wc_price( $row_price ); if ( ! wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) { $product_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>'; } } else { $row_price = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); $product_subtotal = wc_price( $row_price ); if ( wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) { $product_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } } } else { $row_price = $price * $quantity; $product_subtotal = wc_price( $row_price ); } return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $product, $quantity, $this ); } /** * Gets the cart tax (after calculation). * * @return string formatted price */ public function get_cart_tax() { $cart_total_tax = wc_round_tax_total( $this->get_cart_contents_tax() + $this->get_shipping_tax() + $this->get_fee_tax() ); return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' ); } /** * Get a tax amount. * * @param string $tax_rate_id ID of the tax rate to get taxes for. * @return float amount */ public function get_tax_amount( $tax_rate_id ) { $taxes = wc_array_merge_recursive_numeric( $this->get_cart_contents_taxes(), $this->get_fee_taxes() ); return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0; } /** * Get a tax amount. * * @param string $tax_rate_id ID of the tax rate to get taxes for. * @return float amount */ public function get_shipping_tax_amount( $tax_rate_id ) { $taxes = $this->get_shipping_taxes(); return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0; } /** * Get tax row amounts with or without compound taxes includes. * * @param bool $compound True if getting compound taxes. * @param bool $display True if getting total to display. * @return float price */ public function get_taxes_total( $compound = true, $display = true ) { $total = 0; $taxes = $this->get_taxes(); foreach ( $taxes as $key => $tax ) { if ( ! $compound && WC_Tax::is_compound( $key ) ) { continue; } $total += $tax; } if ( $display ) { $total = wc_format_decimal( $total, wc_get_price_decimals() ); } return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this ); } /** * Gets the total discount amount. * * @return mixed formatted price or false if there are none */ public function get_total_discount() { return apply_filters( 'woocommerce_cart_total_discount', $this->get_discount_total() ? wc_price( $this->get_discount_total() ) : false, $this ); } /** * Reset cart totals to the defaults. Useful before running calculations. */ private function reset_totals() { $this->totals = $this->default_totals; $this->fees_api->remove_all_fees(); do_action( 'woocommerce_cart_reset', $this, false ); } /** * Returns 'incl' if tax should be included in cart, otherwise returns 'excl'. * * @return string */ public function get_tax_price_display_mode() { if ( $this->get_customer() && $this->get_customer()->get_is_vat_exempt() ) { return 'excl'; } return get_option( 'woocommerce_tax_display_cart' ); } /** * Returns the hash based on cart contents. * * @since 3.6.0 * @return string hash for cart content */ public function get_cart_hash() { $cart_session = $this->session->get_cart_for_session(); $hash = $cart_session ? md5( wp_json_encode( $cart_session ) . $this->get_total( 'edit' ) ) : ''; $hash = apply_filters_deprecated( 'woocommerce_add_to_cart_hash', array( $hash, $cart_session ), '3.6.0', 'woocommerce_cart_hash' ); return apply_filters( 'woocommerce_cart_hash', $hash, $cart_session ); } } includes/abstracts/abstract-wc-object-query.php 0000644 00000003703 15132754524 0015705 0 ustar 00 <?php /** * Query abstraction layer functionality. * * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Abstract WC Object Query Class * * Extended by classes to provide a query abstraction layer for safe object searching. * * @version 3.1.0 * @package WooCommerce\Abstracts */ abstract class WC_Object_Query { /** * Stores query data. * * @var array */ protected $query_vars = array(); /** * Create a new query. * * @param array $args Criteria to query on in a format similar to WP_Query. */ public function __construct( $args = array() ) { $this->query_vars = wp_parse_args( $args, $this->get_default_query_vars() ); } /** * Get the current query vars. * * @return array */ public function get_query_vars() { return $this->query_vars; } /** * Get the value of a query variable. * * @param string $query_var Query variable to get value for. * @param mixed $default Default value if query variable is not set. * @return mixed Query variable value if set, otherwise default. */ public function get( $query_var, $default = '' ) { if ( isset( $this->query_vars[ $query_var ] ) ) { return $this->query_vars[ $query_var ]; } return $default; } /** * Set a query variable. * * @param string $query_var Query variable to set. * @param mixed $value Value to set for query variable. */ public function set( $query_var, $value ) { $this->query_vars[ $query_var ] = $value; } /** * Get the default allowed query vars. * * @return array */ protected function get_default_query_vars() { return array( 'name' => '', 'parent' => '', 'parent_exclude' => '', 'exclude' => '', 'limit' => get_option( 'posts_per_page' ), 'page' => 1, 'offset' => '', 'paginate' => false, 'order' => 'DESC', 'orderby' => 'date', 'return' => 'objects', ); } } includes/abstracts/abstract-wc-payment-token.php 0000644 00000013263 15132754524 0016071 0 ustar 00 <?php /** * Abstract payment tokens * * Generic payment tokens functionality which can be extended by individual types of payment tokens. * * @class WC_Payment_Token * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-payment-token.php'; /** * WooCommerce Payment Token. * * Representation of a general payment token to be extended by individuals types of tokens * examples: Credit Card, eCheck. * * @class WC_Payment_Token * @version 3.0.0 * @since 2.6.0 * @package WooCommerce\Abstracts */ abstract class WC_Payment_Token extends WC_Legacy_Payment_Token { /** * Token Data (stored in the payment_tokens table). * * @var array */ protected $data = array( 'gateway_id' => '', 'token' => '', 'is_default' => false, 'user_id' => 0, 'type' => '', ); /** * Token Type (CC, eCheck, or a custom type added by an extension). * Set by child classes. * * @var string */ protected $type = ''; /** * Initialize a payment token. * * These fields are accepted by all payment tokens: * is_default - boolean Optional - Indicates this is the default payment token for a user * token - string Required - The actual token to store * gateway_id - string Required - Identifier for the gateway this token is associated with * user_id - int Optional - ID for the user this token is associated with. 0 if this token is not associated with a user * * @since 2.6.0 * @param mixed $token Token. */ public function __construct( $token = '' ) { parent::__construct( $token ); if ( is_numeric( $token ) ) { $this->set_id( $token ); } elseif ( is_object( $token ) ) { $token_id = $token->get_id(); if ( ! empty( $token_id ) ) { $this->set_id( $token->get_id() ); } } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'payment-token' ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /* *-------------------------------------------------------------------------- * Getters *-------------------------------------------------------------------------- */ /** * Returns the raw payment token. * * @since 2.6.0 * @param string $context Context in which to call this. * @return string Raw token */ public function get_token( $context = 'view' ) { return $this->get_prop( 'token', $context ); } /** * Returns the type of this payment token (CC, eCheck, or something else). * Overwritten by child classes. * * @since 2.6.0 * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string Payment Token Type (CC, eCheck) */ public function get_type( $deprecated = '' ) { return $this->type; } /** * Get type to display to user. * Get's overwritten by child classes. * * @since 2.6.0 * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string */ public function get_display_name( $deprecated = '' ) { return $this->get_type(); } /** * Returns the user ID associated with the token or false if this token is not associated. * * @since 2.6.0 * @param string $context In what context to execute this. * @return int User ID if this token is associated with a user or 0 if no user is associated */ public function get_user_id( $context = 'view' ) { return $this->get_prop( 'user_id', $context ); } /** * Returns the ID of the gateway associated with this payment token. * * @since 2.6.0 * @param string $context In what context to execute this. * @return string Gateway ID */ public function get_gateway_id( $context = 'view' ) { return $this->get_prop( 'gateway_id', $context ); } /** * Returns the ID of the gateway associated with this payment token. * * @since 2.6.0 * @param string $context In what context to execute this. * @return string Gateway ID */ public function get_is_default( $context = 'view' ) { return $this->get_prop( 'is_default', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set the raw payment token. * * @since 2.6.0 * @param string $token Payment token. */ public function set_token( $token ) { $this->set_prop( 'token', $token ); } /** * Set the user ID for the user associated with this order. * * @since 2.6.0 * @param int $user_id User ID. */ public function set_user_id( $user_id ) { $this->set_prop( 'user_id', absint( $user_id ) ); } /** * Set the gateway ID. * * @since 2.6.0 * @param string $gateway_id Gateway ID. */ public function set_gateway_id( $gateway_id ) { $this->set_prop( 'gateway_id', $gateway_id ); } /** * Marks the payment as default or non-default. * * @since 2.6.0 * @param boolean $is_default True or false. */ public function set_default( $is_default ) { $this->set_prop( 'is_default', (bool) $is_default ); } /* |-------------------------------------------------------------------------- | Other Methods |-------------------------------------------------------------------------- */ /** * Returns if the token is marked as default. * * @since 2.6.0 * @return boolean True if the token is default */ public function is_default() { return (bool) $this->get_prop( 'is_default', 'view' ); } /** * Validate basic token info (token and type are required). * * @since 2.6.0 * @return boolean True if the passed data is valid */ public function validate() { $token = $this->get_prop( 'token', 'edit' ); if ( empty( $token ) ) { return false; } return true; } } includes/abstracts/abstract-wc-log-handler.php 0000644 00000002612 15132754524 0015466 0 ustar 00 <?php /** * Log handling functionality. * * @class WC_Log_Handler * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Abstract WC Log Handler Class * * @version 1.0.0 * @package WooCommerce\Abstracts */ abstract class WC_Log_Handler implements WC_Log_Handler_Interface { /** * Formats a timestamp for use in log messages. * * @param int $timestamp Log timestamp. * @return string Formatted time for use in log entry. */ protected static function format_time( $timestamp ) { return date( 'c', $timestamp ); } /** * Builds a log entry text from level, timestamp and message. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. * * @return string Formatted log entry. */ protected static function format_entry( $timestamp, $level, $message, $context ) { $time_string = self::format_time( $timestamp ); $level_string = strtoupper( $level ); $entry = "{$time_string} {$level_string} {$message}"; return apply_filters( 'woocommerce_format_log_entry', $entry, array( 'timestamp' => $timestamp, 'level' => $level, 'message' => $message, 'context' => $context, ) ); } } includes/abstracts/abstract-wc-product.php 0000644 00000161046 15132754524 0014761 0 ustar 00 <?php /** * WooCommerce product base class. * * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore; /** * Legacy product contains all deprecated methods for this class and can be * removed in the future. */ require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-product.php'; /** * Abstract Product Class * * The WooCommerce product class handles individual product data. * * @version 3.0.0 * @package WooCommerce\Abstracts */ class WC_Product extends WC_Abstract_Legacy_Product { /** * This is the name of this object type. * * @var string */ protected $object_type = 'product'; /** * Post type. * * @var string */ protected $post_type = 'product'; /** * Cache group. * * @var string */ protected $cache_group = 'products'; /** * Stores product data. * * @var array */ protected $data = array( 'name' => '', 'slug' => '', 'date_created' => null, 'date_modified' => null, 'status' => false, 'featured' => false, 'catalog_visibility' => 'visible', 'description' => '', 'short_description' => '', 'sku' => '', 'price' => '', 'regular_price' => '', 'sale_price' => '', 'date_on_sale_from' => null, 'date_on_sale_to' => null, 'total_sales' => '0', 'tax_status' => 'taxable', 'tax_class' => '', 'manage_stock' => false, 'stock_quantity' => null, 'stock_status' => 'instock', 'backorders' => 'no', 'low_stock_amount' => '', 'sold_individually' => false, 'weight' => '', 'length' => '', 'width' => '', 'height' => '', 'upsell_ids' => array(), 'cross_sell_ids' => array(), 'parent_id' => 0, 'reviews_allowed' => true, 'purchase_note' => '', 'attributes' => array(), 'default_attributes' => array(), 'menu_order' => 0, 'post_password' => '', 'virtual' => false, 'downloadable' => false, 'category_ids' => array(), 'tag_ids' => array(), 'shipping_class_id' => 0, 'downloads' => array(), 'image_id' => '', 'gallery_image_ids' => array(), 'download_limit' => -1, 'download_expiry' => -1, 'rating_counts' => array(), 'average_rating' => 0, 'review_count' => 0, ); /** * Supported features such as 'ajax_add_to_cart'. * * @var array */ protected $supports = array(); /** * Get the product if ID is passed, otherwise the product is new and empty. * This class should NOT be instantiated, but the wc_get_product() function * should be used. It is possible, but the wc_get_product() is preferred. * * @param int|WC_Product|object $product Product to init. */ public function __construct( $product = 0 ) { parent::__construct( $product ); if ( is_numeric( $product ) && $product > 0 ) { $this->set_id( $product ); } elseif ( $product instanceof self ) { $this->set_id( absint( $product->get_id() ) ); } elseif ( ! empty( $product->ID ) ) { $this->set_id( absint( $product->ID ) ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'product-' . $this->get_type() ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /** * Get internal type. Should return string and *should be overridden* by child classes. * * The product_type property is deprecated but is used here for BW compatibility with child classes which may be defining product_type and not have a get_type method. * * @since 3.0.0 * @return string */ public function get_type() { return isset( $this->product_type ) ? $this->product_type : 'simple'; } /** * Get product name. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_name( $context = 'view' ) { return $this->get_prop( 'name', $context ); } /** * Get product slug. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_slug( $context = 'view' ) { return $this->get_prop( 'slug', $context ); } /** * Get product created date. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { return $this->get_prop( 'date_created', $context ); } /** * Get product modified date. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { return $this->get_prop( 'date_modified', $context ); } /** * Get product status. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_status( $context = 'view' ) { return $this->get_prop( 'status', $context ); } /** * If the product is featured. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return boolean */ public function get_featured( $context = 'view' ) { return $this->get_prop( 'featured', $context ); } /** * Get catalog visibility. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_catalog_visibility( $context = 'view' ) { return $this->get_prop( 'catalog_visibility', $context ); } /** * Get product description. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_description( $context = 'view' ) { return $this->get_prop( 'description', $context ); } /** * Get product short description. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_short_description( $context = 'view' ) { return $this->get_prop( 'short_description', $context ); } /** * Get SKU (Stock-keeping unit) - product unique ID. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_sku( $context = 'view' ) { return $this->get_prop( 'sku', $context ); } /** * Returns the product's active price. * * @param string $context What the value is for. Valid values are view and edit. * @return string price */ public function get_price( $context = 'view' ) { return $this->get_prop( 'price', $context ); } /** * Returns the product's regular price. * * @param string $context What the value is for. Valid values are view and edit. * @return string price */ public function get_regular_price( $context = 'view' ) { return $this->get_prop( 'regular_price', $context ); } /** * Returns the product's sale price. * * @param string $context What the value is for. Valid values are view and edit. * @return string price */ public function get_sale_price( $context = 'view' ) { return $this->get_prop( 'sale_price', $context ); } /** * Get date on sale from. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_on_sale_from( $context = 'view' ) { return $this->get_prop( 'date_on_sale_from', $context ); } /** * Get date on sale to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_on_sale_to( $context = 'view' ) { return $this->get_prop( 'date_on_sale_to', $context ); } /** * Get number total of sales. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_total_sales( $context = 'view' ) { return $this->get_prop( 'total_sales', $context ); } /** * Returns the tax status. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_tax_status( $context = 'view' ) { return $this->get_prop( 'tax_status', $context ); } /** * Returns the tax class. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_tax_class( $context = 'view' ) { return $this->get_prop( 'tax_class', $context ); } /** * Return if product manage stock. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return boolean */ public function get_manage_stock( $context = 'view' ) { return $this->get_prop( 'manage_stock', $context ); } /** * Returns number of items available for sale. * * @param string $context What the value is for. Valid values are view and edit. * @return int|null */ public function get_stock_quantity( $context = 'view' ) { return $this->get_prop( 'stock_quantity', $context ); } /** * Return the stock status. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return string */ public function get_stock_status( $context = 'view' ) { return $this->get_prop( 'stock_status', $context ); } /** * Get backorders. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return string yes no or notify */ public function get_backorders( $context = 'view' ) { return $this->get_prop( 'backorders', $context ); } /** * Get low stock amount. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.5.0 * @return int|string Returns empty string if value not set */ public function get_low_stock_amount( $context = 'view' ) { return $this->get_prop( 'low_stock_amount', $context ); } /** * Return if should be sold individually. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return boolean */ public function get_sold_individually( $context = 'view' ) { return $this->get_prop( 'sold_individually', $context ); } /** * Returns the product's weight. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_weight( $context = 'view' ) { return $this->get_prop( 'weight', $context ); } /** * Returns the product length. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_length( $context = 'view' ) { return $this->get_prop( 'length', $context ); } /** * Returns the product width. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_width( $context = 'view' ) { return $this->get_prop( 'width', $context ); } /** * Returns the product height. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_height( $context = 'view' ) { return $this->get_prop( 'height', $context ); } /** * Returns formatted dimensions. * * @param bool $formatted True by default for legacy support - will be false/not set in future versions to return the array only. Use wc_format_dimensions for formatted versions instead. * @return string|array */ public function get_dimensions( $formatted = true ) { if ( $formatted ) { wc_deprecated_argument( 'WC_Product::get_dimensions', '3.0', 'By default, get_dimensions has an argument set to true so that HTML is returned. This is to support the legacy version of the method. To get HTML dimensions, instead use wc_format_dimensions() function. Pass false to this method to return an array of dimensions. This will be the new default behavior in future versions.' ); return apply_filters( 'woocommerce_product_dimensions', wc_format_dimensions( $this->get_dimensions( false ) ), $this ); } return array( 'length' => $this->get_length(), 'width' => $this->get_width(), 'height' => $this->get_height(), ); } /** * Get upsell IDs. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_upsell_ids( $context = 'view' ) { return $this->get_prop( 'upsell_ids', $context ); } /** * Get cross sell IDs. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_cross_sell_ids( $context = 'view' ) { return $this->get_prop( 'cross_sell_ids', $context ); } /** * Get parent ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_parent_id( $context = 'view' ) { return $this->get_prop( 'parent_id', $context ); } /** * Return if reviews is allowed. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_reviews_allowed( $context = 'view' ) { return $this->get_prop( 'reviews_allowed', $context ); } /** * Get purchase note. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_purchase_note( $context = 'view' ) { return $this->get_prop( 'purchase_note', $context ); } /** * Returns product attributes. * * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_attributes( $context = 'view' ) { return $this->get_prop( 'attributes', $context ); } /** * Get default attributes. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_default_attributes( $context = 'view' ) { return $this->get_prop( 'default_attributes', $context ); } /** * Get menu order. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_menu_order( $context = 'view' ) { return $this->get_prop( 'menu_order', $context ); } /** * Get post password. * * @since 3.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_post_password( $context = 'view' ) { return $this->get_prop( 'post_password', $context ); } /** * Get category ids. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_category_ids( $context = 'view' ) { return $this->get_prop( 'category_ids', $context ); } /** * Get tag ids. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_tag_ids( $context = 'view' ) { return $this->get_prop( 'tag_ids', $context ); } /** * Get virtual. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_virtual( $context = 'view' ) { return $this->get_prop( 'virtual', $context ); } /** * Returns the gallery attachment ids. * * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_gallery_image_ids( $context = 'view' ) { return $this->get_prop( 'gallery_image_ids', $context ); } /** * Get shipping class ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_shipping_class_id( $context = 'view' ) { return $this->get_prop( 'shipping_class_id', $context ); } /** * Get downloads. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_downloads( $context = 'view' ) { return $this->get_prop( 'downloads', $context ); } /** * Get download expiry. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_download_expiry( $context = 'view' ) { return $this->get_prop( 'download_expiry', $context ); } /** * Get downloadable. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_downloadable( $context = 'view' ) { return $this->get_prop( 'downloadable', $context ); } /** * Get download limit. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_download_limit( $context = 'view' ) { return $this->get_prop( 'download_limit', $context ); } /** * Get main image ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_image_id( $context = 'view' ) { return $this->get_prop( 'image_id', $context ); } /** * Get rating count. * * @param string $context What the value is for. Valid values are view and edit. * @return array of counts */ public function get_rating_counts( $context = 'view' ) { return $this->get_prop( 'rating_counts', $context ); } /** * Get average rating. * * @param string $context What the value is for. Valid values are view and edit. * @return float */ public function get_average_rating( $context = 'view' ) { return $this->get_prop( 'average_rating', $context ); } /** * Get review count. * * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_review_count( $context = 'view' ) { return $this->get_prop( 'review_count', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting product data. These should not update anything in the | database itself and should only change what is stored in the class | object. */ /** * Set product name. * * @since 3.0.0 * @param string $name Product name. */ public function set_name( $name ) { $this->set_prop( 'name', $name ); } /** * Set product slug. * * @since 3.0.0 * @param string $slug Product slug. */ public function set_slug( $slug ) { $this->set_prop( 'slug', $slug ); } /** * Set product created date. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set product modified date. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set product status. * * @since 3.0.0 * @param string $status Product status. */ public function set_status( $status ) { $this->set_prop( 'status', $status ); } /** * Set if the product is featured. * * @since 3.0.0 * @param bool|string $featured Whether the product is featured or not. */ public function set_featured( $featured ) { $this->set_prop( 'featured', wc_string_to_bool( $featured ) ); } /** * Set catalog visibility. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $visibility Options: 'hidden', 'visible', 'search' and 'catalog'. */ public function set_catalog_visibility( $visibility ) { $options = array_keys( wc_get_product_visibility_options() ); if ( ! in_array( $visibility, $options, true ) ) { $this->error( 'product_invalid_catalog_visibility', __( 'Invalid catalog visibility option.', 'woocommerce' ) ); } $this->set_prop( 'catalog_visibility', $visibility ); } /** * Set product description. * * @since 3.0.0 * @param string $description Product description. */ public function set_description( $description ) { $this->set_prop( 'description', $description ); } /** * Set product short description. * * @since 3.0.0 * @param string $short_description Product short description. */ public function set_short_description( $short_description ) { $this->set_prop( 'short_description', $short_description ); } /** * Set SKU. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $sku Product SKU. */ public function set_sku( $sku ) { $sku = (string) $sku; if ( $this->get_object_read() && ! empty( $sku ) && ! wc_product_has_unique_sku( $this->get_id(), $sku ) ) { $sku_found = wc_get_product_id_by_sku( $sku ); $this->error( 'product_invalid_sku', __( 'Invalid or duplicated SKU.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) ); } $this->set_prop( 'sku', $sku ); } /** * Set the product's active price. * * @param string $price Price. */ public function set_price( $price ) { $this->set_prop( 'price', wc_format_decimal( $price ) ); } /** * Set the product's regular price. * * @since 3.0.0 * @param string $price Regular price. */ public function set_regular_price( $price ) { $this->set_prop( 'regular_price', wc_format_decimal( $price ) ); } /** * Set the product's sale price. * * @since 3.0.0 * @param string $price sale price. */ public function set_sale_price( $price ) { $this->set_prop( 'sale_price', wc_format_decimal( $price ) ); } /** * Set date on sale from. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_on_sale_from( $date = null ) { $this->set_date_prop( 'date_on_sale_from', $date ); } /** * Set date on sale to. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_on_sale_to( $date = null ) { $this->set_date_prop( 'date_on_sale_to', $date ); } /** * Set number total of sales. * * @since 3.0.0 * @param int $total Total of sales. */ public function set_total_sales( $total ) { $this->set_prop( 'total_sales', absint( $total ) ); } /** * Set the tax status. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $status Tax status. */ public function set_tax_status( $status ) { $options = array( 'taxable', 'shipping', 'none', ); // Set default if empty. if ( empty( $status ) ) { $status = 'taxable'; } if ( ! in_array( $status, $options, true ) ) { $this->error( 'product_invalid_tax_status', __( 'Invalid product tax status.', 'woocommerce' ) ); } $this->set_prop( 'tax_status', $status ); } /** * Set the tax class. * * @since 3.0.0 * @param string $class Tax class. */ public function set_tax_class( $class ) { $class = sanitize_title( $class ); $class = 'standard' === $class ? '' : $class; $valid_classes = $this->get_valid_tax_classes(); if ( ! in_array( $class, $valid_classes, true ) ) { $class = ''; } $this->set_prop( 'tax_class', $class ); } /** * Return an array of valid tax classes * * @return array valid tax classes */ protected function get_valid_tax_classes() { return WC_Tax::get_tax_class_slugs(); } /** * Set if product manage stock. * * @since 3.0.0 * @param bool $manage_stock Whether or not manage stock is enabled. */ public function set_manage_stock( $manage_stock ) { $this->set_prop( 'manage_stock', wc_string_to_bool( $manage_stock ) ); } /** * Set number of items available for sale. * * @since 3.0.0 * @param float|null $quantity Stock quantity. */ public function set_stock_quantity( $quantity ) { $this->set_prop( 'stock_quantity', '' !== $quantity ? wc_stock_amount( $quantity ) : null ); } /** * Set stock status. * * @param string $status New status. */ public function set_stock_status( $status = 'instock' ) { $valid_statuses = wc_get_product_stock_status_options(); if ( isset( $valid_statuses[ $status ] ) ) { $this->set_prop( 'stock_status', $status ); } else { $this->set_prop( 'stock_status', 'instock' ); } } /** * Set backorders. * * @since 3.0.0 * @param string $backorders Options: 'yes', 'no' or 'notify'. */ public function set_backorders( $backorders ) { $this->set_prop( 'backorders', $backorders ); } /** * Set low stock amount. * * @param int|string $amount Empty string if value not set. * @since 3.5.0 */ public function set_low_stock_amount( $amount ) { $this->set_prop( 'low_stock_amount', '' === $amount ? '' : absint( $amount ) ); } /** * Set if should be sold individually. * * @since 3.0.0 * @param bool $sold_individually Whether or not product is sold individually. */ public function set_sold_individually( $sold_individually ) { $this->set_prop( 'sold_individually', wc_string_to_bool( $sold_individually ) ); } /** * Set the product's weight. * * @since 3.0.0 * @param float|string $weight Total weight. */ public function set_weight( $weight ) { $this->set_prop( 'weight', '' === $weight ? '' : wc_format_decimal( $weight ) ); } /** * Set the product length. * * @since 3.0.0 * @param float|string $length Total length. */ public function set_length( $length ) { $this->set_prop( 'length', '' === $length ? '' : wc_format_decimal( $length ) ); } /** * Set the product width. * * @since 3.0.0 * @param float|string $width Total width. */ public function set_width( $width ) { $this->set_prop( 'width', '' === $width ? '' : wc_format_decimal( $width ) ); } /** * Set the product height. * * @since 3.0.0 * @param float|string $height Total height. */ public function set_height( $height ) { $this->set_prop( 'height', '' === $height ? '' : wc_format_decimal( $height ) ); } /** * Set upsell IDs. * * @since 3.0.0 * @param array $upsell_ids IDs from the up-sell products. */ public function set_upsell_ids( $upsell_ids ) { $this->set_prop( 'upsell_ids', array_filter( (array) $upsell_ids ) ); } /** * Set crosssell IDs. * * @since 3.0.0 * @param array $cross_sell_ids IDs from the cross-sell products. */ public function set_cross_sell_ids( $cross_sell_ids ) { $this->set_prop( 'cross_sell_ids', array_filter( (array) $cross_sell_ids ) ); } /** * Set parent ID. * * @since 3.0.0 * @param int $parent_id Product parent ID. */ public function set_parent_id( $parent_id ) { $this->set_prop( 'parent_id', absint( $parent_id ) ); } /** * Set if reviews is allowed. * * @since 3.0.0 * @param bool $reviews_allowed Reviews allowed or not. */ public function set_reviews_allowed( $reviews_allowed ) { $this->set_prop( 'reviews_allowed', wc_string_to_bool( $reviews_allowed ) ); } /** * Set purchase note. * * @since 3.0.0 * @param string $purchase_note Purchase note. */ public function set_purchase_note( $purchase_note ) { $this->set_prop( 'purchase_note', $purchase_note ); } /** * Set product attributes. * * Attributes are made up of: * id - 0 for product level attributes. ID for global attributes. * name - Attribute name. * options - attribute value or array of term ids/names. * position - integer sort order. * visible - If visible on frontend. * variation - If used for variations. * Indexed by unqiue key to allow clearing old ones after a set. * * @since 3.0.0 * @param array $raw_attributes Array of WC_Product_Attribute objects. */ public function set_attributes( $raw_attributes ) { $attributes = array_fill_keys( array_keys( $this->get_attributes( 'edit' ) ), null ); foreach ( $raw_attributes as $attribute ) { if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { $attributes[ sanitize_title( $attribute->get_name() ) ] = $attribute; } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); $this->set_prop( 'attributes', $attributes ); } /** * Set default attributes. These will be saved as strings and should map to attribute values. * * @since 3.0.0 * @param array $default_attributes List of default attributes. */ public function set_default_attributes( $default_attributes ) { $this->set_prop( 'default_attributes', array_map( 'strval', array_filter( (array) $default_attributes, 'wc_array_filter_default_attributes' ) ) ); } /** * Set menu order. * * @since 3.0.0 * @param int $menu_order Menu order. */ public function set_menu_order( $menu_order ) { $this->set_prop( 'menu_order', intval( $menu_order ) ); } /** * Set post password. * * @since 3.6.0 * @param int $post_password Post password. */ public function set_post_password( $post_password ) { $this->set_prop( 'post_password', $post_password ); } /** * Set the product categories. * * @since 3.0.0 * @param array $term_ids List of terms IDs. */ public function set_category_ids( $term_ids ) { $this->set_prop( 'category_ids', array_unique( array_map( 'intval', $term_ids ) ) ); } /** * Set the product tags. * * @since 3.0.0 * @param array $term_ids List of terms IDs. */ public function set_tag_ids( $term_ids ) { $this->set_prop( 'tag_ids', array_unique( array_map( 'intval', $term_ids ) ) ); } /** * Set if the product is virtual. * * @since 3.0.0 * @param bool|string $virtual Whether product is virtual or not. */ public function set_virtual( $virtual ) { $this->set_prop( 'virtual', wc_string_to_bool( $virtual ) ); } /** * Set shipping class ID. * * @since 3.0.0 * @param int $id Product shipping class id. */ public function set_shipping_class_id( $id ) { $this->set_prop( 'shipping_class_id', absint( $id ) ); } /** * Set if the product is downloadable. * * @since 3.0.0 * @param bool|string $downloadable Whether product is downloadable or not. */ public function set_downloadable( $downloadable ) { $this->set_prop( 'downloadable', wc_string_to_bool( $downloadable ) ); } /** * Set downloads. * * @since 3.0.0 * @param array $downloads_array Array of WC_Product_Download objects or arrays. */ public function set_downloads( $downloads_array ) { $downloads = array(); $errors = array(); foreach ( $downloads_array as $download ) { if ( is_a( $download, 'WC_Product_Download' ) ) { $download_object = $download; } else { $download_object = new WC_Product_Download(); // If we don't have a previous hash, generate UUID for download. if ( empty( $download['download_id'] ) ) { $download['download_id'] = wp_generate_uuid4(); } $download_object->set_id( $download['download_id'] ); $download_object->set_name( $download['name'] ); $download_object->set_file( $download['file'] ); } // Validate the file extension. if ( ! $download_object->is_allowed_filetype() ) { if ( $this->get_object_read() ) { /* translators: %1$s: Downloadable file */ $errors[] = sprintf( __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), '<code>' . basename( $download_object->get_file() ) . '</code>', '<code>' . implode( ', ', array_keys( $download_object->get_allowed_mime_types() ) ) . '</code>' ); } continue; } // Validate the file exists. if ( ! $download_object->file_exists() ) { if ( $this->get_object_read() ) { /* translators: %s: Downloadable file */ $errors[] = sprintf( __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), '<code>' . $download_object->get_file() . '</code>' ); } continue; } $downloads[ $download_object->get_id() ] = $download_object; } if ( $errors ) { $this->error( 'product_invalid_download', $errors[0] ); } $this->set_prop( 'downloads', $downloads ); } /** * Set download limit. * * @since 3.0.0 * @param int|string $download_limit Product download limit. */ public function set_download_limit( $download_limit ) { $this->set_prop( 'download_limit', -1 === (int) $download_limit || '' === $download_limit ? -1 : absint( $download_limit ) ); } /** * Set download expiry. * * @since 3.0.0 * @param int|string $download_expiry Product download expiry. */ public function set_download_expiry( $download_expiry ) { $this->set_prop( 'download_expiry', -1 === (int) $download_expiry || '' === $download_expiry ? -1 : absint( $download_expiry ) ); } /** * Set gallery attachment ids. * * @since 3.0.0 * @param array $image_ids List of image ids. */ public function set_gallery_image_ids( $image_ids ) { $image_ids = wp_parse_id_list( $image_ids ); $this->set_prop( 'gallery_image_ids', $image_ids ); } /** * Set main image ID. * * @since 3.0.0 * @param int|string $image_id Product image id. */ public function set_image_id( $image_id = '' ) { $this->set_prop( 'image_id', $image_id ); } /** * Set rating counts. Read only. * * @param array $counts Product rating counts. */ public function set_rating_counts( $counts ) { $this->set_prop( 'rating_counts', array_filter( array_map( 'absint', (array) $counts ) ) ); } /** * Set average rating. Read only. * * @param float $average Product average rating. */ public function set_average_rating( $average ) { $this->set_prop( 'average_rating', wc_format_decimal( $average ) ); } /** * Set review count. Read only. * * @param int $count Product review count. */ public function set_review_count( $count ) { $this->set_prop( 'review_count', absint( $count ) ); } /* |-------------------------------------------------------------------------- | Other Methods |-------------------------------------------------------------------------- */ /** * Ensure properties are set correctly before save. * * @since 3.0.0 */ public function validate_props() { // Before updating, ensure stock props are all aligned. Qty, backorders and low stock amount are not needed if not stock managed. if ( ! $this->get_manage_stock() ) { $this->set_stock_quantity( '' ); $this->set_backorders( 'no' ); $this->set_low_stock_amount( '' ); return; } $stock_is_above_notification_threshold = ( $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); $backorders_are_allowed = ( 'no' !== $this->get_backorders() ); if ( $stock_is_above_notification_threshold ) { $new_stock_status = 'instock'; } elseif ( $backorders_are_allowed ) { $new_stock_status = 'onbackorder'; } else { $new_stock_status = 'outofstock'; } $this->set_stock_status( $new_stock_status ); } /** * Save data (either create or update depending on if we are working on an existing product). * * @since 3.0.0 * @return int */ public function save() { $this->validate_props(); if ( ! $this->data_store ) { return $this->get_id(); } /** * Trigger action before saving to the DB. Allows you to adjust object props before save. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); $state = $this->before_data_store_save_or_update(); if ( $this->get_id() ) { $changeset = $this->get_changes(); $this->data_store->update( $this ); } else { $changeset = null; $this->data_store->create( $this ); } $this->after_data_store_save_or_update( $state ); // Update attributes lookup table if the product is new OR it's not but there are actually any changes. if ( is_null( $changeset ) || ! empty( $changeset ) ) { wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $this, $changeset ); } /** * Trigger action after saving to the DB. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); return $this->get_id(); } /** * Do any extra processing needed before the actual product save * (but after triggering the 'woocommerce_before_..._object_save' action) * * @return mixed A state value that will be passed to after_data_store_save_or_update. */ protected function before_data_store_save_or_update() { } /** * Do any extra processing needed after the actual product save * (but before triggering the 'woocommerce_after_..._object_save' action) * * @param mixed $state The state object that was returned by before_data_store_save_or_update. */ protected function after_data_store_save_or_update( $state ) { $this->maybe_defer_product_sync(); } /** * Delete the product, set its ID to 0, and return result. * * @param bool $force_delete Should the product be deleted permanently. * @return bool result */ public function delete( $force_delete = false ) { $product_id = $this->get_id(); $deleted = parent::delete( $force_delete ); if ( $deleted ) { $this->maybe_defer_product_sync(); wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $product_id ); } return $deleted; } /** * If this is a child product, queue its parent for syncing at the end of the request. */ protected function maybe_defer_product_sync() { $parent_id = $this->get_parent_id(); if ( $parent_id ) { wc_deferred_product_sync( $parent_id ); } } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- */ /** * Check if a product supports a given feature. * * Product classes should override this to declare support (or lack of support) for a feature. * * @param string $feature string The name of a feature to test support for. * @return bool True if the product supports the feature, false otherwise. * @since 2.5.0 */ public function supports( $feature ) { return apply_filters( 'woocommerce_product_supports', in_array( $feature, $this->supports, true ), $feature, $this ); } /** * Returns whether or not the product post exists. * * @return bool */ public function exists() { return false !== $this->get_status(); } /** * Checks the product type. * * Backwards compatibility with downloadable/virtual. * * @param string|array $type Array or string of types. * @return bool */ public function is_type( $type ) { return ( $this->get_type() === $type || ( is_array( $type ) && in_array( $this->get_type(), $type, true ) ) ); } /** * Checks if a product is downloadable. * * @return bool */ public function is_downloadable() { return apply_filters( 'woocommerce_is_downloadable', true === $this->get_downloadable(), $this ); } /** * Checks if a product is virtual (has no shipping). * * @return bool */ public function is_virtual() { return apply_filters( 'woocommerce_is_virtual', true === $this->get_virtual(), $this ); } /** * Returns whether or not the product is featured. * * @return bool */ public function is_featured() { return true === $this->get_featured(); } /** * Check if a product is sold individually (no quantities). * * @return bool */ public function is_sold_individually() { return apply_filters( 'woocommerce_is_sold_individually', true === $this->get_sold_individually(), $this ); } /** * Returns whether or not the product is visible in the catalog. * * @return bool */ public function is_visible() { $visible = $this->is_visible_core(); return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() ); } /** * Returns whether or not the product is visible in the catalog (doesn't trigger filters). * * @return bool */ protected function is_visible_core() { $visible = 'visible' === $this->get_catalog_visibility() || ( is_search() && 'search' === $this->get_catalog_visibility() ) || ( ! is_search() && 'catalog' === $this->get_catalog_visibility() ); if ( 'trash' === $this->get_status() ) { $visible = false; } elseif ( 'publish' !== $this->get_status() && ! current_user_can( 'edit_post', $this->get_id() ) ) { $visible = false; } if ( $this->get_parent_id() ) { $parent_product = wc_get_product( $this->get_parent_id() ); if ( $parent_product && 'publish' !== $parent_product->get_status() ) { $visible = false; } } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) { $visible = false; } return $visible; } /** * Returns false if the product cannot be bought. * * @return bool */ public function is_purchasable() { return apply_filters( 'woocommerce_is_purchasable', $this->exists() && ( 'publish' === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) ) && '' !== $this->get_price(), $this ); } /** * Returns whether or not the product is on sale. * * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function is_on_sale( $context = 'view' ) { if ( '' !== (string) $this->get_sale_price( $context ) && $this->get_regular_price( $context ) > $this->get_sale_price( $context ) ) { $on_sale = true; if ( $this->get_date_on_sale_from( $context ) && $this->get_date_on_sale_from( $context )->getTimestamp() > time() ) { $on_sale = false; } if ( $this->get_date_on_sale_to( $context ) && $this->get_date_on_sale_to( $context )->getTimestamp() < time() ) { $on_sale = false; } } else { $on_sale = false; } return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; } /** * Returns whether or not the product has dimensions set. * * @return bool */ public function has_dimensions() { return ( $this->get_length() || $this->get_height() || $this->get_width() ) && ! $this->get_virtual(); } /** * Returns whether or not the product has weight set. * * @return bool */ public function has_weight() { return $this->get_weight() && ! $this->get_virtual(); } /** * Returns whether or not the product can be purchased. * This returns true for 'instock' and 'onbackorder' stock statuses. * * @return bool */ public function is_in_stock() { return apply_filters( 'woocommerce_product_is_in_stock', 'outofstock' !== $this->get_stock_status(), $this ); } /** * Checks if a product needs shipping. * * @return bool */ public function needs_shipping() { return apply_filters( 'woocommerce_product_needs_shipping', ! $this->is_virtual(), $this ); } /** * Returns whether or not the product is taxable. * * @return bool */ public function is_taxable() { return apply_filters( 'woocommerce_product_is_taxable', $this->get_tax_status() === 'taxable' && wc_tax_enabled(), $this ); } /** * Returns whether or not the product shipping is taxable. * * @return bool */ public function is_shipping_taxable() { return $this->needs_shipping() && ( $this->get_tax_status() === 'taxable' || $this->get_tax_status() === 'shipping' ); } /** * Returns whether or not the product is stock managed. * * @return bool */ public function managing_stock() { if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { return $this->get_manage_stock(); } return false; } /** * Returns whether or not the product can be backordered. * * @return bool */ public function backorders_allowed() { return apply_filters( 'woocommerce_product_backorders_allowed', ( 'yes' === $this->get_backorders() || 'notify' === $this->get_backorders() ), $this->get_id(), $this ); } /** * Returns whether or not the product needs to notify the customer on backorder. * * @return bool */ public function backorders_require_notification() { return apply_filters( 'woocommerce_product_backorders_require_notification', ( $this->managing_stock() && 'notify' === $this->get_backorders() ), $this ); } /** * Check if a product is on backorder. * * @param int $qty_in_cart (default: 0). * @return bool */ public function is_on_backorder( $qty_in_cart = 0 ) { if ( 'onbackorder' === $this->get_stock_status() ) { return true; } return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_stock_quantity() - $qty_in_cart ) < 0; } /** * Returns whether or not the product has enough stock for the order. * * @param mixed $quantity Quantity of a product added to an order. * @return bool */ public function has_enough_stock( $quantity ) { return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity; } /** * Returns whether or not the product has any visible attributes. * * @return boolean */ public function has_attributes() { foreach ( $this->get_attributes() as $attribute ) { if ( $attribute->get_visible() ) { return true; } } return false; } /** * Returns whether or not the product has any child product. * * @return bool */ public function has_child() { return 0 < count( $this->get_children() ); } /** * Does a child have dimensions? * * @since 3.0.0 * @return bool */ public function child_has_dimensions() { return false; } /** * Does a child have a weight? * * @since 3.0.0 * @return boolean */ public function child_has_weight() { return false; } /** * Check if downloadable product has a file attached. * * @since 1.6.2 * * @param string $download_id file identifier. * @return bool Whether downloadable product has a file attached. */ public function has_file( $download_id = '' ) { return $this->is_downloadable() && $this->get_file( $download_id ); } /** * Returns whether or not the product has additional options that need * selecting before adding to cart. * * @since 3.0.0 * @return boolean */ public function has_options() { return apply_filters( 'woocommerce_product_has_options', false, $this ); } /* |-------------------------------------------------------------------------- | Non-CRUD Getters |-------------------------------------------------------------------------- */ /** * Get the product's title. For products this is the product name. * * @return string */ public function get_title() { return apply_filters( 'woocommerce_product_title', $this->get_name(), $this ); } /** * Product permalink. * * @return string */ public function get_permalink() { return get_permalink( $this->get_id() ); } /** * Returns the children IDs if applicable. Overridden by child classes. * * @return array of IDs */ public function get_children() { return array(); } /** * If the stock level comes from another product ID, this should be modified. * * @since 3.0.0 * @return int */ public function get_stock_managed_by_id() { return $this->get_id(); } /** * Returns the price in html format. * * @param string $deprecated Deprecated param. * * @return string */ public function get_price_html( $deprecated = '' ) { if ( '' === $this->get_price() ) { $price = apply_filters( 'woocommerce_empty_price_html', '', $this ); } elseif ( $this->is_on_sale() ) { $price = wc_format_sale_price( wc_get_price_to_display( $this, array( 'price' => $this->get_regular_price() ) ), wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); } else { $price = wc_price( wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } /** * Get product name with SKU or ID. Used within admin. * * @return string Formatted product name */ public function get_formatted_name() { if ( $this->get_sku() ) { $identifier = $this->get_sku(); } else { $identifier = '#' . $this->get_id(); } return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ); } /** * Get min quantity which can be purchased at once. * * @since 3.0.0 * @return int */ public function get_min_purchase_quantity() { return 1; } /** * Get max quantity which can be purchased at once. * * @since 3.0.0 * @return int Quantity or -1 if unlimited. */ public function get_max_purchase_quantity() { return $this->is_sold_individually() ? 1 : ( $this->backorders_allowed() || ! $this->managing_stock() ? -1 : $this->get_stock_quantity() ); } /** * Get the add to url used mainly in loops. * * @return string */ public function add_to_cart_url() { return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_permalink(), $this ); } /** * Get the add to cart button text for the single page. * * @return string */ public function single_add_to_cart_text() { return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this ); } /** * Get the add to cart button text. * * @return string */ public function add_to_cart_text() { return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Read more about “%s”', 'woocommerce' ), $this->get_name() ), $this ); } /** * Returns the main product image. * * @param string $size (default: 'woocommerce_thumbnail'). * @param array $attr Image attributes. * @param bool $placeholder True to return $placeholder if no image is found, or false to return an empty string. * @return string */ public function get_image( $size = 'woocommerce_thumbnail', $attr = array(), $placeholder = true ) { $image = ''; if ( $this->get_image_id() ) { $image = wp_get_attachment_image( $this->get_image_id(), $size, false, $attr ); } elseif ( $this->get_parent_id() ) { $parent_product = wc_get_product( $this->get_parent_id() ); if ( $parent_product ) { $image = $parent_product->get_image( $size, $attr, $placeholder ); } } if ( ! $image && $placeholder ) { $image = wc_placeholder_img( $size, $attr ); } return apply_filters( 'woocommerce_product_get_image', $image, $this, $size, $attr, $placeholder, $image ); } /** * Returns the product shipping class SLUG. * * @return string */ public function get_shipping_class() { $class_id = $this->get_shipping_class_id(); if ( $class_id ) { $term = get_term_by( 'id', $class_id, 'product_shipping_class' ); if ( $term && ! is_wp_error( $term ) ) { return $term->slug; } } return ''; } /** * Returns a single product attribute as a string. * * @param string $attribute to get. * @return string */ public function get_attribute( $attribute ) { $attributes = $this->get_attributes(); $attribute = sanitize_title( $attribute ); if ( isset( $attributes[ $attribute ] ) ) { $attribute_object = $attributes[ $attribute ]; } elseif ( isset( $attributes[ 'pa_' . $attribute ] ) ) { $attribute_object = $attributes[ 'pa_' . $attribute ]; } else { return ''; } return $attribute_object->is_taxonomy() ? implode( ', ', wc_get_product_terms( $this->get_id(), $attribute_object->get_name(), array( 'fields' => 'names' ) ) ) : wc_implode_text_attributes( $attribute_object->get_options() ); } /** * Get the total amount (COUNT) of ratings, or just the count for one rating e.g. number of 5 star ratings. * * @param int $value Optional. Rating value to get the count for. By default returns the count of all rating values. * @return int */ public function get_rating_count( $value = null ) { $counts = $this->get_rating_counts(); if ( is_null( $value ) ) { return array_sum( $counts ); } elseif ( isset( $counts[ $value ] ) ) { return absint( $counts[ $value ] ); } else { return 0; } } /** * Get a file by $download_id. * * @param string $download_id file identifier. * @return array|false if not found */ public function get_file( $download_id = '' ) { $files = $this->get_downloads(); if ( '' === $download_id ) { $file = count( $files ) ? current( $files ) : false; } elseif ( isset( $files[ $download_id ] ) ) { $file = $files[ $download_id ]; } else { $file = false; } return apply_filters( 'woocommerce_product_file', $file, $this, $download_id ); } /** * Get file download path identified by $download_id. * * @param string $download_id file identifier. * @return string */ public function get_file_download_path( $download_id ) { $files = $this->get_downloads(); $file_path = isset( $files[ $download_id ] ) ? $files[ $download_id ]->get_file() : ''; // allow overriding based on the particular file being requested. return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id ); } /** * Get the suffix to display after prices > 0. * * @param string $price to calculate, left blank to just use get_price(). * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax(). * @return string */ public function get_price_suffix( $price = '', $qty = 1 ) { $html = ''; $suffix = get_option( 'woocommerce_price_display_suffix' ); if ( $suffix && wc_tax_enabled() && 'taxable' === $this->get_tax_status() ) { if ( '' === $price ) { $price = $this->get_price(); } $replacements = array( '{price_including_tax}' => wc_price( wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine, WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound '{price_excluding_tax}' => wc_price( wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound ); $html = str_replace( array_keys( $replacements ), array_values( $replacements ), ' <small class="woocommerce-price-suffix">' . wp_kses_post( $suffix ) . '</small>' ); } return apply_filters( 'woocommerce_get_price_suffix', $html, $this, $price, $qty ); } /** * Returns the availability of the product. * * @return string[] */ public function get_availability() { return apply_filters( 'woocommerce_get_availability', array( 'availability' => $this->get_availability_text(), 'class' => $this->get_availability_class(), ), $this ); } /** * Get availability text based on stock status. * * @return string */ protected function get_availability_text() { if ( ! $this->is_in_stock() ) { $availability = __( 'Out of stock', 'woocommerce' ); } elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) { $availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : ''; } elseif ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) { $availability = __( 'Available on backorder', 'woocommerce' ); } elseif ( $this->managing_stock() ) { $availability = wc_format_stock_for_display( $this ); } else { $availability = ''; } return apply_filters( 'woocommerce_get_availability_text', $availability, $this ); } /** * Get availability classname based on stock status. * * @return string */ protected function get_availability_class() { if ( ! $this->is_in_stock() ) { $class = 'out-of-stock'; } elseif ( ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) || ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) ) { $class = 'available-on-backorder'; } else { $class = 'in-stock'; } return apply_filters( 'woocommerce_get_availability_class', $class, $this ); } } includes/abstracts/abstract-wc-deprecated-hooks.php 0000644 00000006160 15132754524 0016515 0 ustar 00 <?php /** * Abstract deprecated hooks * * @package WooCommerce\Abstracts * @since 3.0.0 * @version 3.3.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Deprecated_Hooks class maps old actions and filters to new ones. This is the base class for handling those deprecated hooks. * * Based on the WCS_Hook_Deprecator class by Prospress. */ abstract class WC_Deprecated_Hooks { /** * Array of deprecated hooks we need to handle. * * @var array */ protected $deprecated_hooks = array(); /** * Array of versions on each hook has been deprecated. * * @var array */ protected $deprecated_version = array(); /** * Constructor. */ public function __construct() { $new_hooks = array_keys( $this->deprecated_hooks ); array_walk( $new_hooks, array( $this, 'hook_in' ) ); } /** * Hook into the new hook so we can handle deprecated hooks once fired. * * @param string $hook_name Hook name. */ abstract public function hook_in( $hook_name ); /** * Get old hooks to map to new hook. * * @param string $new_hook New hook name. * @return array */ public function get_old_hooks( $new_hook ) { $old_hooks = isset( $this->deprecated_hooks[ $new_hook ] ) ? $this->deprecated_hooks[ $new_hook ] : array(); $old_hooks = is_array( $old_hooks ) ? $old_hooks : array( $old_hooks ); return $old_hooks; } /** * If the hook is Deprecated, call the old hooks here. */ public function maybe_handle_deprecated_hook() { $new_hook = current_filter(); $old_hooks = $this->get_old_hooks( $new_hook ); $new_callback_args = func_get_args(); $return_value = $new_callback_args[0]; foreach ( $old_hooks as $old_hook ) { $return_value = $this->handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ); } return $return_value; } /** * If the old hook is in-use, trigger it. * * @param string $new_hook New hook name. * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @param mixed $return_value Returned value. * @return mixed */ abstract public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ); /** * Get deprecated version. * * @param string $old_hook Old hook name. * @return string */ protected function get_deprecated_version( $old_hook ) { return ! empty( $this->deprecated_version[ $old_hook ] ) ? $this->deprecated_version[ $old_hook ] : Constants::get_constant( 'WC_VERSION' ); } /** * Display a deprecated notice for old hooks. * * @param string $old_hook Old hook. * @param string $new_hook New hook. */ protected function display_notice( $old_hook, $new_hook ) { wc_deprecated_hook( esc_html( $old_hook ), esc_html( $this->get_deprecated_version( $old_hook ) ), esc_html( $new_hook ) ); } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed */ abstract protected function trigger_hook( $old_hook, $new_callback_args ); } includes/abstracts/abstract-wc-data.php 0000644 00000052631 15132754524 0014211 0 ustar 00 <?php /** * Abstract Data. * * Handles generic data interaction which is implemented by * the different data store classes. * * @class WC_Data * @version 3.0.0 * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Abstract WC Data Class * * Implemented by classes using the same CRUD(s) pattern. * * @version 2.6.0 * @package WooCommerce\Abstracts */ abstract class WC_Data { /** * ID for this object. * * @since 3.0.0 * @var int */ protected $id = 0; /** * Core data for this object. Name value pairs (name + default value). * * @since 3.0.0 * @var array */ protected $data = array(); /** * Core data changes for this object. * * @since 3.0.0 * @var array */ protected $changes = array(); /** * This is false until the object is read from the DB. * * @since 3.0.0 * @var bool */ protected $object_read = false; /** * This is the name of this object type. * * @since 3.0.0 * @var string */ protected $object_type = 'data'; /** * Extra data for this object. Name value pairs (name + default value). * Used as a standard way for sub classes (like product types) to add * additional information to an inherited class. * * @since 3.0.0 * @var array */ protected $extra_data = array(); /** * Set to _data on construct so we can track and reset data if needed. * * @since 3.0.0 * @var array */ protected $default_data = array(); /** * Contains a reference to the data store for this class. * * @since 3.0.0 * @var object */ protected $data_store; /** * Stores meta in cache for future reads. * A group must be set to to enable caching. * * @since 3.0.0 * @var string */ protected $cache_group = ''; /** * Stores additional meta data. * * @since 3.0.0 * @var array */ protected $meta_data = null; /** * Default constructor. * * @param int|object|array $read ID to load from the DB (optional) or already queried data. */ public function __construct( $read = 0 ) { $this->data = array_merge( $this->data, $this->extra_data ); $this->default_data = $this->data; } /** * Only store the object ID to avoid serializing the data object instance. * * @return array */ public function __sleep() { return array( 'id' ); } /** * Re-run the constructor with the object ID. * * If the object no longer exists, remove the ID. */ public function __wakeup() { try { $this->__construct( absint( $this->id ) ); } catch ( Exception $e ) { $this->set_id( 0 ); $this->set_object_read( true ); } } /** * When the object is cloned, make sure meta is duplicated correctly. * * @since 3.0.2 */ public function __clone() { $this->maybe_read_meta_data(); if ( ! empty( $this->meta_data ) ) { foreach ( $this->meta_data as $array_key => $meta ) { $this->meta_data[ $array_key ] = clone $meta; if ( ! empty( $meta->id ) ) { $this->meta_data[ $array_key ]->id = null; } } } } /** * Get the data store. * * @since 3.0.0 * @return object */ public function get_data_store() { return $this->data_store; } /** * Returns the unique ID for this object. * * @since 2.6.0 * @return int */ public function get_id() { return $this->id; } /** * Delete an object, set the ID to 0, and return result. * * @since 2.6.0 * @param bool $force_delete Should the date be deleted permanently. * @return bool result */ public function delete( $force_delete = false ) { if ( $this->data_store ) { $this->data_store->delete( $this, array( 'force_delete' => $force_delete ) ); $this->set_id( 0 ); return true; } return false; } /** * Save should create or update based on object existence. * * @since 2.6.0 * @return int */ public function save() { if ( ! $this->data_store ) { return $this->get_id(); } /** * Trigger action before saving to the DB. Allows you to adjust object props before save. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); if ( $this->get_id() ) { $this->data_store->update( $this ); } else { $this->data_store->create( $this ); } /** * Trigger action after saving to the DB. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); return $this->get_id(); } /** * Change data to JSON format. * * @since 2.6.0 * @return string Data in JSON format. */ public function __toString() { return wp_json_encode( $this->get_data() ); } /** * Returns all data for this object. * * @since 2.6.0 * @return array */ public function get_data() { return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) ); } /** * Returns array of expected data keys for this object. * * @since 3.0.0 * @return array */ public function get_data_keys() { return array_keys( $this->data ); } /** * Returns all "extra" data keys for an object (for sub objects like product types). * * @since 3.0.0 * @return array */ public function get_extra_data_keys() { return array_keys( $this->extra_data ); } /** * Filter null meta values from array. * * @since 3.0.0 * @param mixed $meta Meta value to check. * @return bool */ protected function filter_null_meta( $meta ) { return ! is_null( $meta->value ); } /** * Get All Meta Data. * * @since 2.6.0 * @return array of objects. */ public function get_meta_data() { $this->maybe_read_meta_data(); return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) ); } /** * Check if the key is an internal one. * * @since 3.2.0 * @param string $key Key to check. * @return bool true if it's an internal key, false otherwise */ protected function is_internal_meta_key( $key ) { $internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true ); if ( ! $internal_meta_key ) { return false; } $has_setter_or_getter = is_callable( array( $this, 'set_' . $key ) ) || is_callable( array( $this, 'get_' . $key ) ); if ( ! $has_setter_or_getter ) { return false; } /* translators: %s: $key Key to check */ wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'woocommerce' ), $key ), '3.2.0' ); return true; } /** * Get Meta Data by Key. * * @since 2.6.0 * @param string $key Meta Key. * @param bool $single return first found meta with key, or all with $key. * @param string $context What the value is for. Valid values are view and edit. * @return mixed */ public function get_meta( $key = '', $single = true, $context = 'view' ) { if ( $this->is_internal_meta_key( $key ) ) { $function = 'get_' . $key; if ( is_callable( array( $this, $function ) ) ) { return $this->{$function}(); } } $this->maybe_read_meta_data(); $meta_data = $this->get_meta_data(); $array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true ); $value = $single ? '' : array(); if ( ! empty( $array_keys ) ) { // We don't use the $this->meta_data property directly here because we don't want meta with a null value (i.e. meta which has been deleted via $this->delete_meta_data()). if ( $single ) { $value = $meta_data[ current( $array_keys ) ]->value; } else { $value = array_intersect_key( $meta_data, array_flip( $array_keys ) ); } } if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $key, $value, $this ); } return $value; } /** * See if meta data exists, since get_meta always returns a '' or array(). * * @since 3.0.0 * @param string $key Meta Key. * @return boolean */ public function meta_exists( $key = '' ) { $this->maybe_read_meta_data(); $array_keys = wp_list_pluck( $this->get_meta_data(), 'key' ); return in_array( $key, $array_keys, true ); } /** * Set all meta data from array. * * @since 2.6.0 * @param array $data Key/Value pairs. */ public function set_meta_data( $data ) { if ( ! empty( $data ) && is_array( $data ) ) { $this->maybe_read_meta_data(); foreach ( $data as $meta ) { $meta = (array) $meta; if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) { $this->meta_data[] = new WC_Meta_Data( array( 'id' => $meta['id'], 'key' => $meta['key'], 'value' => $meta['value'], ) ); } } } } /** * Add meta data. * * @since 2.6.0 * * @param string $key Meta key. * @param string|array $value Meta value. * @param bool $unique Should this be a unique key?. */ public function add_meta_data( $key, $value, $unique = false ) { if ( $this->is_internal_meta_key( $key ) ) { $function = 'set_' . $key; if ( is_callable( array( $this, $function ) ) ) { return $this->{$function}( $value ); } } $this->maybe_read_meta_data(); if ( $unique ) { $this->delete_meta_data( $key ); } $this->meta_data[] = new WC_Meta_Data( array( 'key' => $key, 'value' => $value, ) ); } /** * Update meta data by key or ID, if provided. * * @since 2.6.0 * * @param string $key Meta key. * @param string|array $value Meta value. * @param int $meta_id Meta ID. */ public function update_meta_data( $key, $value, $meta_id = 0 ) { if ( $this->is_internal_meta_key( $key ) ) { $function = 'set_' . $key; if ( is_callable( array( $this, $function ) ) ) { return $this->{$function}( $value ); } } $this->maybe_read_meta_data(); $array_key = false; if ( $meta_id ) { $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true ); $array_key = $array_keys ? current( $array_keys ) : false; } else { // Find matches by key. $matches = array(); foreach ( $this->meta_data as $meta_data_array_key => $meta ) { if ( $meta->key === $key ) { $matches[] = $meta_data_array_key; } } if ( ! empty( $matches ) ) { // Set matches to null so only one key gets the new value. foreach ( $matches as $meta_data_array_key ) { $this->meta_data[ $meta_data_array_key ]->value = null; } $array_key = current( $matches ); } } if ( false !== $array_key ) { $meta = $this->meta_data[ $array_key ]; $meta->key = $key; $meta->value = $value; } else { $this->add_meta_data( $key, $value, true ); } } /** * Delete meta data. * * @since 2.6.0 * @param string $key Meta key. */ public function delete_meta_data( $key ) { $this->maybe_read_meta_data(); $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true ); if ( $array_keys ) { foreach ( $array_keys as $array_key ) { $this->meta_data[ $array_key ]->value = null; } } } /** * Delete meta data. * * @since 2.6.0 * @param int $mid Meta ID. */ public function delete_meta_data_by_mid( $mid ) { $this->maybe_read_meta_data(); $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true ); if ( $array_keys ) { foreach ( $array_keys as $array_key ) { $this->meta_data[ $array_key ]->value = null; } } } /** * Read meta data if null. * * @since 3.0.0 */ protected function maybe_read_meta_data() { if ( is_null( $this->meta_data ) ) { $this->read_meta_data(); } } /** * Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column. * * @since 4.7.0 * * @return string */ public function get_meta_cache_key() { if ( ! $this->get_id() ) { wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' ); return false; } return self::generate_meta_cache_key( $this->get_id(), $this->cache_group ); } /** * Generate cache key from id and group. * * @since 4.7.0 * * @param int|string $id Object ID. * @param string $cache_group Group name use to store cache. Whole group cache can be invalidated in one go. * * @return string Meta cache key. */ public static function generate_meta_cache_key( $id, $cache_group ) { return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id; } /** * Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data. * * @since 4.7.0 * * @param array $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }. * @param string $cache_group Name of cache group. */ public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) { foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) { $cache_key = self::generate_meta_cache_key( $object_id, $cache_group ); wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group ); } } /** * Read Meta Data from the database. Ignore any internal properties. * Uses it's own caches because get_metadata does not provide meta_ids. * * @since 2.6.0 * @param bool $force_read True to force a new DB read (and update cache). */ public function read_meta_data( $force_read = false ) { $this->meta_data = array(); $cache_loaded = false; if ( ! $this->get_id() ) { return; } if ( ! $this->data_store ) { return; } if ( ! empty( $this->cache_group ) ) { // Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented. $cache_key = $this->get_meta_cache_key(); } if ( ! $force_read ) { if ( ! empty( $this->cache_group ) ) { $cached_meta = wp_cache_get( $cache_key, $this->cache_group ); $cache_loaded = ! empty( $cached_meta ); } } // We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different. $raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this ); if ( $raw_meta_data ) { foreach ( $raw_meta_data as $meta ) { $this->meta_data[] = new WC_Meta_Data( array( 'id' => (int) $meta->meta_id, 'key' => $meta->meta_key, 'value' => maybe_unserialize( $meta->meta_value ), ) ); } if ( ! $cache_loaded && ! empty( $this->cache_group ) ) { wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group ); } } } /** * Update Meta Data in the database. * * @since 2.6.0 */ public function save_meta_data() { if ( ! $this->data_store || is_null( $this->meta_data ) ) { return; } foreach ( $this->meta_data as $array_key => $meta ) { if ( is_null( $meta->value ) ) { if ( ! empty( $meta->id ) ) { $this->data_store->delete_meta( $this, $meta ); unset( $this->meta_data[ $array_key ] ); } } elseif ( empty( $meta->id ) ) { $meta->id = $this->data_store->add_meta( $this, $meta ); $meta->apply_changes(); } else { if ( $meta->get_changes() ) { $this->data_store->update_meta( $this, $meta ); $meta->apply_changes(); } } } if ( ! empty( $this->cache_group ) ) { $cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id(); wp_cache_delete( $cache_key, $this->cache_group ); } } /** * Set ID. * * @since 3.0.0 * @param int $id ID. */ public function set_id( $id ) { $this->id = absint( $id ); } /** * Set all props to default values. * * @since 3.0.0 */ public function set_defaults() { $this->data = $this->default_data; $this->changes = array(); $this->set_object_read( false ); } /** * Set object read property. * * @since 3.0.0 * @param boolean $read Should read?. */ public function set_object_read( $read = true ) { $this->object_read = (bool) $read; } /** * Get object read property. * * @since 3.0.0 * @return boolean */ public function get_object_read() { return (bool) $this->object_read; } /** * Set a collection of props in one go, collect any errors, and return the result. * Only sets using public methods. * * @since 3.0.0 * * @param array $props Key value pairs to set. Key is the prop and should map to a setter function name. * @param string $context In what context to run this. * * @return bool|WP_Error */ public function set_props( $props, $context = 'set' ) { $errors = false; foreach ( $props as $prop => $value ) { try { /** * Checks if the prop being set is allowed, and the value is not null. */ if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) { continue; } $setter = "set_$prop"; if ( is_callable( array( $this, $setter ) ) ) { $this->{$setter}( $value ); } } catch ( WC_Data_Exception $e ) { if ( ! $errors ) { $errors = new WP_Error(); } $errors->add( $e->getErrorCode(), $e->getMessage() ); } } return $errors && count( $errors->get_error_codes() ) ? $errors : true; } /** * Sets a prop for a setter method. * * This stores changes in a special array so we can track what needs saving * the the DB later. * * @since 3.0.0 * @param string $prop Name of prop to set. * @param mixed $value Value of the prop. */ protected function set_prop( $prop, $value ) { if ( array_key_exists( $prop, $this->data ) ) { if ( true === $this->object_read ) { if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) { $this->changes[ $prop ] = $value; } } else { $this->data[ $prop ] = $value; } } } /** * Return data changes only. * * @since 3.0.0 * @return array */ public function get_changes() { return $this->changes; } /** * Merge changes with data and clear. * * @since 3.0.0 */ public function apply_changes() { $this->data = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine $this->changes = array(); } /** * Prefix for action and filter hooks on data. * * @since 3.0.0 * @return string */ protected function get_hook_prefix() { return 'woocommerce_' . $this->object_type . '_get_'; } /** * Gets a prop for a getter method. * * Gets the value from either current pending changes, or the data itself. * Context controls what happens to the value before it's returned. * * @since 3.0.0 * @param string $prop Name of prop to get. * @param string $context What the value is for. Valid values are view and edit. * @return mixed */ protected function get_prop( $prop, $context = 'view' ) { $value = null; if ( array_key_exists( $prop, $this->data ) ) { $value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ]; if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); } } return $value; } /** * Sets a date prop whilst handling formatting and datetime objects. * * @since 3.0.0 * @param string $prop Name of prop to set. * @param string|integer $value Value of the prop. */ protected function set_date_prop( $prop, $value ) { try { if ( empty( $value ) ) { $this->set_prop( $prop, null ); return; } if ( is_a( $value, 'WC_DateTime' ) ) { $datetime = $value; } elseif ( is_numeric( $value ) ) { // Timestamps are handled as UTC timestamps in all cases. $datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) ); } else { // Strings are defined in local WP timezone. Convert to UTC. if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $value, $date_bits ) ) { $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; } else { $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) ); } $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); } // Set local timezone or offset. if ( get_option( 'timezone_string' ) ) { $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { $datetime->set_utc_offset( wc_timezone_offset() ); } $this->set_prop( $prop, $datetime ); } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. } /** * When invalid data is found, throw an exception unless reading from the DB. * * @throws WC_Data_Exception Data Exception. * @since 3.0.0 * @param string $code Error code. * @param string $message Error message. * @param int $http_status_code HTTP status code. * @param array $data Extra error data. */ protected function error( $code, $message, $http_status_code = 400, $data = array() ) { throw new WC_Data_Exception( $code, $message, $http_status_code, $data ); } } includes/abstracts/abstract-wc-order.php 0000644 00000205005 15132754524 0014406 0 ustar 00 <?php /** * Abstract Order * * Handles generic order data and database interaction which is extended by both * WC_Order (regular orders) and WC_Order_Refund (refunds are negative orders). * * @class WC_Abstract_Order * @version 3.0.0 * @package WooCommerce\Classes */ use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-order.php'; /** * WC_Abstract_Order class. */ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { use WC_Item_Totals; /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * Notes: cart_tax = cart_tax is the new name for the legacy 'order_tax' * which is the tax for items only, not shipping. * * @since 3.0.0 * @var array */ protected $data = array( 'parent_id' => 0, 'status' => '', 'currency' => '', 'version' => '', 'prices_include_tax' => false, 'date_created' => null, 'date_modified' => null, 'discount_total' => 0, 'discount_tax' => 0, 'shipping_total' => 0, 'shipping_tax' => 0, 'cart_tax' => 0, 'total' => 0, 'total_tax' => 0, ); /** * Order items will be stored here, sometimes before they persist in the DB. * * @since 3.0.0 * @var array */ protected $items = array(); /** * Order items that need deleting are stored here. * * @since 3.0.0 * @var array */ protected $items_to_delete = array(); /** * Stores meta in cache for future reads. * * A group must be set to to enable caching. * * @var string */ protected $cache_group = 'orders'; /** * Which data store to load. * * @var string */ protected $data_store_name = 'order'; /** * This is the name of this object type. * * @var string */ protected $object_type = 'order'; /** * Get the order if ID is passed, otherwise the order is new and empty. * This class should NOT be instantiated, but the wc_get_order function or new WC_Order_Factory * should be used. It is possible, but the aforementioned are preferred and are the only * methods that will be maintained going forward. * * @param int|object|WC_Order $order Order to read. */ public function __construct( $order = 0 ) { parent::__construct( $order ); if ( is_numeric( $order ) && $order > 0 ) { $this->set_id( $order ); } elseif ( $order instanceof self ) { $this->set_id( $order->get_id() ); } elseif ( ! empty( $order->ID ) ) { $this->set_id( $order->ID ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( $this->data_store_name ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /** * Get internal type. * * @return string */ public function get_type() { return 'shop_order'; } /** * Get all class data in array format. * * @since 3.0.0 * @return array */ public function get_data() { return array_merge( array( 'id' => $this->get_id(), ), $this->data, array( 'meta_data' => $this->get_meta_data(), 'line_items' => $this->get_items( 'line_item' ), 'tax_lines' => $this->get_items( 'tax' ), 'shipping_lines' => $this->get_items( 'shipping' ), 'fee_lines' => $this->get_items( 'fee' ), 'coupon_lines' => $this->get_items( 'coupon' ), ) ); } /* |-------------------------------------------------------------------------- | CRUD methods |-------------------------------------------------------------------------- | | Methods which create, read, update and delete orders from the database. | Written in abstract fashion so that the way orders are stored can be | changed more easily in the future. | | A save method is included for convenience (chooses update or create based | on if the order exists yet). | */ /** * Save data to the database. * * @since 3.0.0 * @return int order ID */ public function save() { if ( ! $this->data_store ) { return $this->get_id(); } try { /** * Trigger action before saving to the DB. Allows you to adjust object props before save. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); if ( $this->get_id() ) { $this->data_store->update( $this ); } else { $this->data_store->create( $this ); } $this->save_items(); /** * Trigger action after saving to the DB. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); } catch ( Exception $e ) { $this->handle_exception( $e, __( 'Error saving order.', 'woocommerce' ) ); } return $this->get_id(); } /** * Log an error about this order is exception is encountered. * * @param Exception $e Exception object. * @param string $message Message regarding exception thrown. * @since 3.7.0 */ protected function handle_exception( $e, $message = 'Error' ) { wc_get_logger()->error( $message, array( 'order' => $this, 'error' => $e, ) ); } /** * Save all order items which are part of this order. */ protected function save_items() { $items_changed = false; foreach ( $this->items_to_delete as $item ) { $item->delete(); $items_changed = true; } $this->items_to_delete = array(); // Add/save items. foreach ( $this->items as $item_group => $items ) { if ( is_array( $items ) ) { $items = array_filter( $items ); foreach ( $items as $item_key => $item ) { $item->set_order_id( $this->get_id() ); $item_id = $item->save(); // If ID changed (new item saved to DB)... if ( $item_id !== $item_key ) { $this->items[ $item_group ][ $item_id ] = $item; unset( $this->items[ $item_group ][ $item_key ] ); $items_changed = true; } } } } if ( $items_changed ) { delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get parent order ID. * * @since 3.0.0 * @param string $context View or edit context. * @return integer */ public function get_parent_id( $context = 'view' ) { return $this->get_prop( 'parent_id', $context ); } /** * Gets order currency. * * @param string $context View or edit context. * @return string */ public function get_currency( $context = 'view' ) { return $this->get_prop( 'currency', $context ); } /** * Get order_version. * * @param string $context View or edit context. * @return string */ public function get_version( $context = 'view' ) { return $this->get_prop( 'version', $context ); } /** * Get prices_include_tax. * * @param string $context View or edit context. * @return bool */ public function get_prices_include_tax( $context = 'view' ) { return $this->get_prop( 'prices_include_tax', $context ); } /** * Get date_created. * * @param string $context View or edit context. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { return $this->get_prop( 'date_created', $context ); } /** * Get date_modified. * * @param string $context View or edit context. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { return $this->get_prop( 'date_modified', $context ); } /** * Return the order statuses without wc- internal prefix. * * @param string $context View or edit context. * @return string */ public function get_status( $context = 'view' ) { $status = $this->get_prop( 'status', $context ); if ( empty( $status ) && 'view' === $context ) { // In view context, return the default status if no status has been set. $status = apply_filters( 'woocommerce_default_order_status', 'pending' ); } return $status; } /** * Get discount_total. * * @param string $context View or edit context. * @return string */ public function get_discount_total( $context = 'view' ) { return $this->get_prop( 'discount_total', $context ); } /** * Get discount_tax. * * @param string $context View or edit context. * @return string */ public function get_discount_tax( $context = 'view' ) { return $this->get_prop( 'discount_tax', $context ); } /** * Get shipping_total. * * @param string $context View or edit context. * @return string */ public function get_shipping_total( $context = 'view' ) { return $this->get_prop( 'shipping_total', $context ); } /** * Get shipping_tax. * * @param string $context View or edit context. * @return string */ public function get_shipping_tax( $context = 'view' ) { return $this->get_prop( 'shipping_tax', $context ); } /** * Gets cart tax amount. * * @param string $context View or edit context. * @return float */ public function get_cart_tax( $context = 'view' ) { return $this->get_prop( 'cart_tax', $context ); } /** * Gets order grand total. incl. taxes. Used in gateways. * * @param string $context View or edit context. * @return float */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax amount. Alias for get_order_tax(). * * @param string $context View or edit context. * @return float */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /* |-------------------------------------------------------------------------- | Non-CRUD Getters |-------------------------------------------------------------------------- */ /** * Gets the total discount amount. * * @param bool $ex_tax Show discount excl any tax. * @return float */ public function get_total_discount( $ex_tax = true ) { if ( $ex_tax ) { $total_discount = $this->get_discount_total(); } else { $total_discount = $this->get_discount_total() + $this->get_discount_tax(); } return apply_filters( 'woocommerce_order_get_total_discount', NumberUtil::round( $total_discount, WC_ROUNDING_PRECISION ), $this ); } /** * Gets order subtotal. * * @return float */ public function get_subtotal() { $subtotal = NumberUtil::round( $this->get_cart_subtotal_for_order(), wc_get_price_decimals() ); return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this ); } /** * Get taxes, merged by code, formatted ready for output. * * @return array */ public function get_tax_totals() { $tax_totals = array(); foreach ( $this->get_items( 'tax' ) as $key => $tax ) { $code = $tax->get_rate_code(); if ( ! isset( $tax_totals[ $code ] ) ) { $tax_totals[ $code ] = new stdClass(); $tax_totals[ $code ]->amount = 0; } $tax_totals[ $code ]->id = $key; $tax_totals[ $code ]->rate_id = $tax->get_rate_id(); $tax_totals[ $code ]->is_compound = $tax->is_compound(); $tax_totals[ $code ]->label = $tax->get_label(); $tax_totals[ $code ]->amount += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total(); $tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount, array( 'currency' => $this->get_currency() ) ); } if ( apply_filters( 'woocommerce_order_hide_zero_taxes', true ) ) { $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) ); $tax_totals = array_intersect_key( $tax_totals, $amounts ); } return apply_filters( 'woocommerce_order_get_tax_totals', $tax_totals, $this ); } /** * Get all valid statuses for this order * * @since 3.0.0 * @return array Internal status keys e.g. 'wc-processing' */ protected function get_valid_statuses() { return array_keys( wc_get_order_statuses() ); } /** * Get user ID. Used by orders, not other order types like refunds. * * @param string $context View or edit context. * @return int */ public function get_user_id( $context = 'view' ) { return 0; } /** * Get user. Used by orders, not other order types like refunds. * * @return WP_User|false */ public function get_user() { return false; } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting order data. These should not update anything in the | database itself and should only change what is stored in the class | object. However, for backwards compatibility pre 3.0.0 some of these | setters may handle both. */ /** * Set parent order ID. * * @since 3.0.0 * @param int $value Value to set. * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid. */ public function set_parent_id( $value ) { if ( $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) { $this->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) ); } $this->set_prop( 'parent_id', absint( $value ) ); } /** * Set order status. * * @since 3.0.0 * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @return array details of change */ public function set_status( $new_status ) { $old_status = $this->get_status(); $new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status; // If setting the status, ensure it's set to a valid status. if ( true === $this->object_read ) { // Only allow valid new status. if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status ) { $new_status = 'pending'; } // If the old status is set but unknown (e.g. draft) assume its pending for action usage. if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && 'trash' !== $old_status ) { $old_status = 'pending'; } } $this->set_prop( 'status', $new_status ); return array( 'from' => $old_status, 'to' => $new_status, ); } /** * Set order_version. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_version( $value ) { $this->set_prop( 'version', $value ); } /** * Set order_currency. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_currency( $value ) { if ( $value && ! in_array( $value, array_keys( get_woocommerce_currencies() ), true ) ) { $this->error( 'order_invalid_currency', __( 'Invalid currency code', 'woocommerce' ) ); } $this->set_prop( 'currency', $value ? $value : get_woocommerce_currency() ); } /** * Set prices_include_tax. * * @param bool $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_prices_include_tax( $value ) { $this->set_prop( 'prices_include_tax', (bool) $value ); } /** * Set date_created. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set date_modified. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set discount_total. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_discount_total( $value ) { $this->set_prop( 'discount_total', wc_format_decimal( $value ) ); } /** * Set discount_tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_discount_tax( $value ) { $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); } /** * Set shipping_total. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_shipping_total( $value ) { $this->set_prop( 'shipping_total', wc_format_decimal( $value ) ); } /** * Set shipping_tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_shipping_tax( $value ) { $this->set_prop( 'shipping_tax', wc_format_decimal( $value ) ); $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); } /** * Set cart tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_cart_tax( $value ) { $this->set_prop( 'cart_tax', wc_format_decimal( $value ) ); $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); } /** * Sets order tax (sum of cart and shipping tax). Used internally only. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ protected function set_total_tax( $value ) { // We round here because this is a total entry, as opposed to line items in other setters. $this->set_prop( 'total_tax', wc_format_decimal( NumberUtil::round( $value, wc_get_price_decimals() ) ) ); } /** * Set total. * * @param string $value Value to set. * @param string $deprecated Function used to set different totals based on this. * * @return bool|void * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_total( $value, $deprecated = '' ) { if ( $deprecated ) { wc_deprecated_argument( 'total_type', '3.0', 'Use dedicated total setter methods instead.' ); return $this->legacy_set_total( $value, $deprecated ); } $this->set_prop( 'total', wc_format_decimal( $value, wc_get_price_decimals() ) ); } /* |-------------------------------------------------------------------------- | Order Item Handling |-------------------------------------------------------------------------- | | Order items are used for products, taxes, shipping, and fees within | each order. */ /** * Remove all line items (products, coupons, shipping, taxes) from the order. * * @param string $type Order item type. Default null. */ public function remove_order_items( $type = null ) { if ( ! empty( $type ) ) { $this->data_store->delete_items( $this, $type ); $group = $this->type_to_group( $type ); if ( $group ) { unset( $this->items[ $group ] ); } } else { $this->data_store->delete_items( $this ); $this->items = array(); } } /** * Convert a type to a types group. * * @param string $type type to lookup. * @return string */ protected function type_to_group( $type ) { $type_to_group = apply_filters( 'woocommerce_order_type_to_group', array( 'line_item' => 'line_items', 'tax' => 'tax_lines', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ) ); return isset( $type_to_group[ $type ] ) ? $type_to_group[ $type ] : ''; } /** * Return an array of items/products within this order. * * @param string|array $types Types of line items to get (array or string). * @return WC_Order_Item[] */ public function get_items( $types = 'line_item' ) { $items = array(); $types = array_filter( (array) $types ); foreach ( $types as $type ) { $group = $this->type_to_group( $type ); if ( $group ) { if ( ! isset( $this->items[ $group ] ) ) { $this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) ); } // Don't use array_merge here because keys are numeric. $items = $items + $this->items[ $group ]; } } return apply_filters( 'woocommerce_order_get_items', $items, $this, $types ); } /** * Return array of values for calculations. * * @param string $field Field name to return. * * @return array Array of values. */ protected function get_values_for_total( $field ) { $items = array_map( function ( $item ) use ( $field ) { return wc_add_number_precision( $item[ $field ], false ); }, array_values( $this->get_items() ) ); return $items; } /** * Return an array of coupons within this order. * * @since 3.7.0 * @return WC_Order_Item_Coupon[] */ public function get_coupons() { return $this->get_items( 'coupon' ); } /** * Return an array of fees within this order. * * @return WC_Order_item_Fee[] */ public function get_fees() { return $this->get_items( 'fee' ); } /** * Return an array of taxes within this order. * * @return WC_Order_Item_Tax[] */ public function get_taxes() { return $this->get_items( 'tax' ); } /** * Return an array of shipping costs within this order. * * @return WC_Order_Item_Shipping[] */ public function get_shipping_methods() { return $this->get_items( 'shipping' ); } /** * Gets formatted shipping method title. * * @return string */ public function get_shipping_method() { $names = array(); foreach ( $this->get_shipping_methods() as $shipping_method ) { $names[] = $shipping_method->get_name(); } return apply_filters( 'woocommerce_order_shipping_method', implode( ', ', $names ), $this ); } /** * Get used coupon codes only. * * @since 3.7.0 * @return array */ public function get_coupon_codes() { $coupon_codes = array(); $coupons = $this->get_items( 'coupon' ); if ( $coupons ) { foreach ( $coupons as $coupon ) { $coupon_codes[] = $coupon->get_code(); } } return $coupon_codes; } /** * Gets the count of order items of a certain type. * * @param string $item_type Item type to lookup. * @return int|string */ public function get_item_count( $item_type = '' ) { $items = $this->get_items( empty( $item_type ) ? 'line_item' : $item_type ); $count = 0; foreach ( $items as $item ) { $count += $item->get_quantity(); } return apply_filters( 'woocommerce_get_item_count', $count, $item_type, $this ); } /** * Get an order item object, based on its type. * * @since 3.0.0 * @param int $item_id ID of item to get. * @param bool $load_from_db Prior to 3.2 this item was loaded direct from WC_Order_Factory, not this object. This param is here for backwards compatility with that. If false, uses the local items variable instead. * @return WC_Order_Item|false */ public function get_item( $item_id, $load_from_db = true ) { if ( $load_from_db ) { return WC_Order_Factory::get_order_item( $item_id ); } // Search for item id. if ( $this->items ) { foreach ( $this->items as $group => $items ) { if ( isset( $items[ $item_id ] ) ) { return $items[ $item_id ]; } } } // Load all items of type and cache. $type = $this->data_store->get_order_item_type( $this, $item_id ); if ( ! $type ) { return false; } $items = $this->get_items( $type ); return ! empty( $items[ $item_id ] ) ? $items[ $item_id ] : false; } /** * Get key for where a certain item type is stored in _items. * * @since 3.0.0 * @param string $item object Order item (product, shipping, fee, coupon, tax). * @return string */ protected function get_items_key( $item ) { if ( is_a( $item, 'WC_Order_Item_Product' ) ) { return 'line_items'; } elseif ( is_a( $item, 'WC_Order_Item_Fee' ) ) { return 'fee_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { return 'shipping_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Tax' ) ) { return 'tax_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { return 'coupon_lines'; } return apply_filters( 'woocommerce_get_items_key', '', $item ); } /** * Remove item from the order. * * @param int $item_id Item ID to delete. * @return false|void */ public function remove_item( $item_id ) { $item = $this->get_item( $item_id, false ); $items_key = $item ? $this->get_items_key( $item ) : false; if ( ! $items_key ) { return false; } // Unset and remove later. $this->items_to_delete[] = $item; unset( $this->items[ $items_key ][ $item->get_id() ] ); } /** * Adds an order item to this order. The order item will not persist until save. * * @since 3.0.0 * @param WC_Order_Item $item Order item object (product, shipping, fee, coupon, tax). * @return false|void */ public function add_item( $item ) { $items_key = $this->get_items_key( $item ); if ( ! $items_key ) { return false; } // Make sure existing items are loaded so we can append this new one. if ( ! isset( $this->items[ $items_key ] ) ) { $this->items[ $items_key ] = $this->get_items( $item->get_type() ); } // Set parent. $item->set_order_id( $this->get_id() ); // Append new row with generated temporary ID. $item_id = $item->get_id(); if ( $item_id ) { $this->items[ $items_key ][ $item_id ] = $item; } else { $this->items[ $items_key ][ 'new:' . $items_key . count( $this->items[ $items_key ] ) ] = $item; } } /** * Check and records coupon usage tentatively so that counts validation is correct. Display an error if coupon usage limit has been reached. * * If you are using this method, make sure to `release_held_coupons` in case an Exception is thrown. * * @throws Exception When not able to apply coupon. * * @param string $billing_email Billing email of order. */ public function hold_applied_coupons( $billing_email ) { $held_keys = array(); $held_keys_for_user = array(); $error = null; try { foreach ( WC()->cart->get_applied_coupons() as $code ) { $coupon = new WC_Coupon( $code ); if ( ! $coupon->get_data_store() ) { continue; } // Hold coupon for when global coupon usage limit is present. if ( 0 < $coupon->get_usage_limit() ) { $held_key = $this->hold_coupon( $coupon ); if ( $held_key ) { $held_keys[ $coupon->get_id() ] = $held_key; } } // Hold coupon for when usage limit per customer is enabled. if ( 0 < $coupon->get_usage_limit_per_user() ) { if ( ! isset( $user_ids_and_emails ) ) { $user_alias = get_current_user_id() ? wp_get_current_user()->ID : sanitize_email( $billing_email ); $user_ids_and_emails = $this->get_billing_and_current_user_aliases( $billing_email ); } $held_key_for_user = $this->hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ); if ( $held_key_for_user ) { $held_keys_for_user[ $coupon->get_id() ] = $held_key_for_user; } } } } catch ( Exception $e ) { $error = $e; } finally { // Even in case of error, we will save keys for whatever coupons that were held so our data remains accurate. // We save them in bulk instead of one by one for performance reasons. if ( 0 < count( $held_keys_for_user ) || 0 < count( $held_keys ) ) { $this->get_data_store()->set_coupon_held_keys( $this, $held_keys, $held_keys_for_user ); } if ( $error instanceof Exception ) { throw $error; } } } /** * Hold coupon if a global usage limit is defined. * * @param WC_Coupon $coupon Coupon object. * * @return string Meta key which indicates held coupon. * @throws Exception When can't be held. */ private function hold_coupon( $coupon ) { $result = $coupon->get_data_store()->check_and_hold_coupon( $coupon ); if ( false === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } elseif ( 0 === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'Coupon %s was used in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } return $result; } /** * Hold coupon if usage limit per customer is defined. * * @param WC_Coupon $coupon Coupon object. * @param array $user_ids_and_emails Array of user Id and emails to check for usage limit. * @param string $user_alias User ID or email to use to record current usage. * * @return string Meta key which indicates held coupon. * @throws Exception When coupon can't be held. */ private function hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ) { $result = $coupon->get_data_store()->check_and_hold_coupon_for_user( $coupon, $user_ids_and_emails, $user_alias ); if ( false === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } elseif ( 0 === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'You have used this coupon %s in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } return $result; } /** * Helper method to get all aliases for current user and provide billing email. * * @param string $billing_email Billing email provided in form. * * @return array Array of all aliases. * @throws Exception When validation fails. */ private function get_billing_and_current_user_aliases( $billing_email ) { $emails = array( $billing_email ); if ( get_current_user_id() ) { $emails[] = wp_get_current_user()->user_email; } $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); $customer_data_store = WC_Data_Store::load( 'customer' ); $user_ids = $customer_data_store->get_user_ids_for_billing_email( $emails ); return array_merge( $user_ids, $emails ); } /** * Apply a coupon to the order and recalculate totals. * * @since 3.2.0 * @param string|WC_Coupon $raw_coupon Coupon code or object. * @return true|WP_Error True if applied, error if not. */ public function apply_coupon( $raw_coupon ) { if ( is_a( $raw_coupon, 'WC_Coupon' ) ) { $coupon = $raw_coupon; } elseif ( is_string( $raw_coupon ) ) { $code = wc_format_coupon_code( $raw_coupon ); $coupon = new WC_Coupon( $code ); if ( $coupon->get_code() !== $code ) { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon code', 'woocommerce' ) ); } } else { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); } // Check to make sure coupon is not already applied. $applied_coupons = $this->get_items( 'coupon' ); foreach ( $applied_coupons as $applied_coupon ) { if ( $applied_coupon->get_code() === $coupon->get_code() ) { return new WP_Error( 'invalid_coupon', __( 'Coupon code already applied!', 'woocommerce' ) ); } } $discounts = new WC_Discounts( $this ); $applied = $discounts->apply_coupon( $coupon ); if ( is_wp_error( $applied ) ) { return $applied; } $data_store = $coupon->get_data_store(); // Check specific for guest checkouts here as well since WC_Cart handles that seperately in check_customer_coupons. if ( $data_store && 0 === $this->get_customer_id() ) { $usage_count = $data_store->get_usage_by_email( $coupon, $this->get_billing_email() ); if ( 0 < $coupon->get_usage_limit_per_user() && $usage_count >= $coupon->get_usage_limit_per_user() ) { return new WP_Error( 'invalid_coupon', $coupon->get_coupon_error( 106 ), array( 'status' => 400, ) ); } } $this->set_coupon_discount_amounts( $discounts ); $this->save(); // Recalculate totals and taxes. $this->recalculate_coupons(); // Record usage so counts and validation is correct. $used_by = $this->get_user_id(); if ( ! $used_by ) { $used_by = $this->get_billing_email(); } $coupon->increase_usage_count( $used_by ); return true; } /** * Remove a coupon from the order and recalculate totals. * * Coupons affect line item totals, but there is no relationship between * coupon and line total, so to remove a coupon we need to work from the * line subtotal (price before discount) and re-apply all coupons in this * order. * * Manual discounts are not affected; those are separate and do not affect * stored line totals. * * @since 3.2.0 * @param string $code Coupon code. * @return void */ public function remove_coupon( $code ) { $coupons = $this->get_items( 'coupon' ); // Remove the coupon line. foreach ( $coupons as $item_id => $coupon ) { if ( $coupon->get_code() === $code ) { $this->remove_item( $item_id ); $coupon_object = new WC_Coupon( $code ); $coupon_object->decrease_usage_count( $this->get_user_id() ); $this->recalculate_coupons(); break; } } } /** * Apply all coupons in this order again to all line items. * This method is public since WooCommerce 3.8.0. * * @since 3.2.0 */ public function recalculate_coupons() { // Reset line item totals. foreach ( $this->get_items() as $item ) { $item->set_total( $item->get_subtotal() ); $item->set_total_tax( $item->get_subtotal_tax() ); } $discounts = new WC_Discounts( $this ); foreach ( $this->get_items( 'coupon' ) as $coupon_item ) { $coupon_code = $coupon_item->get_code(); $coupon_id = wc_get_coupon_id_by_code( $coupon_code ); // If we have a coupon ID (loaded via wc_get_coupon_id_by_code) we can simply load the new coupon object using the ID. if ( $coupon_id ) { $coupon_object = new WC_Coupon( $coupon_id ); } else { // If we do not have a coupon ID (was it virtual? has it been deleted?) we must create a temporary coupon using what data we have stored during checkout. $coupon_object = new WC_Coupon(); $coupon_object->set_props( (array) $coupon_item->get_meta( 'coupon_data', true ) ); $coupon_object->set_code( $coupon_code ); $coupon_object->set_virtual( true ); // If there is no coupon amount (maybe dynamic?), set it to the given **discount** amount so the coupon's same value is applied. if ( ! $coupon_object->get_amount() ) { // If the order originally had prices including tax, remove the discount + discount tax. if ( $this->get_prices_include_tax() ) { $coupon_object->set_amount( $coupon_item->get_discount() + $coupon_item->get_discount_tax() ); } else { $coupon_object->set_amount( $coupon_item->get_discount() ); } $coupon_object->set_discount_type( 'fixed_cart' ); } } /** * Allow developers to filter this coupon before it get's re-applied to the order. * * @since 3.2.0 */ $coupon_object = apply_filters( 'woocommerce_order_recalculate_coupons_coupon_object', $coupon_object, $coupon_code, $coupon_item, $this ); if ( $coupon_object ) { $discounts->apply_coupon( $coupon_object, false ); } } $this->set_coupon_discount_amounts( $discounts ); $this->set_item_discount_amounts( $discounts ); // Recalculate totals and taxes. $this->calculate_totals( true ); } /** * After applying coupons via the WC_Discounts class, update line items. * * @since 3.2.0 * @param WC_Discounts $discounts Discounts class. */ protected function set_item_discount_amounts( $discounts ) { $item_discounts = $discounts->get_discounts_by_item(); $tax_location = $this->get_tax_location(); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); if ( $item_discounts ) { foreach ( $item_discounts as $item_id => $amount ) { $item = $this->get_item( $item_id, false ); // If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart. if ( $this->get_prices_include_tax() && wc_tax_enabled() && 'taxable' === $item->get_tax_status() ) { $taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true ); // Use unrounded taxes so totals will be re-calculated accurately, like in cart. $amount = $amount - array_sum( $taxes ); } $item->set_total( max( 0, $item->get_total() - $amount ) ); } } } /** * After applying coupons via the WC_Discounts class, update or create coupon items. * * @since 3.2.0 * @param WC_Discounts $discounts Discounts class. */ protected function set_coupon_discount_amounts( $discounts ) { $coupons = $this->get_items( 'coupon' ); $coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' ); $all_discounts = $discounts->get_discounts(); $coupon_discounts = $discounts->get_discounts_by_coupon(); $tax_location = $this->get_tax_location(); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'], ); if ( $coupon_discounts ) { foreach ( $coupon_discounts as $coupon_code => $amount ) { $item_id = isset( $coupon_code_to_id[ $coupon_code ] ) ? $coupon_code_to_id[ $coupon_code ] : 0; if ( ! $item_id ) { $coupon_item = new WC_Order_Item_Coupon(); $coupon_item->set_code( $coupon_code ); } else { $coupon_item = $this->get_item( $item_id, false ); } $discount_tax = 0; // Work out how much tax has been removed as a result of the discount from this coupon. foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) { $item = $this->get_item( $item_id, false ); if ( 'taxable' !== $item->get_tax_status() || ! wc_tax_enabled() ) { continue; } $taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), $this->get_prices_include_tax() ) ); if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes = wc_round_tax_total( $taxes ); } $discount_tax += $taxes; if ( $this->get_prices_include_tax() ) { $amount = $amount - $taxes; } } $coupon_item->set_discount( $amount ); $coupon_item->set_discount_tax( $discount_tax ); $this->add_item( $coupon_item ); } } } /** * Add a product line item to the order. This is the only line item type with * its own method because it saves looking up order amounts (costs are added up for you). * * @param WC_Product $product Product object. * @param int $qty Quantity to add. * @param array $args Args for the added product. * @return int */ public function add_product( $product, $qty = 1, $args = array() ) { if ( $product ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $total = wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'order' => $order, ) ); $default_args = array( 'name' => $product->get_name(), 'tax_class' => $product->get_tax_class(), 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, 'variation' => $product->is_type( 'variation' ) ? $product->get_attributes() : array(), 'subtotal' => $total, 'total' => $total, 'quantity' => $qty, ); } else { $default_args = array( 'quantity' => $qty, ); } $args = wp_parse_args( $args, $default_args ); // BW compatibility with old args. if ( isset( $args['totals'] ) ) { foreach ( $args['totals'] as $key => $value ) { if ( 'tax' === $key ) { $args['total_tax'] = $value; } elseif ( 'tax_data' === $key ) { $args['taxes'] = $value; } else { $args[ $key ] = $value; } } } $item = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Order_Item_Product::class ); $item->set_props( $args ); $item->set_backorder_meta(); $item->set_order_id( $this->get_id() ); $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' ); delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' ); return $item->get_id(); } /* |-------------------------------------------------------------------------- | Payment Token Handling |-------------------------------------------------------------------------- | | Payment tokens are hashes used to take payments by certain gateways. | */ /** * Add a payment token to an order * * @since 2.6 * @param WC_Payment_Token $token Payment token object. * @return boolean|int The new token ID or false if it failed. */ public function add_payment_token( $token ) { if ( empty( $token ) || ! ( $token instanceof WC_Payment_Token ) ) { return false; } $token_ids = $this->data_store->get_payment_token_ids( $this ); $token_ids[] = $token->get_id(); $this->data_store->update_payment_token_ids( $this, $token_ids ); do_action( 'woocommerce_payment_token_added_to_order', $this->get_id(), $token->get_id(), $token, $token_ids ); return $token->get_id(); } /** * Returns a list of all payment tokens associated with the current order * * @since 2.6 * @return array An array of payment token objects */ public function get_payment_tokens() { return $this->data_store->get_payment_token_ids( $this ); } /* |-------------------------------------------------------------------------- | Calculations. |-------------------------------------------------------------------------- | | These methods calculate order totals and taxes based on the current data. | */ /** * Calculate shipping total. * * @since 2.2 * @return float */ public function calculate_shipping() { $shipping_total = 0; foreach ( $this->get_shipping_methods() as $shipping ) { $shipping_total += $shipping->get_total(); } $this->set_shipping_total( $shipping_total ); $this->save(); return $this->get_shipping_total(); } /** * Get all tax classes for items in the order. * * @since 2.6.3 * @return array */ public function get_items_tax_classes() { $found_tax_classes = array(); foreach ( $this->get_items() as $item ) { if ( is_callable( array( $item, 'get_tax_status' ) ) && in_array( $item->get_tax_status(), array( 'taxable', 'shipping' ), true ) ) { $found_tax_classes[] = $item->get_tax_class(); } } return array_unique( $found_tax_classes ); } /** * Get tax location for this order. * * @since 3.2.0 * @param array $args array Override the location. * @return array */ protected function get_tax_location( $args = array() ) { $tax_based_on = get_option( 'woocommerce_tax_based_on' ); if ( 'shipping' === $tax_based_on && ! $this->get_shipping_country() ) { $tax_based_on = 'billing'; } $args = wp_parse_args( $args, array( 'country' => 'billing' === $tax_based_on ? $this->get_billing_country() : $this->get_shipping_country(), 'state' => 'billing' === $tax_based_on ? $this->get_billing_state() : $this->get_shipping_state(), 'postcode' => 'billing' === $tax_based_on ? $this->get_billing_postcode() : $this->get_shipping_postcode(), 'city' => 'billing' === $tax_based_on ? $this->get_billing_city() : $this->get_shipping_city(), ) ); // Default to base. if ( 'base' === $tax_based_on || empty( $args['country'] ) ) { $args['country'] = WC()->countries->get_base_country(); $args['state'] = WC()->countries->get_base_state(); $args['postcode'] = WC()->countries->get_base_postcode(); $args['city'] = WC()->countries->get_base_city(); } return apply_filters( 'woocommerce_order_get_tax_location', $args, $this ); } /** * Get tax rates for an order. Use order's shipping or billing address, defaults to base location. * * @param string $tax_class Tax class to get rates for. * @param array $location_args Location to compute rates for. Should be in form: array( country, state, postcode, city). * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. * * @return mixed|void Tax rates. */ protected function get_tax_rates( $tax_class, $location_args = array(), $customer = null ) { $tax_location = $this->get_tax_location( $location_args ); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer ); } /** * Calculate taxes for all line items and shipping, and store the totals and tax rows. * * If by default the taxes are based on the shipping address and the current order doesn't * have any, it would use the billing address rather than using the Shopping base location. * * Will use the base country unless customer addresses are set. * * @param array $args Added in 3.0.0 to pass things like location. */ public function calculate_taxes( $args = array() ) { do_action( 'woocommerce_order_before_calculate_taxes', $args, $this ); $calculate_tax_for = $this->get_tax_location( $args ); $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); if ( 'inherit' === $shipping_tax_class ) { $found_classes = array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() ); $shipping_tax_class = count( $found_classes ) ? current( $found_classes ) : false; } $is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this ); // Trigger tax recalculation for all items. foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { if ( ! $is_vat_exempt ) { $item->calculate_taxes( $calculate_tax_for ); } else { $item->set_taxes( false ); } } foreach ( $this->get_shipping_methods() as $item_id => $item ) { if ( false !== $shipping_tax_class && ! $is_vat_exempt ) { $item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) ); } else { $item->set_taxes( false ); } } $this->update_taxes(); } /** * Calculate fees for all line items. * * @return float Fee total. */ public function get_total_fees() { return array_reduce( $this->get_fees(), function( $carry, $item ) { return $carry + $item->get_total(); } ); } /** * Update tax lines for the order based on the line item taxes themselves. */ public function update_taxes() { $cart_taxes = array(); $shipping_taxes = array(); $existing_taxes = $this->get_taxes(); $saved_rate_ids = array(); foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $tax_amount = (float) $this->round_line_tax( $tax, false ); $cart_taxes[ $tax_rate_id ] = isset( $cart_taxes[ $tax_rate_id ] ) ? (float) $cart_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; } } foreach ( $this->get_shipping_methods() as $item_id => $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $tax_amount = (float) $tax; if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $tax_amount = wc_round_tax_total( $tax_amount ); } $shipping_taxes[ $tax_rate_id ] = isset( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; } } foreach ( $existing_taxes as $tax ) { // Remove taxes which no longer exist for cart/shipping. if ( ( ! array_key_exists( $tax->get_rate_id(), $cart_taxes ) && ! array_key_exists( $tax->get_rate_id(), $shipping_taxes ) ) || in_array( $tax->get_rate_id(), $saved_rate_ids, true ) ) { $this->remove_item( $tax->get_id() ); continue; } $saved_rate_ids[] = $tax->get_rate_id(); $tax->set_rate( $tax->get_rate_id() ); $tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 ); $tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) ); $tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 ); $tax->save(); } $new_rate_ids = wp_parse_id_list( array_diff( array_keys( $cart_taxes + $shipping_taxes ), $saved_rate_ids ) ); // New taxes. foreach ( $new_rate_ids as $tax_rate_id ) { $item = new WC_Order_Item_Tax(); $item->set_rate( $tax_rate_id ); $item->set_tax_total( isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] : 0 ); $item->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 ); $this->add_item( $item ); } $this->set_shipping_tax( array_sum( $shipping_taxes ) ); $this->set_cart_tax( array_sum( $cart_taxes ) ); $this->save(); } /** * Helper function. * If you add all items in this order in cart again, this would be the cart subtotal (assuming all other settings are same). * * @return float Cart subtotal. */ protected function get_cart_subtotal_for_order() { return wc_remove_number_precision( $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) ) ); } /** * Helper function. * If you add all items in this order in cart again, this would be the cart total (assuming all other settings are same). * * @return float Cart total. */ protected function get_cart_total_for_order() { return wc_remove_number_precision( $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) ) ); } /** * Calculate totals by looking at the contents of the order. Stores the totals and returns the orders final total. * * @since 2.2 * @param bool $and_taxes Calc taxes if true. * @return float calculated grand total. */ public function calculate_totals( $and_taxes = true ) { do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this ); $fees_total = 0; $shipping_total = 0; $cart_subtotal_tax = 0; $cart_total_tax = 0; $cart_subtotal = $this->get_cart_subtotal_for_order(); $cart_total = $this->get_cart_total_for_order(); // Sum shipping costs. foreach ( $this->get_shipping_methods() as $shipping ) { $shipping_total += NumberUtil::round( $shipping->get_total(), wc_get_price_decimals() ); } $this->set_shipping_total( $shipping_total ); // Sum fee costs. foreach ( $this->get_fees() as $item ) { $fee_total = $item->get_total(); if ( 0 > $fee_total ) { $max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, wc_get_price_decimals() ) * -1; if ( $fee_total < $max_discount && 0 > $max_discount ) { $item->set_total( $max_discount ); } } $fees_total += $item->get_total(); } // Calculate taxes for items, shipping, discounts. Note; this also triggers save(). if ( $and_taxes ) { $this->calculate_taxes(); } // Sum taxes again so we can work out how much tax was discounted. This uses original values, not those possibly rounded to 2dp. foreach ( $this->get_items() as $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $cart_total_tax += (float) $tax; } foreach ( $taxes['subtotal'] as $tax_rate_id => $tax ) { $cart_subtotal_tax += (float) $tax; } } $this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, wc_get_price_decimals() ) ); $this->set_discount_tax( wc_round_tax_total( $cart_subtotal_tax - $cart_total_tax ) ); $this->set_total( NumberUtil::round( $cart_total + $fees_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() ) ); do_action( 'woocommerce_order_after_calculate_totals', $and_taxes, $this ); $this->save(); return $this->get_total(); } /** * Get item subtotal - this is the cost before discount. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_item_subtotal( $item, $inc_tax = false, $round = true ) { $subtotal = 0; if ( is_callable( array( $item, 'get_subtotal' ) ) && $item->get_quantity() ) { if ( $inc_tax ) { $subtotal = ( $item->get_subtotal() + $item->get_subtotal_tax() ) / $item->get_quantity(); } else { $subtotal = floatval( $item->get_subtotal() ) / $item->get_quantity(); } $subtotal = $round ? number_format( (float) $subtotal, wc_get_price_decimals(), '.', '' ) : $subtotal; } return apply_filters( 'woocommerce_order_amount_item_subtotal', $subtotal, $this, $item, $inc_tax, $round ); } /** * Get line subtotal - this is the cost before discount. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_line_subtotal( $item, $inc_tax = false, $round = true ) { $subtotal = 0; if ( is_callable( array( $item, 'get_subtotal' ) ) ) { if ( $inc_tax ) { $subtotal = $item->get_subtotal() + $item->get_subtotal_tax(); } else { $subtotal = $item->get_subtotal(); } $subtotal = $round ? NumberUtil::round( $subtotal, wc_get_price_decimals() ) : $subtotal; } return apply_filters( 'woocommerce_order_amount_line_subtotal', $subtotal, $this, $item, $inc_tax, $round ); } /** * Calculate item cost - useful for gateways. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_item_total( $item, $inc_tax = false, $round = true ) { $total = 0; if ( is_callable( array( $item, 'get_total' ) ) && $item->get_quantity() ) { if ( $inc_tax ) { $total = ( $item->get_total() + $item->get_total_tax() ) / $item->get_quantity(); } else { $total = floatval( $item->get_total() ) / $item->get_quantity(); } $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; } return apply_filters( 'woocommerce_order_amount_item_total', $total, $this, $item, $inc_tax, $round ); } /** * Calculate line total - useful for gateways. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_line_total( $item, $inc_tax = false, $round = true ) { $total = 0; if ( is_callable( array( $item, 'get_total' ) ) ) { // Check if we need to add line tax to the line total. $total = $inc_tax ? $item->get_total() + $item->get_total_tax() : $item->get_total(); // Check if we need to round. $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; } return apply_filters( 'woocommerce_order_amount_line_total', $total, $this, $item, $inc_tax, $round ); } /** * Get item tax - useful for gateways. * * @param mixed $item Item to get total from. * @param bool $round (default: true). * @return float */ public function get_item_tax( $item, $round = true ) { $tax = 0; if ( is_callable( array( $item, 'get_total_tax' ) ) && $item->get_quantity() ) { $tax = $item->get_total_tax() / $item->get_quantity(); $tax = $round ? wc_round_tax_total( $tax ) : $tax; } return apply_filters( 'woocommerce_order_amount_item_tax', $tax, $item, $round, $this ); } /** * Get line tax - useful for gateways. * * @param mixed $item Item to get total from. * @return float */ public function get_line_tax( $item ) { return apply_filters( 'woocommerce_order_amount_line_tax', is_callable( array( $item, 'get_total_tax' ) ) ? wc_round_tax_total( $item->get_total_tax() ) : 0, $item, $this ); } /** * Gets line subtotal - formatted for display. * * @param object $item Item to get total from. * @param string $tax_display Incl or excl tax display mode. * @return string */ public function get_formatted_line_subtotal( $item, $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); if ( 'excl' === $tax_display ) { $ex_tax_label = $this->get_prices_include_tax() ? 1 : 0; $subtotal = wc_price( $this->get_line_subtotal( $item ), array( 'ex_tax_label' => $ex_tax_label, 'currency' => $this->get_currency(), ) ); } else { $subtotal = wc_price( $this->get_line_subtotal( $item, true ), array( 'currency' => $this->get_currency() ) ); } return apply_filters( 'woocommerce_order_formatted_line_subtotal', $subtotal, $item, $this ); } /** * Gets order total - formatted for display. * * @return string */ public function get_formatted_order_total() { $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this ); } /** * Gets subtotal - subtotal is shown before discounts, but with localised taxes. * * @param bool $compound (default: false). * @param string $tax_display (default: the tax_display_cart value). * @return string */ public function get_subtotal_to_display( $compound = false, $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); $subtotal = $this->get_cart_subtotal_for_order(); if ( ! $compound ) { if ( 'incl' === $tax_display ) { $subtotal_taxes = 0; foreach ( $this->get_items() as $item ) { $subtotal_taxes += self::round_line_tax( $item->get_subtotal_tax(), false ); } $subtotal += wc_round_tax_total( $subtotal_taxes ); } $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); if ( 'excl' === $tax_display && $this->get_prices_include_tax() && wc_tax_enabled() ) { $subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } } else { if ( 'incl' === $tax_display ) { return ''; } // Add Shipping Costs. $subtotal += $this->get_shipping_total(); // Remove non-compound taxes. foreach ( $this->get_taxes() as $tax ) { if ( $tax->is_compound() ) { continue; } $subtotal = $subtotal + $tax->get_tax_total() + $tax->get_shipping_tax_total(); } // Remove discounts. $subtotal = $subtotal - $this->get_total_discount(); $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); } return apply_filters( 'woocommerce_order_subtotal_to_display', $subtotal, $compound, $this ); } /** * Gets shipping (formatted). * * @param string $tax_display Excl or incl tax display mode. * @return string */ public function get_shipping_to_display( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); if ( 0 < abs( (float) $this->get_shipping_total() ) ) { if ( 'excl' === $tax_display ) { // Show shipping excluding tax. $shipping = wc_price( $this->get_shipping_total(), array( 'currency' => $this->get_currency() ) ); if ( (float) $this->get_shipping_tax() > 0 && $this->get_prices_include_tax() ) { $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>', $this, $tax_display ); } } else { // Show shipping including tax. $shipping = wc_price( $this->get_shipping_total() + $this->get_shipping_tax(), array( 'currency' => $this->get_currency() ) ); if ( (float) $this->get_shipping_tax() > 0 && ! $this->get_prices_include_tax() ) { $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>', $this, $tax_display ); } } /* translators: %s: method */ $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_shipped_via', ' <small class="shipped_via">' . sprintf( __( 'via %s', 'woocommerce' ), $this->get_shipping_method() ) . '</small>', $this ); } elseif ( $this->get_shipping_method() ) { $shipping = $this->get_shipping_method(); } else { $shipping = __( 'Free!', 'woocommerce' ); } return apply_filters( 'woocommerce_order_shipping_to_display', $shipping, $this, $tax_display ); } /** * Get the discount amount (formatted). * * @since 2.3.0 * @param string $tax_display Excl or incl tax display mode. * @return string */ public function get_discount_to_display( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display && 'excl' === get_option( 'woocommerce_tax_display_cart' ) ), array( 'currency' => $this->get_currency() ) ), $this ); } /** * Add total row for subtotal. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_subtotal_row( &$total_rows, $tax_display ) { $subtotal = $this->get_subtotal_to_display( false, $tax_display ); if ( $subtotal ) { $total_rows['cart_subtotal'] = array( 'label' => __( 'Subtotal:', 'woocommerce' ), 'value' => $subtotal, ); } } /** * Add total row for discounts. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_discount_row( &$total_rows, $tax_display ) { if ( $this->get_total_discount() > 0 ) { $total_rows['discount'] = array( 'label' => __( 'Discount:', 'woocommerce' ), 'value' => '-' . $this->get_discount_to_display( $tax_display ), ); } } /** * Add total row for shipping. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_shipping_row( &$total_rows, $tax_display ) { if ( $this->get_shipping_method() ) { $total_rows['shipping'] = array( 'label' => __( 'Shipping:', 'woocommerce' ), 'value' => $this->get_shipping_to_display( $tax_display ), ); } } /** * Add total row for fees. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_fee_rows( &$total_rows, $tax_display ) { $fees = $this->get_fees(); if ( $fees ) { foreach ( $fees as $id => $fee ) { if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) { continue; } $total_rows[ 'fee_' . $fee->get_id() ] = array( 'label' => $fee->get_name() . ':', 'value' => wc_price( 'excl' === $tax_display ? $fee->get_total() : $fee->get_total() + $fee->get_total_tax(), array( 'currency' => $this->get_currency() ) ), ); } } } /** * Add total row for taxes. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_tax_rows( &$total_rows, $tax_display ) { // Tax for tax exclusive prices. if ( 'excl' === $tax_display && wc_tax_enabled() ) { if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { foreach ( $this->get_tax_totals() as $code => $tax ) { $total_rows[ sanitize_title( $code ) ] = array( 'label' => $tax->label . ':', 'value' => $tax->formatted_amount, ); } } else { $total_rows['tax'] = array( 'label' => WC()->countries->tax_or_vat() . ':', 'value' => wc_price( $this->get_total_tax(), array( 'currency' => $this->get_currency() ) ), ); } } } /** * Add total row for grand total. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_total_row( &$total_rows, $tax_display ) { $total_rows['order_total'] = array( 'label' => __( 'Total:', 'woocommerce' ), 'value' => $this->get_formatted_order_total( $tax_display ), ); } /** * Get totals for display on pages and in emails. * * @param mixed $tax_display Excl or incl tax display mode. * @return array */ public function get_order_item_totals( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); $total_rows = array(); $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); $this->add_order_item_totals_total_row( $total_rows, $tax_display ); return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- | | Checks if a condition is true or false. | */ /** * Checks the order status against a passed in status. * * @param array|string $status Status to check. * @return bool */ public function has_status( $status ) { return apply_filters( 'woocommerce_order_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status, true ) ) || $this->get_status() === $status, $this, $status ); } /** * Check whether this order has a specific shipping method or not. * * @param string $method_id Method ID to check. * @return bool */ public function has_shipping_method( $method_id ) { foreach ( $this->get_shipping_methods() as $shipping_method ) { if ( strpos( $shipping_method->get_method_id(), $method_id ) === 0 ) { return true; } } return false; } /** * Returns true if the order contains a free product. * * @since 2.5.0 * @return bool */ public function has_free_item() { foreach ( $this->get_items() as $item ) { if ( ! $item->get_total() ) { return true; } } return false; } } includes/abstracts/abstract-wc-shipping-method.php 0000644 00000037504 15132754524 0016401 0 ustar 00 <?php /** * Abstract shipping method * * @class WC_Shipping_Method * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WooCommerce Shipping Method Class. * * Extended by shipping methods to handle shipping calculations etc. * * @class WC_Shipping_Method * @version 3.0.0 * @package WooCommerce\Abstracts */ abstract class WC_Shipping_Method extends WC_Settings_API { /** * Features this method supports. Possible features used by core: * - shipping-zones Shipping zone functionality + instances * - instance-settings Instance settings screens. * - settings Non-instance settings screens. Enabled by default for BW compatibility with methods before instances existed. * - instance-settings-modal Allows the instance settings to be loaded within a modal in the zones UI. * * @var array */ public $supports = array( 'settings' ); /** * Unique ID for the shipping method - must be set. * * @var string */ public $id = ''; /** * Method title. * * @var string */ public $method_title = ''; /** * Method description. * * @var string */ public $method_description = ''; /** * Yes or no based on whether the method is enabled. * * @var string */ public $enabled = 'yes'; /** * Shipping method title for the frontend. * * @var string */ public $title; /** * This is an array of rates - methods must populate this array to register shipping costs. * * @var array */ public $rates = array(); /** * If 'taxable' tax will be charged for this method (if applicable). * * @var string */ public $tax_status = 'taxable'; /** * Fee for the method (if applicable). * * @var string */ public $fee = null; /** * Minimum fee for the method (if applicable). * * @var string */ public $minimum_fee = null; /** * Instance ID if used. * * @var int */ public $instance_id = 0; /** * Instance form fields. * * @var array */ public $instance_form_fields = array(); /** * Instance settings. * * @var array */ public $instance_settings = array(); /** * Availability - legacy. Used for method Availability. * No longer useful for instance based shipping methods. * * @deprecated 2.6.0 * @var string */ public $availability; /** * Availability countries - legacy. Used for method Availability. * No longer useful for instance based shipping methods. * * @deprecated 2.6.0 * @var array */ public $countries = array(); /** * Constructor. * * @param int $instance_id Instance ID. */ public function __construct( $instance_id = 0 ) { $this->instance_id = absint( $instance_id ); } /** * Check if a shipping method supports a given feature. * * Methods should override this to declare support (or lack of support) for a feature. * * @param string $feature The name of a feature to test support for. * @return bool True if the shipping method supports the feature, false otherwise. */ public function supports( $feature ) { return apply_filters( 'woocommerce_shipping_method_supports', in_array( $feature, $this->supports ), $feature, $this ); } /** * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. * * @param array $package Package array. */ public function calculate_shipping( $package = array() ) {} /** * Whether or not we need to calculate tax on top of the shipping rate. * * @return boolean */ public function is_taxable() { return wc_tax_enabled() && 'taxable' === $this->tax_status && ( WC()->customer && ! WC()->customer->get_is_vat_exempt() ); } /** * Whether or not this method is enabled in settings. * * @since 2.6.0 * @return boolean */ public function is_enabled() { return 'yes' === $this->enabled; } /** * Return the shipping method instance ID. * * @since 2.6.0 * @return int */ public function get_instance_id() { return $this->instance_id; } /** * Return the shipping method title. * * @since 2.6.0 * @return string */ public function get_method_title() { return apply_filters( 'woocommerce_shipping_method_title', $this->method_title, $this ); } /** * Return the shipping method description. * * @since 2.6.0 * @return string */ public function get_method_description() { return apply_filters( 'woocommerce_shipping_method_description', $this->method_description, $this ); } /** * Return the shipping title which is user set. * * @return string */ public function get_title() { return apply_filters( 'woocommerce_shipping_method_title', $this->title, $this->id ); } /** * Return calculated rates for a package. * * @since 2.6.0 * @param array $package Package array. * @return array */ public function get_rates_for_package( $package ) { $this->rates = array(); if ( $this->is_available( $package ) && ( empty( $package['ship_via'] ) || in_array( $this->id, $package['ship_via'] ) ) ) { $this->calculate_shipping( $package ); } return $this->rates; } /** * Returns a rate ID based on this methods ID and instance, with an optional * suffix if distinguishing between multiple rates. * * @since 2.6.0 * @param string $suffix Suffix. * @return string */ public function get_rate_id( $suffix = '' ) { $rate_id = array( $this->id ); if ( $this->instance_id ) { $rate_id[] = $this->instance_id; } if ( $suffix ) { $rate_id[] = $suffix; } return implode( ':', $rate_id ); } /** * Add a shipping rate. If taxes are not set they will be calculated based on cost. * * @param array $args Arguments (default: array()). */ public function add_rate( $args = array() ) { $args = apply_filters( 'woocommerce_shipping_method_add_rate_args', wp_parse_args( $args, array( 'id' => $this->get_rate_id(), // ID for the rate. If not passed, this id:instance default will be used. 'label' => '', // Label for the rate. 'cost' => '0', // Amount or array of costs (per item shipping). 'taxes' => '', // Pass taxes, or leave empty to have it calculated for you, or 'false' to disable calculations. 'calc_tax' => 'per_order', // Calc tax per_order or per_item. Per item needs an array of costs. 'meta_data' => array(), // Array of misc meta data to store along with this rate - key value pairs. 'package' => false, // Package array this rate was generated for @since 2.6.0. 'price_decimals' => wc_get_price_decimals(), ) ), $this ); // ID and label are required. if ( ! $args['id'] || ! $args['label'] ) { return; } // Total up the cost. $total_cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost']; $taxes = $args['taxes']; // Taxes - if not an array and not set to false, calc tax based on cost and passed calc_tax variable. This saves shipping methods having to do complex tax calculations. if ( ! is_array( $taxes ) && false !== $taxes && $total_cost > 0 && $this->is_taxable() ) { $taxes = 'per_item' === $args['calc_tax'] ? $this->get_taxes_per_item( $args['cost'] ) : WC_Tax::calc_shipping_tax( $total_cost, WC_Tax::get_shipping_tax_rates() ); } // Round the total cost after taxes have been calculated. $total_cost = wc_format_decimal( $total_cost, $args['price_decimals'] ); // Create rate object. $rate = new WC_Shipping_Rate(); $rate->set_id( $args['id'] ); $rate->set_method_id( $this->id ); $rate->set_instance_id( $this->instance_id ); $rate->set_label( $args['label'] ); $rate->set_cost( $total_cost ); $rate->set_taxes( $taxes ); if ( ! empty( $args['meta_data'] ) ) { foreach ( $args['meta_data'] as $key => $value ) { $rate->add_meta_data( $key, $value ); } } // Store package data. if ( $args['package'] ) { $items_in_package = array(); foreach ( $args['package']['contents'] as $item ) { $product = $item['data']; $items_in_package[] = $product->get_name() . ' × ' . $item['quantity']; } $rate->add_meta_data( __( 'Items', 'woocommerce' ), implode( ', ', $items_in_package ) ); } $this->rates[ $args['id'] ] = apply_filters( 'woocommerce_shipping_method_add_rate', $rate, $args, $this ); } /** * Calc taxes per item being shipping in costs array. * * @since 2.6.0 * @param array $costs Costs. * @return array of taxes */ protected function get_taxes_per_item( $costs ) { $taxes = array(); // If we have an array of costs we can look up each items tax class and add tax accordingly. if ( is_array( $costs ) ) { $cart = WC()->cart->get_cart(); foreach ( $costs as $cost_key => $amount ) { if ( ! isset( $cart[ $cost_key ] ) ) { continue; } $item_taxes = WC_Tax::calc_shipping_tax( $amount, WC_Tax::get_shipping_tax_rates( $cart[ $cost_key ]['data']->get_tax_class() ) ); // Sum the item taxes. foreach ( array_keys( $taxes + $item_taxes ) as $key ) { $taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 ); } } // Add any cost for the order - order costs are in the key 'order'. if ( isset( $costs['order'] ) ) { $item_taxes = WC_Tax::calc_shipping_tax( $costs['order'], WC_Tax::get_shipping_tax_rates() ); // Sum the item taxes. foreach ( array_keys( $taxes + $item_taxes ) as $key ) { $taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 ); } } } return $taxes; } /** * Is this method available? * * @param array $package Package. * @return bool */ public function is_available( $package ) { $available = $this->is_enabled(); // Country availability (legacy, for non-zone based methods). if ( ! $this->instance_id && $available ) { $countries = is_array( $this->countries ) ? $this->countries : array(); switch ( $this->availability ) { case 'specific': case 'including': $available = in_array( $package['destination']['country'], array_intersect( $countries, array_keys( WC()->countries->get_shipping_countries() ) ) ); break; case 'excluding': $available = in_array( $package['destination']['country'], array_diff( array_keys( WC()->countries->get_shipping_countries() ), $countries ) ); break; default: $available = in_array( $package['destination']['country'], array_keys( WC()->countries->get_shipping_countries() ) ); break; } } return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $available, $package, $this ); } /** * Get fee to add to shipping cost. * * @param string|float $fee Fee. * @param float $total Total. * @return float */ public function get_fee( $fee, $total ) { if ( strstr( $fee, '%' ) ) { $fee = ( $total / 100 ) * str_replace( '%', '', $fee ); } if ( ! empty( $this->minimum_fee ) && $this->minimum_fee > $fee ) { $fee = $this->minimum_fee; } return $fee; } /** * Does this method have a settings page? * * @return bool */ public function has_settings() { return $this->instance_id ? $this->supports( 'instance-settings' ) : $this->supports( 'settings' ); } /** * Return admin options as a html string. * * @return string */ public function get_admin_options_html() { if ( $this->instance_id ) { $settings_html = $this->generate_settings_html( $this->get_instance_form_fields(), false ); } else { $settings_html = $this->generate_settings_html( $this->get_form_fields(), false ); } return '<table class="form-table">' . $settings_html . '</table>'; } /** * Output the shipping settings screen. */ public function admin_options() { if ( ! $this->instance_id ) { echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>'; } echo wp_kses_post( wpautop( $this->get_method_description() ) ); echo $this->get_admin_options_html(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } /** * Get_option function. * * Gets and option from the settings API, using defaults if necessary to prevent undefined notices. * * @param string $key Key. * @param mixed $empty_value Empty value. * @return mixed The value specified for the option or a default value for the option. */ public function get_option( $key, $empty_value = null ) { // Instance options take priority over global options. if ( $this->instance_id && array_key_exists( $key, $this->get_instance_form_fields() ) ) { return $this->get_instance_option( $key, $empty_value ); } // Return global option. $option = apply_filters( 'woocommerce_shipping_' . $this->id . '_option', parent::get_option( $key, $empty_value ), $key, $this ); return $option; } /** * Gets an option from the settings API, using defaults if necessary to prevent undefined notices. * * @param string $key Key. * @param mixed $empty_value Empty value. * @return mixed The value specified for the option or a default value for the option. */ public function get_instance_option( $key, $empty_value = null ) { if ( empty( $this->instance_settings ) ) { $this->init_instance_settings(); } // Get option default if unset. if ( ! isset( $this->instance_settings[ $key ] ) ) { $form_fields = $this->get_instance_form_fields(); $this->instance_settings[ $key ] = $this->get_field_default( $form_fields[ $key ] ); } if ( ! is_null( $empty_value ) && '' === $this->instance_settings[ $key ] ) { $this->instance_settings[ $key ] = $empty_value; } $instance_option = apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_option', $this->instance_settings[ $key ], $key, $this ); return $instance_option; } /** * Get settings fields for instances of this shipping method (within zones). * Should be overridden by shipping methods to add options. * * @since 2.6.0 * @return array */ public function get_instance_form_fields() { return apply_filters( 'woocommerce_shipping_instance_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->instance_form_fields ) ); } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_instance_option_key() { return $this->instance_id ? $this->plugin_id . $this->id . '_' . $this->instance_id . '_settings' : ''; } /** * Initialise Settings for instances. * * @since 2.6.0 */ public function init_instance_settings() { $this->instance_settings = get_option( $this->get_instance_option_key(), null ); // If there are no settings defined, use defaults. if ( ! is_array( $this->instance_settings ) ) { $form_fields = $this->get_instance_form_fields(); $this->instance_settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) ); } } /** * Processes and saves global shipping method options in the admin area. * * This method is usually attached to woocommerce_update_options_x hooks. * * @since 2.6.0 * @return bool was anything saved? */ public function process_admin_options() { if ( ! $this->instance_id ) { return parent::process_admin_options(); } // Check we are processing the correct form for this instance. if ( ! isset( $_REQUEST['instance_id'] ) || absint( $_REQUEST['instance_id'] ) !== $this->instance_id ) { // WPCS: input var ok, CSRF ok. return false; } $this->init_instance_settings(); $post_data = $this->get_post_data(); foreach ( $this->get_instance_form_fields() as $key => $field ) { if ( 'title' !== $this->get_field_type( $field ) ) { try { $this->instance_settings[ $key ] = $this->get_field_value( $key, $field, $post_data ); } catch ( Exception $e ) { $this->add_error( $e->getMessage() ); } } } return update_option( $this->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_settings_values', $this->instance_settings, $this ), 'yes' ); } } includes/abstracts/abstract-wc-payment-gateway.php 0000644 00000033542 15132754524 0016414 0 ustar 00 <?php /** * Abstract payment gateway * * Hanldes generic payment gateway functionality which is extended by idividual payment gateways. * * @class WC_Payment_Gateway * @version 2.1.0 * @package WooCommerce\Abstracts */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WooCommerce Payment Gateway class. * * Extended by individual payment gateways to handle payments. * * @class WC_Payment_Gateway * @extends WC_Settings_API * @version 2.1.0 * @package WooCommerce\Abstracts */ abstract class WC_Payment_Gateway extends WC_Settings_API { /** * Set if the place order button should be renamed on selection. * * @var string */ public $order_button_text; /** * Yes or no based on whether the method is enabled. * * @var string */ public $enabled = 'yes'; /** * Payment method title for the frontend. * * @var string */ public $title; /** * Payment method description for the frontend. * * @var string */ public $description; /** * Chosen payment method id. * * @var bool */ public $chosen; /** * Gateway title. * * @var string */ public $method_title = ''; /** * Gateway description. * * @var string */ public $method_description = ''; /** * True if the gateway shows fields on the checkout. * * @var bool */ public $has_fields; /** * Countries this gateway is allowed for. * * @var array */ public $countries; /** * Available for all counties or specific. * * @var string */ public $availability; /** * Icon for the gateway. * * @var string */ public $icon; /** * Supported features such as 'default_credit_card_form', 'refunds'. * * @var array */ public $supports = array( 'products' ); /** * Maximum transaction amount, zero does not define a maximum. * * @var int */ public $max_amount = 0; /** * Optional URL to view a transaction. * * @var string */ public $view_transaction_url = ''; /** * Optional label to show for "new payment method" in the payment * method/token selection radio selection. * * @var string */ public $new_method_label = ''; /** * Pay button ID if supported. * * @var string */ public $pay_button_id = ''; /** * Contains a users saved tokens for this gateway. * * @var array */ protected $tokens = array(); /** * Returns a users saved tokens for this gateway. * * @since 2.6.0 * @return array */ public function get_tokens() { if ( count( $this->tokens ) > 0 ) { return $this->tokens; } if ( is_user_logged_in() && $this->supports( 'tokenization' ) ) { $this->tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), $this->id ); } return $this->tokens; } /** * Return the title for admin screens. * * @return string */ public function get_method_title() { return apply_filters( 'woocommerce_gateway_method_title', $this->method_title, $this ); } /** * Return the description for admin screens. * * @return string */ public function get_method_description() { return apply_filters( 'woocommerce_gateway_method_description', $this->method_description, $this ); } /** * Output the gateway settings screen. */ public function admin_options() { echo '<h2>' . esc_html( $this->get_method_title() ); wc_back_link( __( 'Return to payments', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ); echo '</h2>'; echo wp_kses_post( wpautop( $this->get_method_description() ) ); parent::admin_options(); } /** * Init settings for gateways. */ public function init_settings() { parent::init_settings(); $this->enabled = ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'] ? 'yes' : 'no'; } /** * Return whether or not this gateway still requires setup to function. * * When this gateway is toggled on via AJAX, if this returns true a * redirect will occur to the settings page instead. * * @since 3.4.0 * @return bool */ public function needs_setup() { return false; } /** * Get the return url (thank you page). * * @param WC_Order|null $order Order object. * @return string */ public function get_return_url( $order = null ) { if ( $order ) { $return_url = $order->get_checkout_order_received_url(); } else { $return_url = wc_get_endpoint_url( 'order-received', '', wc_get_checkout_url() ); } return apply_filters( 'woocommerce_get_return_url', $return_url, $order ); } /** * Get a link to the transaction on the 3rd party gateway site (if applicable). * * @param WC_Order $order the order object. * @return string transaction URL, or empty string. */ public function get_transaction_url( $order ) { $return_url = ''; $transaction_id = $order->get_transaction_id(); if ( ! empty( $this->view_transaction_url ) && ! empty( $transaction_id ) ) { $return_url = sprintf( $this->view_transaction_url, $transaction_id ); } return apply_filters( 'woocommerce_get_transaction_url', $return_url, $order, $this ); } /** * Get the order total in checkout and pay_for_order. * * @return float */ protected function get_order_total() { $total = 0; $order_id = absint( get_query_var( 'order-pay' ) ); // Gets order total from "pay for order" page. if ( 0 < $order_id ) { $order = wc_get_order( $order_id ); if ( $order ) { $total = (float) $order->get_total(); } // Gets order total from cart/checkout. } elseif ( 0 < WC()->cart->total ) { $total = (float) WC()->cart->total; } return $total; } /** * Check if the gateway is available for use. * * @return bool */ public function is_available() { $is_available = ( 'yes' === $this->enabled ); if ( WC()->cart && 0 < $this->get_order_total() && 0 < $this->max_amount && $this->max_amount < $this->get_order_total() ) { $is_available = false; } return $is_available; } /** * Check if the gateway has fields on the checkout. * * @return bool */ public function has_fields() { return (bool) $this->has_fields; } /** * Return the gateway's title. * * @return string */ public function get_title() { return apply_filters( 'woocommerce_gateway_title', $this->title, $this->id ); } /** * Return the gateway's description. * * @return string */ public function get_description() { return apply_filters( 'woocommerce_gateway_description', $this->description, $this->id ); } /** * Return the gateway's icon. * * @return string */ public function get_icon() { $icon = $this->icon ? '<img src="' . WC_HTTPS::force_https_url( $this->icon ) . '" alt="' . esc_attr( $this->get_title() ) . '" />' : ''; return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id ); } /** * Return the gateway's pay button ID. * * @since 3.9.0 * @return string */ public function get_pay_button_id() { return sanitize_html_class( $this->pay_button_id ); } /** * Set as current gateway. * * Set this as the current gateway. */ public function set_current() { $this->chosen = true; } /** * Process Payment. * * Process the payment. Override this in your gateway. When implemented, this should. * return the success and redirect in an array. e.g: * * return array( * 'result' => 'success', * 'redirect' => $this->get_return_url( $order ) * ); * * @param int $order_id Order ID. * @return array */ public function process_payment( $order_id ) { return array(); } /** * Process refund. * * If the gateway declares 'refunds' support, this will allow it to refund. * a passed in amount. * * @param int $order_id Order ID. * @param float|null $amount Refund amount. * @param string $reason Refund reason. * @return boolean True or false based on success, or a WP_Error object. */ public function process_refund( $order_id, $amount = null, $reason = '' ) { return false; } /** * Validate frontend fields. * * Validate payment fields on the frontend. * * @return bool */ public function validate_fields() { return true; } /** * If There are no payment fields show the description if set. * Override this in your gateway if you have some. */ public function payment_fields() { $description = $this->get_description(); if ( $description ) { echo wpautop( wptexturize( $description ) ); // @codingStandardsIgnoreLine. } if ( $this->supports( 'default_credit_card_form' ) ) { $this->credit_card_form(); // Deprecated, will be removed in a future version. } } /** * Check if a gateway supports a given feature. * * Gateways should override this to declare support (or lack of support) for a feature. * For backward compatibility, gateways support 'products' by default, but nothing else. * * @param string $feature string The name of a feature to test support for. * @return bool True if the gateway supports the feature, false otherwise. * @since 1.5.7 */ public function supports( $feature ) { return apply_filters( 'woocommerce_payment_gateway_supports', in_array( $feature, $this->supports ), $feature, $this ); } /** * Can the order be refunded via this gateway? * * Should be extended by gateways to do their own checks. * * @param WC_Order $order Order object. * @return bool If false, the automatic refund button is hidden in the UI. */ public function can_refund_order( $order ) { return $order && $this->supports( 'refunds' ); } /** * Core credit card form which gateways can use if needed. Deprecated - inherit WC_Payment_Gateway_CC instead. * * @param array $args Arguments. * @param array $fields Fields. */ public function credit_card_form( $args = array(), $fields = array() ) { wc_deprecated_function( 'credit_card_form', '2.6', 'WC_Payment_Gateway_CC->form' ); $cc_form = new WC_Payment_Gateway_CC(); $cc_form->id = $this->id; $cc_form->supports = $this->supports; $cc_form->form(); } /** * Enqueues our tokenization script to handle some of the new form options. * * @since 2.6.0 */ public function tokenization_script() { wp_enqueue_script( 'woocommerce-tokenization-form', plugins_url( '/assets/js/frontend/tokenization-form' . ( Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min' ) . '.js', WC_PLUGIN_FILE ), array( 'jquery' ), WC()->version ); wp_localize_script( 'woocommerce-tokenization-form', 'wc_tokenization_form_params', array( 'is_registration_required' => WC()->checkout()->is_registration_required(), 'is_logged_in' => is_user_logged_in(), ) ); } /** * Grab and display our saved payment methods. * * @since 2.6.0 */ public function saved_payment_methods() { $html = '<ul class="woocommerce-SavedPaymentMethods wc-saved-payment-methods" data-count="' . esc_attr( count( $this->get_tokens() ) ) . '">'; foreach ( $this->get_tokens() as $token ) { $html .= $this->get_saved_payment_method_option_html( $token ); } $html .= $this->get_new_payment_method_option_html(); $html .= '</ul>'; echo apply_filters( 'wc_payment_gateway_form_saved_payment_methods_html', $html, $this ); // @codingStandardsIgnoreLine } /** * Gets saved payment method HTML from a token. * * @since 2.6.0 * @param WC_Payment_Token $token Payment Token. * @return string Generated payment method HTML */ public function get_saved_payment_method_option_html( $token ) { $html = sprintf( '<li class="woocommerce-SavedPaymentMethods-token"> <input id="wc-%1$s-payment-token-%2$s" type="radio" name="wc-%1$s-payment-token" value="%2$s" style="width:auto;" class="woocommerce-SavedPaymentMethods-tokenInput" %4$s /> <label for="wc-%1$s-payment-token-%2$s">%3$s</label> </li>', esc_attr( $this->id ), esc_attr( $token->get_id() ), esc_html( $token->get_display_name() ), checked( $token->is_default(), true, false ) ); return apply_filters( 'woocommerce_payment_gateway_get_saved_payment_method_option_html', $html, $token, $this ); } /** * Displays a radio button for entering a new payment method (new CC details) instead of using a saved method. * Only displayed when a gateway supports tokenization. * * @since 2.6.0 */ public function get_new_payment_method_option_html() { $label = apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html_label', $this->new_method_label ? $this->new_method_label : __( 'Use a new payment method', 'woocommerce' ), $this ); $html = sprintf( '<li class="woocommerce-SavedPaymentMethods-new"> <input id="wc-%1$s-payment-token-new" type="radio" name="wc-%1$s-payment-token" value="new" style="width:auto;" class="woocommerce-SavedPaymentMethods-tokenInput" /> <label for="wc-%1$s-payment-token-new">%2$s</label> </li>', esc_attr( $this->id ), esc_html( $label ) ); return apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html', $html, $this ); } /** * Outputs a checkbox for saving a new payment method to the database. * * @since 2.6.0 */ public function save_payment_method_checkbox() { $html = sprintf( '<p class="form-row woocommerce-SavedPaymentMethods-saveNew"> <input id="wc-%1$s-new-payment-method" name="wc-%1$s-new-payment-method" type="checkbox" value="true" style="width:auto;" /> <label for="wc-%1$s-new-payment-method" style="display:inline;">%2$s</label> </p>', esc_attr( $this->id ), esc_html__( 'Save to account', 'woocommerce' ) ); echo apply_filters( 'woocommerce_payment_gateway_save_new_payment_method_option_html', $html, $this ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Add payment method via account screen. This should be extended by gateway plugins. * * @since 3.2.0 Included here from 3.2.0, but supported from 3.0.0. * @return array */ public function add_payment_method() { return array( 'result' => 'failure', 'redirect' => wc_get_endpoint_url( 'payment-methods' ), ); } } includes/abstracts/abstract-wc-widget.php 0000644 00000030263 15132754524 0014560 0 ustar 00 <?php /** * Abstract widget class * * @class WC_Widget * @package WooCommerce\Abstracts */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Widget * * @package WooCommerce\Abstracts * @version 2.5.0 * @extends WP_Widget */ abstract class WC_Widget extends WP_Widget { /** * CSS class. * * @var string */ public $widget_cssclass; /** * Widget description. * * @var string */ public $widget_description; /** * Widget ID. * * @var string */ public $widget_id; /** * Widget name. * * @var string */ public $widget_name; /** * Settings. * * @var array */ public $settings; /** * Constructor. */ public function __construct() { $widget_ops = array( 'classname' => $this->widget_cssclass, 'description' => $this->widget_description, 'customize_selective_refresh' => true, 'show_instance_in_rest' => true, ); parent::__construct( $this->widget_id, $this->widget_name, $widget_ops ); add_action( 'save_post', array( $this, 'flush_widget_cache' ) ); add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) ); add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) ); } /** * Get cached widget. * * @param array $args Arguments. * @return bool true if the widget is cached otherwise false */ public function get_cached_widget( $args ) { // Don't get cache if widget_id doesn't exists. if ( empty( $args['widget_id'] ) ) { return false; } $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); if ( ! is_array( $cache ) ) { $cache = array(); } if ( isset( $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] ) ) { echo $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ]; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped return true; } return false; } /** * Cache the widget. * * @param array $args Arguments. * @param string $content Content. * @return string the content that was cached */ public function cache_widget( $args, $content ) { // Don't set any cache if widget_id doesn't exist. if ( empty( $args['widget_id'] ) ) { return $content; } $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); if ( ! is_array( $cache ) ) { $cache = array(); } $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] = $content; wp_cache_set( $this->get_widget_id_for_cache( $this->widget_id ), $cache, 'widget' ); return $content; } /** * Flush the cache. */ public function flush_widget_cache() { foreach ( array( 'https', 'http' ) as $scheme ) { wp_cache_delete( $this->get_widget_id_for_cache( $this->widget_id, $scheme ), 'widget' ); } } /** * Get this widgets title. * * @param array $instance Array of instance options. * @return string */ protected function get_instance_title( $instance ) { if ( isset( $instance['title'] ) ) { return $instance['title']; } if ( isset( $this->settings, $this->settings['title'], $this->settings['title']['std'] ) ) { return $this->settings['title']['std']; } return ''; } /** * Output the html at the start of a widget. * * @param array $args Arguments. * @param array $instance Instance. */ public function widget_start( $args, $instance ) { echo $args['before_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped $title = apply_filters( 'widget_title', $this->get_instance_title( $instance ), $instance, $this->id_base ); if ( $title ) { echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } } /** * Output the html at the end of a widget. * * @param array $args Arguments. */ public function widget_end( $args ) { echo $args['after_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } /** * Updates a particular instance of a widget. * * @see WP_Widget->update * @param array $new_instance New instance. * @param array $old_instance Old instance. * @return array */ public function update( $new_instance, $old_instance ) { $instance = $old_instance; if ( empty( $this->settings ) ) { return $instance; } // Loop settings and get values to save. foreach ( $this->settings as $key => $setting ) { if ( ! isset( $setting['type'] ) ) { continue; } // Format the value based on settings type. switch ( $setting['type'] ) { case 'number': $instance[ $key ] = absint( $new_instance[ $key ] ); if ( isset( $setting['min'] ) && '' !== $setting['min'] ) { $instance[ $key ] = max( $instance[ $key ], $setting['min'] ); } if ( isset( $setting['max'] ) && '' !== $setting['max'] ) { $instance[ $key ] = min( $instance[ $key ], $setting['max'] ); } break; case 'textarea': $instance[ $key ] = wp_kses( trim( wp_unslash( $new_instance[ $key ] ) ), wp_kses_allowed_html( 'post' ) ); break; case 'checkbox': $instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1; break; default: $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : $setting['std']; break; } /** * Sanitize the value of a setting. */ $instance[ $key ] = apply_filters( 'woocommerce_widget_settings_sanitize_option', $instance[ $key ], $new_instance, $key, $setting ); } $this->flush_widget_cache(); return $instance; } /** * Outputs the settings update form. * * @see WP_Widget->form * * @param array $instance Instance. */ public function form( $instance ) { if ( empty( $this->settings ) ) { return; } foreach ( $this->settings as $key => $setting ) { $class = isset( $setting['class'] ) ? $setting['class'] : ''; $value = isset( $instance[ $key ] ) ? $instance[ $key ] : $setting['std']; switch ( $setting['type'] ) { case 'text': ?> <p> <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo wp_kses_post( $setting['label'] ); ?></label><?php // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> <input class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="text" value="<?php echo esc_attr( $value ); ?>" /> </p> <?php break; case 'number': ?> <p> <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label> <input class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="number" step="<?php echo esc_attr( $setting['step'] ); ?>" min="<?php echo esc_attr( $setting['min'] ); ?>" max="<?php echo esc_attr( $setting['max'] ); ?>" value="<?php echo esc_attr( $value ); ?>" /> </p> <?php break; case 'select': ?> <p> <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label> <select class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>"> <?php foreach ( $setting['options'] as $option_key => $option_value ) : ?> <option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( $option_key, $value ); ?>><?php echo esc_html( $option_value ); ?></option> <?php endforeach; ?> </select> </p> <?php break; case 'textarea': ?> <p> <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label> <textarea class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" cols="20" rows="3"><?php echo esc_textarea( $value ); ?></textarea> <?php if ( isset( $setting['desc'] ) ) : ?> <small><?php echo esc_html( $setting['desc'] ); ?></small> <?php endif; ?> </p> <?php break; case 'checkbox': ?> <p> <input class="checkbox <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="checkbox" value="1" <?php checked( $value, 1 ); ?> /> <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label> </p> <?php break; // Default: run an action. default: do_action( 'woocommerce_widget_field_' . $setting['type'], $key, $value, $setting, $instance ); break; } } } /** * Get current page URL with various filtering props supported by WC. * * @return string * @since 3.3.0 */ protected function get_current_page_url() { if ( Constants::is_defined( 'SHOP_IS_ON_FRONT' ) ) { $link = home_url(); } elseif ( is_shop() ) { $link = get_permalink( wc_get_page_id( 'shop' ) ); } elseif ( is_product_category() ) { $link = get_term_link( get_query_var( 'product_cat' ), 'product_cat' ); } elseif ( is_product_tag() ) { $link = get_term_link( get_query_var( 'product_tag' ), 'product_tag' ); } else { $queried_object = get_queried_object(); $link = get_term_link( $queried_object->slug, $queried_object->taxonomy ); } // Min/Max. if ( isset( $_GET['min_price'] ) ) { $link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link ); } if ( isset( $_GET['max_price'] ) ) { $link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link ); } // Order by. if ( isset( $_GET['orderby'] ) ) { $link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link ); } /** * Search Arg. * To support quote characters, first they are decoded from " entities, then URL encoded. */ if ( get_search_query() ) { $link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link ); } // Post Type Arg. if ( isset( $_GET['post_type'] ) ) { $link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link ); // Prevent post type and page id when pretty permalinks are disabled. if ( is_shop() ) { $link = remove_query_arg( 'page_id', $link ); } } // Min Rating Arg. if ( isset( $_GET['rating_filter'] ) ) { $link = add_query_arg( 'rating_filter', wc_clean( wp_unslash( $_GET['rating_filter'] ) ), $link ); } // All current filters. if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure, WordPress.CodeAnalysis.AssignmentInCondition.Found foreach ( $_chosen_attributes as $name => $data ) { $filter_name = wc_attribute_taxonomy_slug( $name ); if ( ! empty( $data['terms'] ) ) { $link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); } if ( 'or' === $data['query_type'] ) { $link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); } } } return apply_filters( 'woocommerce_widget_get_current_page_url', $link, $this ); } /** * Get widget id plus scheme/protocol to prevent serving mixed content from (persistently) cached widgets. * * @since 3.4.0 * @param string $widget_id Id of the cached widget. * @param string $scheme Scheme for the widget id. * @return string Widget id including scheme/protocol. */ protected function get_widget_id_for_cache( $widget_id, $scheme = '' ) { if ( $scheme ) { $widget_id_for_cache = $widget_id . '-' . $scheme; } else { $widget_id_for_cache = $widget_id . '-' . ( is_ssl() ? 'https' : 'http' ); } return apply_filters( 'woocommerce_cached_widget_id', $widget_id_for_cache ); } } includes/abstracts/abstract-wc-settings-api.php 0000644 00000072434 15132754524 0015712 0 ustar 00 <?php /** * Abstract Settings API Class * * Admin Settings API used by Integrations, Shipping Methods, and Payment Gateways. * * @package WooCommerce\Abstracts */ defined( 'ABSPATH' ) || exit; /** * WC_Settings_API class. */ abstract class WC_Settings_API { /** * The plugin ID. Used for option names. * * @var string */ public $plugin_id = 'woocommerce_'; /** * ID of the class extending the settings API. Used in option names. * * @var string */ public $id = ''; /** * Validation errors. * * @var array of strings */ public $errors = array(); /** * Setting values. * * @var array */ public $settings = array(); /** * Form option fields. * * @var array */ public $form_fields = array(); /** * The posted settings data. When empty, $_POST data will be used. * * @var array */ protected $data = array(); /** * Get the form fields after they are initialized. * * @return array of options */ public function get_form_fields() { return apply_filters( 'woocommerce_settings_api_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->form_fields ) ); } /** * Set default required properties for each field. * * @param array $field Setting field array. * @return array */ protected function set_defaults( $field ) { if ( ! isset( $field['default'] ) ) { $field['default'] = ''; } return $field; } /** * Output the admin options table. */ public function admin_options() { echo '<table class="form-table">' . $this->generate_settings_html( $this->get_form_fields(), false ) . '</table>'; // WPCS: XSS ok. } /** * Initialise settings form fields. * * Add an array of fields to be displayed on the gateway's settings screen. * * @since 1.0.0 */ public function init_form_fields() {} /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . $this->id . '_settings'; } /** * Get a fields type. Defaults to "text" if not set. * * @param array $field Field key. * @return string */ public function get_field_type( $field ) { return empty( $field['type'] ) ? 'text' : $field['type']; } /** * Get a fields default value. Defaults to "" if not set. * * @param array $field Field key. * @return string */ public function get_field_default( $field ) { return empty( $field['default'] ) ? '' : $field['default']; } /** * Get a field's posted and validated value. * * @param string $key Field key. * @param array $field Field array. * @param array $post_data Posted data. * @return string */ public function get_field_value( $key, $field, $post_data = array() ) { $type = $this->get_field_type( $field ); $field_key = $this->get_field_key( $key ); $post_data = empty( $post_data ) ? $_POST : $post_data; // WPCS: CSRF ok, input var ok. $value = isset( $post_data[ $field_key ] ) ? $post_data[ $field_key ] : null; if ( isset( $field['sanitize_callback'] ) && is_callable( $field['sanitize_callback'] ) ) { return call_user_func( $field['sanitize_callback'], $value ); } // Look for a validate_FIELDID_field method for special handling. if ( is_callable( array( $this, 'validate_' . $key . '_field' ) ) ) { return $this->{'validate_' . $key . '_field'}( $key, $value ); } // Look for a validate_FIELDTYPE_field method. if ( is_callable( array( $this, 'validate_' . $type . '_field' ) ) ) { return $this->{'validate_' . $type . '_field'}( $key, $value ); } // Fallback to text. return $this->validate_text_field( $key, $value ); } /** * Sets the POSTed data. This method can be used to set specific data, instead of taking it from the $_POST array. * * @param array $data Posted data. */ public function set_post_data( $data = array() ) { $this->data = $data; } /** * Returns the POSTed data, to be used to save the settings. * * @return array */ public function get_post_data() { if ( ! empty( $this->data ) && is_array( $this->data ) ) { return $this->data; } return $_POST; // WPCS: CSRF ok, input var ok. } /** * Update a single option. * * @since 3.4.0 * @param string $key Option key. * @param mixed $value Value to set. * @return bool was anything saved? */ public function update_option( $key, $value = '' ) { if ( empty( $this->settings ) ) { $this->init_settings(); } $this->settings[ $key ] = $value; return update_option( $this->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' ); } /** * Processes and saves options. * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out. * * @return bool was anything saved? */ public function process_admin_options() { $this->init_settings(); $post_data = $this->get_post_data(); foreach ( $this->get_form_fields() as $key => $field ) { if ( 'title' !== $this->get_field_type( $field ) ) { try { $this->settings[ $key ] = $this->get_field_value( $key, $field, $post_data ); } catch ( Exception $e ) { $this->add_error( $e->getMessage() ); } } } return update_option( $this->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' ); } /** * Add an error message for display in admin on save. * * @param string $error Error message. */ public function add_error( $error ) { $this->errors[] = $error; } /** * Get admin error messages. */ public function get_errors() { return $this->errors; } /** * Display admin error messages. */ public function display_errors() { if ( $this->get_errors() ) { echo '<div id="woocommerce_errors" class="error notice is-dismissible">'; foreach ( $this->get_errors() as $error ) { echo '<p>' . wp_kses_post( $error ) . '</p>'; } echo '</div>'; } } /** * Initialise Settings. * * Store all settings in a single database entry * and make sure the $settings array is either the default * or the settings stored in the database. * * @since 1.0.0 * @uses get_option(), add_option() */ public function init_settings() { $this->settings = get_option( $this->get_option_key(), null ); // If there are no settings defined, use defaults. if ( ! is_array( $this->settings ) ) { $form_fields = $this->get_form_fields(); $this->settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) ); } } /** * Get option from DB. * * Gets an option from the settings API, using defaults if necessary to prevent undefined notices. * * @param string $key Option key. * @param mixed $empty_value Value when empty. * @return string The value specified for the option or a default value for the option. */ public function get_option( $key, $empty_value = null ) { if ( empty( $this->settings ) ) { $this->init_settings(); } // Get option default if unset. if ( ! isset( $this->settings[ $key ] ) ) { $form_fields = $this->get_form_fields(); $this->settings[ $key ] = isset( $form_fields[ $key ] ) ? $this->get_field_default( $form_fields[ $key ] ) : ''; } if ( ! is_null( $empty_value ) && '' === $this->settings[ $key ] ) { $this->settings[ $key ] = $empty_value; } return $this->settings[ $key ]; } /** * Prefix key for settings. * * @param string $key Field key. * @return string */ public function get_field_key( $key ) { return $this->plugin_id . $this->id . '_' . $key; } /** * Generate Settings HTML. * * Generate the HTML for the fields on the "settings" screen. * * @param array $form_fields (default: array()) Array of form fields. * @param bool $echo Echo or return. * @return string the html for the settings * @since 1.0.0 * @uses method_exists() */ public function generate_settings_html( $form_fields = array(), $echo = true ) { if ( empty( $form_fields ) ) { $form_fields = $this->get_form_fields(); } $html = ''; foreach ( $form_fields as $k => $v ) { $type = $this->get_field_type( $v ); if ( method_exists( $this, 'generate_' . $type . '_html' ) ) { $html .= $this->{'generate_' . $type . '_html'}( $k, $v ); } else { $html .= $this->generate_text_html( $k, $v ); } } if ( $echo ) { echo $html; // WPCS: XSS ok. } else { return $html; } } /** * Get HTML for tooltips. * * @param array $data Data for the tooltip. * @return string */ public function get_tooltip_html( $data ) { if ( true === $data['desc_tip'] ) { $tip = $data['description']; } elseif ( ! empty( $data['desc_tip'] ) ) { $tip = $data['desc_tip']; } else { $tip = ''; } return $tip ? wc_help_tip( $tip, true ) : ''; } /** * Get HTML for descriptions. * * @param array $data Data for the description. * @return string */ public function get_description_html( $data ) { if ( true === $data['desc_tip'] ) { $description = ''; } elseif ( ! empty( $data['desc_tip'] ) ) { $description = $data['description']; } elseif ( ! empty( $data['description'] ) ) { $description = $data['description']; } else { $description = ''; } return $description ? '<p class="description">' . wp_kses_post( $description ) . '</p>' . "\n" : ''; } /** * Get custom attributes. * * @param array $data Field data. * @return string */ public function get_custom_attribute_html( $data ) { $custom_attributes = array(); if ( ! empty( $data['custom_attributes'] ) && is_array( $data['custom_attributes'] ) ) { foreach ( $data['custom_attributes'] as $attribute => $attribute_value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; } } return implode( ' ', $custom_attributes ); } /** * Generate Text Input HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_text_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <input class="input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="<?php echo esc_attr( $data['type'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( $this->get_option( $key ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Price Input HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_price_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <input class="wc_input_price input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $this->get_option( $key ) ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Decimal Input HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_decimal_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <input class="wc_input_decimal input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( wc_format_localized_decimal( $this->get_option( $key ) ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Password Input HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_password_html( $key, $data ) { $data['type'] = 'password'; return $this->generate_text_html( $key, $data ); } /** * Generate Color Picker Input HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_color_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <span class="colorpickpreview" style="background:<?php echo esc_attr( $this->get_option( $key ) ); ?>;"> </span> <input class="colorpick <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( $this->get_option( $key ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <div id="colorPickerDiv_<?php echo esc_attr( $field_key ); ?>" class="colorpickdiv" style="z-index: 100; background: #eee; border: 1px solid #ccc; position: absolute; display: none;"></div> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Textarea HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_textarea_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <textarea rows="3" cols="20" class="input-text wide-input <?php echo esc_attr( $data['class'] ); ?>" type="<?php echo esc_attr( $data['type'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>><?php echo esc_textarea( $this->get_option( $key ) ); ?></textarea> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Checkbox HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_checkbox_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'label' => '', 'disabled' => false, 'class' => '', 'css' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), ); $data = wp_parse_args( $data, $defaults ); if ( ! $data['label'] ) { $data['label'] = $data['title']; } ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <label for="<?php echo esc_attr( $field_key ); ?>"> <input <?php disabled( $data['disabled'], true ); ?> class="<?php echo esc_attr( $data['class'] ); ?>" type="checkbox" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="1" <?php checked( $this->get_option( $key ), 'yes' ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <?php echo wp_kses_post( $data['label'] ); ?></label><br/> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Select HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_select_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), 'options' => array(), ); $data = wp_parse_args( $data, $defaults ); $value = $this->get_option( $key ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <select class="select <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>> <?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?> <?php if ( is_array( $option_value ) ) : ?> <optgroup label="<?php echo esc_attr( $option_key ); ?>"> <?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?> <option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( (string) $option_key_inner, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value_inner ); ?></option> <?php endforeach; ?> </optgroup> <?php else : ?> <option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value ); ?></option> <?php endif; ?> <?php endforeach; ?> </select> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Multiselect HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_multiselect_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'disabled' => false, 'class' => '', 'css' => '', 'placeholder' => '', 'type' => 'text', 'desc_tip' => false, 'description' => '', 'custom_attributes' => array(), 'select_buttons' => false, 'options' => array(), ); $data = wp_parse_args( $data, $defaults ); $value = (array) $this->get_option( $key, array() ); ob_start(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <fieldset> <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend> <select multiple="multiple" class="multiselect <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>[]" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>> <?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?> <?php if ( is_array( $option_value ) ) : ?> <optgroup label="<?php echo esc_attr( $option_key ); ?>"> <?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?> <option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( in_array( (string) $option_key_inner, $value, true ), true ); ?>><?php echo esc_html( $option_value_inner ); ?></option> <?php endforeach; ?> </optgroup> <?php else : ?> <option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( in_array( (string) $option_key, $value, true ), true ); ?>><?php echo esc_html( $option_value ); ?></option> <?php endif; ?> <?php endforeach; ?> </select> <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?> <?php if ( $data['select_buttons'] ) : ?> <br/><a class="select_all button" href="#"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a> <a class="select_none button" href="#"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></a> <?php endif; ?> </fieldset> </td> </tr> <?php return ob_get_clean(); } /** * Generate Title HTML. * * @param string $key Field key. * @param array $data Field data. * @since 1.0.0 * @return string */ public function generate_title_html( $key, $data ) { $field_key = $this->get_field_key( $key ); $defaults = array( 'title' => '', 'class' => '', ); $data = wp_parse_args( $data, $defaults ); ob_start(); ?> </table> <h3 class="wc-settings-sub-title <?php echo esc_attr( $data['class'] ); ?>" id="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?></h3> <?php if ( ! empty( $data['description'] ) ) : ?> <p><?php echo wp_kses_post( $data['description'] ); ?></p> <?php endif; ?> <table class="form-table"> <?php return ob_get_clean(); } /** * Validate Text Field. * * Make sure the data is escaped correctly, etc. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_text_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return wp_kses_post( trim( stripslashes( $value ) ) ); } /** * Validate Price Field. * * Make sure the data is escaped correctly, etc. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_price_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return ( '' === $value ) ? '' : wc_format_decimal( trim( stripslashes( $value ) ) ); } /** * Validate Decimal Field. * * Make sure the data is escaped correctly, etc. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_decimal_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return ( '' === $value ) ? '' : wc_format_decimal( trim( stripslashes( $value ) ) ); } /** * Validate Password Field. No input sanitization is used to avoid corrupting passwords. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_password_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return trim( stripslashes( $value ) ); } /** * Validate Textarea Field. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_textarea_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return wp_kses( trim( stripslashes( $value ) ), array_merge( array( 'iframe' => array( 'src' => true, 'style' => true, 'id' => true, 'class' => true, ), ), wp_kses_allowed_html( 'post' ) ) ); } /** * Validate Checkbox Field. * * If not set, return "no", otherwise return "yes". * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_checkbox_field( $key, $value ) { return ! is_null( $value ) ? 'yes' : 'no'; } /** * Validate Select Field. * * @param string $key Field key. * @param string $value Posted Value. * @return string */ public function validate_select_field( $key, $value ) { $value = is_null( $value ) ? '' : $value; return wc_clean( stripslashes( $value ) ); } /** * Validate Multiselect Field. * * @param string $key Field key. * @param string $value Posted Value. * @return string|array */ public function validate_multiselect_field( $key, $value ) { return is_array( $value ) ? array_map( 'wc_clean', array_map( 'stripslashes', $value ) ) : ''; } /** * Validate the data on the "Settings" form. * * @deprecated 2.6.0 No longer used. * @param array $form_fields Array of fields. */ public function validate_settings_fields( $form_fields = array() ) { wc_deprecated_function( 'validate_settings_fields', '2.6' ); } /** * Format settings if needed. * * @deprecated 2.6.0 Unused. * @param array $value Value to format. * @return array */ public function format_settings( $value ) { wc_deprecated_function( 'format_settings', '2.6' ); return $value; } } includes/abstracts/abstract-wc-session.php 0000644 00000004457 15132754524 0014766 0 ustar 00 <?php /** * Handle data for the current customers session * * @class WC_Session * @version 2.0.0 * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Session */ abstract class WC_Session { /** * Customer ID. * * @var int $_customer_id Customer ID. */ protected $_customer_id; /** * Session Data. * * @var array $_data Data array. */ protected $_data = array(); /** * Dirty when the session needs saving. * * @var bool $_dirty When something changes */ protected $_dirty = false; /** * Init hooks and session data. Extended by child classes. * * @since 3.3.0 */ public function init() {} /** * Cleanup session data. Extended by child classes. */ public function cleanup_sessions() {} /** * Magic get method. * * @param mixed $key Key to get. * @return mixed */ public function __get( $key ) { return $this->get( $key ); } /** * Magic set method. * * @param mixed $key Key to set. * @param mixed $value Value to set. */ public function __set( $key, $value ) { $this->set( $key, $value ); } /** * Magic isset method. * * @param mixed $key Key to check. * @return bool */ public function __isset( $key ) { return isset( $this->_data[ sanitize_title( $key ) ] ); } /** * Magic unset method. * * @param mixed $key Key to unset. */ public function __unset( $key ) { if ( isset( $this->_data[ $key ] ) ) { unset( $this->_data[ $key ] ); $this->_dirty = true; } } /** * Get a session variable. * * @param string $key Key to get. * @param mixed $default used if the session variable isn't set. * @return array|string value of session variable */ public function get( $key, $default = null ) { $key = sanitize_key( $key ); return isset( $this->_data[ $key ] ) ? maybe_unserialize( $this->_data[ $key ] ) : $default; } /** * Set a session variable. * * @param string $key Key to set. * @param mixed $value Value to set. */ public function set( $key, $value ) { if ( $value !== $this->get( $key ) ) { $this->_data[ sanitize_key( $key ) ] = maybe_serialize( $value ); $this->_dirty = true; } } /** * Get customer ID. * * @return int */ public function get_customer_id() { return $this->_customer_id; } } includes/abstracts/class-wc-background-process.php 0000644 00000011566 15132754524 0016377 0 ustar 00 <?php /** * Abstract WP_Background_Process class. * * Uses https://github.com/A5hleyRich/wp-background-processing to handle DB * updates in the background. * * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WP_Async_Request', false ) ) { include_once dirname( WC_PLUGIN_FILE ) . '/includes/libraries/wp-async-request.php'; } if ( ! class_exists( 'WP_Background_Process', false ) ) { include_once dirname( WC_PLUGIN_FILE ) . '/includes/libraries/wp-background-process.php'; } /** * WC_Background_Process class. */ abstract class WC_Background_Process extends WP_Background_Process { /** * Is queue empty. * * @return bool */ protected function is_queue_empty() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine. return ! ( $count > 0 ); } /** * Get batch. * * @return stdClass Return the first batch from the queue. */ protected function get_batch() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $query = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1", $key ) ); // @codingStandardsIgnoreLine. $batch = new stdClass(); $batch->key = $query->$column; $batch->data = array_filter( (array) maybe_unserialize( $query->$value_column ) ); return $batch; } /** * See if the batch limit has been exceeded. * * @return bool */ protected function batch_limit_exceeded() { return $this->time_exceeded() || $this->memory_exceeded(); } /** * Handle. * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } if ( $this->batch_limit_exceeded() ) { // Batch limits reached. break; } } // Update or delete current batch. if ( ! empty( $batch->data ) ) { $this->update( $batch->key, $batch->data ); } else { $this->delete( $batch->key ); } } while ( ! $this->batch_limit_exceeded() && ! $this->is_queue_empty() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } } /** * Get memory limit. * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32G'; } return wp_convert_hr_to_bytes( $memory_limit ); } /** * Schedule cron healthcheck. * * @param array $schedules Schedules. * @return array */ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); if ( property_exists( $this, 'cron_interval' ) ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } // Adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, /* translators: %d: interval */ 'display' => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ), ); return $schedules; } /** * Delete all batches. * * @return WC_Background_Process */ public function delete_all_batches() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine. return $this; } /** * Kill process. * * Stop processing queue items, clear cronjob and delete all batches. */ public function kill_process() { if ( ! $this->is_queue_empty() ) { $this->delete_all_batches(); wp_clear_scheduled_hook( $this->cron_hook_identifier ); } } } includes/abstracts/abstract-wc-integration.php 0000644 00000003535 15132754524 0015622 0 ustar 00 <?php /** * Abstract Integration class * * Extension of the Settings API which in turn gets extended * by individual integrations to offer additional functionality. * * @class WC_Settings_API * @version 2.6.0 * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Abstract Integration Class * * Extended by individual integrations to offer additional functionality. * * @class WC_Integration * @extends WC_Settings_API * @version 2.6.0 * @package WooCommerce\Abstracts */ abstract class WC_Integration extends WC_Settings_API { /** * Yes or no based on whether the integration is enabled. * * @var string */ public $enabled = 'yes'; /** * Integration title. * * @var string */ public $method_title = ''; /** * Integration description. * * @var string */ public $method_description = ''; /** * Return the title for admin screens. * * @return string */ public function get_method_title() { return apply_filters( 'woocommerce_integration_title', $this->method_title, $this ); } /** * Return the description for admin screens. * * @return string */ public function get_method_description() { return apply_filters( 'woocommerce_integration_description', $this->method_description, $this ); } /** * Output the gateway settings screen. */ public function admin_options() { echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>'; echo wp_kses_post( wpautop( $this->get_method_description() ) ); echo '<div><input type="hidden" name="section" value="' . esc_attr( $this->id ) . '" /></div>'; parent::admin_options(); } /** * Init settings for gateways. */ public function init_settings() { parent::init_settings(); $this->enabled = ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'] ? 'yes' : 'no'; } } includes/abstracts/abstract-wc-privacy.php 0000644 00000007547 15132754524 0014763 0 ustar 00 <?php /** * WooCommerce abstract privacy class. * * @since 3.4.0 * @package WooCommerce\Abstracts */ defined( 'ABSPATH' ) || exit; /** * Abstract class that is intended to be extended by * specific privacy class. It handles the display * of the privacy message of the privacy id to the admin, * privacy data to be exported and privacy data to be deleted. * * @version 3.4.0 * @package WooCommerce\Abstracts */ abstract class WC_Abstract_Privacy { /** * This is the name of this object type. * * @var string */ public $name; /** * This is a list of exporters. * * @var array */ protected $exporters = array(); /** * This is a list of erasers. * * @var array */ protected $erasers = array(); /** * This is a priority for the wp_privacy_personal_data_exporters filter * * @var int */ protected $export_priority; /** * This is a priority for the wp_privacy_personal_data_erasers filter * * @var int */ protected $erase_priority; /** * WC_Abstract_Privacy Constructor. * * @param string $name Plugin identifier. * @param int $export_priority Export priority. * @param int $erase_priority Erase priority. */ public function __construct( $name = '', $export_priority = 5, $erase_priority = 10 ) { $this->name = $name; $this->export_priority = $export_priority; $this->erase_priority = $erase_priority; $this->init(); } /** * Hook in events. */ protected function init() { add_action( 'admin_init', array( $this, 'add_privacy_message' ) ); // We set priority to 5 to help WooCommerce's findings appear before those from extensions in exported items. add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ), $this->export_priority ); add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ), $this->erase_priority ); } /** * Adds the privacy message on WC privacy page. */ public function add_privacy_message() { if ( function_exists( 'wp_add_privacy_policy_content' ) ) { $content = $this->get_privacy_message(); if ( $content ) { wp_add_privacy_policy_content( $this->name, $this->get_privacy_message() ); } } } /** * Gets the message of the privacy to display. * To be overloaded by the implementor. * * @return string */ public function get_privacy_message() { return ''; } /** * Integrate this exporter implementation within the WordPress core exporters. * * @param array $exporters List of exporter callbacks. * @return array */ public function register_exporters( $exporters = array() ) { foreach ( $this->exporters as $id => $exporter ) { $exporters[ $id ] = $exporter; } return $exporters; } /** * Integrate this eraser implementation within the WordPress core erasers. * * @param array $erasers List of eraser callbacks. * @return array */ public function register_erasers( $erasers = array() ) { foreach ( $this->erasers as $id => $eraser ) { $erasers[ $id ] = $eraser; } return $erasers; } /** * Add exporter to list of exporters. * * @param string $id ID of the Exporter. * @param string $name Exporter name. * @param string|array $callback Exporter callback. * * @return array */ public function add_exporter( $id, $name, $callback ) { $this->exporters[ $id ] = array( 'exporter_friendly_name' => $name, 'callback' => $callback, ); return $this->exporters; } /** * Add eraser to list of erasers. * * @param string $id ID of the Eraser. * @param string $name Exporter name. * @param string|array $callback Exporter callback. * * @return array */ public function add_eraser( $id, $name, $callback ) { $this->erasers[ $id ] = array( 'eraser_friendly_name' => $name, 'callback' => $callback, ); return $this->erasers; } } includes/wc-notice-functions.php 0000644 00000016775 15132754524 0013011 0 ustar 00 <?php /** * WooCommerce Message Functions * * Functions for error/message handling and display. * * @package WooCommerce\Functions * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Get the count of notices added, either for all notices (default) or for one. * particular notice type specified by $notice_type. * * @since 2.1 * @param string $notice_type Optional. The name of the notice type - either error, success or notice. * @return int */ function wc_notice_count( $notice_type = '' ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return; } $notice_count = 0; $all_notices = WC()->session->get( 'wc_notices', array() ); if ( isset( $all_notices[ $notice_type ] ) ) { $notice_count = count( $all_notices[ $notice_type ] ); } elseif ( empty( $notice_type ) ) { foreach ( $all_notices as $notices ) { $notice_count += count( $notices ); } } return $notice_count; } /** * Check if a notice has already been added. * * @since 2.1 * @param string $message The text to display in the notice. * @param string $notice_type Optional. The name of the notice type - either error, success or notice. * @return bool */ function wc_has_notice( $message, $notice_type = 'success' ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return false; } $notices = WC()->session->get( 'wc_notices', array() ); $notices = isset( $notices[ $notice_type ] ) ? $notices[ $notice_type ] : array(); return array_search( $message, wp_list_pluck( $notices, 'notice' ), true ) !== false; } /** * Add and store a notice. * * @since 2.1 * @version 3.9.0 * @param string $message The text to display in the notice. * @param string $notice_type Optional. The name of the notice type - either error, success or notice. * @param array $data Optional notice data. */ function wc_add_notice( $message, $notice_type = 'success', $data = array() ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return; } $notices = WC()->session->get( 'wc_notices', array() ); // Backward compatibility. if ( 'success' === $notice_type ) { $message = apply_filters( 'woocommerce_add_message', $message ); } $message = apply_filters( 'woocommerce_add_' . $notice_type, $message ); if ( ! empty( $message ) ) { $notices[ $notice_type ][] = array( 'notice' => $message, 'data' => $data, ); } WC()->session->set( 'wc_notices', $notices ); } /** * Set all notices at once. * * @since 2.6.0 * @param array[] $notices Array of notices. */ function wc_set_notices( $notices ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.6' ); return; } WC()->session->set( 'wc_notices', $notices ); } /** * Unset all notices. * * @since 2.1 */ function wc_clear_notices() { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return; } WC()->session->set( 'wc_notices', null ); } /** * Prints messages and errors which are stored in the session, then clears them. * * @since 2.1 * @param bool $return true to return rather than echo. @since 3.5.0. * @return string|null */ function wc_print_notices( $return = false ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return; } $all_notices = WC()->session->get( 'wc_notices', array() ); $notice_types = apply_filters( 'woocommerce_notice_types', array( 'error', 'success', 'notice' ) ); // Buffer output. ob_start(); foreach ( $notice_types as $notice_type ) { if ( wc_notice_count( $notice_type ) > 0 ) { $messages = array(); foreach ( $all_notices[ $notice_type ] as $notice ) { $messages[] = isset( $notice['notice'] ) ? $notice['notice'] : $notice; } wc_get_template( "notices/{$notice_type}.php", array( 'messages' => array_filter( $messages ), // @deprecated 3.9.0 'notices' => array_filter( $all_notices[ $notice_type ] ), ) ); } } wc_clear_notices(); $notices = wc_kses_notice( ob_get_clean() ); if ( $return ) { return $notices; } echo $notices; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Print a single notice immediately. * * @since 2.1 * @version 3.9.0 * @param string $message The text to display in the notice. * @param string $notice_type Optional. The singular name of the notice type - either error, success or notice. * @param array $data Optional notice data. @since 3.9.0. */ function wc_print_notice( $message, $notice_type = 'success', $data = array() ) { if ( 'success' === $notice_type ) { $message = apply_filters( 'woocommerce_add_message', $message ); } $message = apply_filters( 'woocommerce_add_' . $notice_type, $message ); wc_get_template( "notices/{$notice_type}.php", array( 'messages' => array( $message ), // @deprecated 3.9.0 'notices' => array( array( 'notice' => $message, 'data' => $data, ), ), ) ); } /** * Returns all queued notices, optionally filtered by a notice type. * * @since 2.1 * @version 3.9.0 * @param string $notice_type Optional. The singular name of the notice type - either error, success or notice. * @return array[] */ function wc_get_notices( $notice_type = '' ) { if ( ! did_action( 'woocommerce_init' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); return; } $all_notices = WC()->session->get( 'wc_notices', array() ); if ( empty( $notice_type ) ) { $notices = $all_notices; } elseif ( isset( $all_notices[ $notice_type ] ) ) { $notices = $all_notices[ $notice_type ]; } else { $notices = array(); } return $notices; } /** * Add notices for WP Errors. * * @param WP_Error $errors Errors. */ function wc_add_wp_error_notices( $errors ) { if ( is_wp_error( $errors ) && $errors->get_error_messages() ) { foreach ( $errors->get_error_messages() as $error ) { wc_add_notice( $error, 'error' ); } } } /** * Filters out the same tags as wp_kses_post, but allows tabindex for <a> element. * * @since 3.5.0 * @param string $message Content to filter through kses. * @return string */ function wc_kses_notice( $message ) { $allowed_tags = array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'tabindex' => true, ), ) ); /** * Kses notice allowed tags. * * @since 3.9.0 * @param array[]|string $allowed_tags An array of allowed HTML elements and attributes, or a context name such as 'post'. */ return wp_kses( $message, apply_filters( 'woocommerce_kses_notice_allowed_tags', $allowed_tags ) ); } /** * Get notice data attribute. * * @since 3.9.0 * @param array $notice Notice data. * @return string */ function wc_get_notice_data_attr( $notice ) { if ( empty( $notice['data'] ) ) { return; } $attr = ''; foreach ( $notice['data'] as $key => $value ) { $attr .= sprintf( ' data-%1$s="%2$s"', sanitize_title( $key ), esc_attr( $value ) ); } return $attr; } includes/class-wc-data-store.php 0000644 00000013545 15132754524 0012660 0 ustar 00 <?php /** * WC Data Store. * * @package WooCommerce\Classes * @since 3.0.0 * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Data store class. */ class WC_Data_Store { /** * Contains an instance of the data store class that we are working with. * * @var WC_Data_Store */ private $instance = null; /** * Contains an array of default WC supported data stores. * Format of object name => class name. * Example: 'product' => 'WC_Product_Data_Store_CPT' * You can also pass something like product_<type> for product stores and * that type will be used first when available, if a store is requested like * this and doesn't exist, then the store would fall back to 'product'. * Ran through `woocommerce_data_stores`. * * @var array */ private $stores = array( 'coupon' => 'WC_Coupon_Data_Store_CPT', 'customer' => 'WC_Customer_Data_Store', 'customer-download' => 'WC_Customer_Download_Data_Store', 'customer-download-log' => 'WC_Customer_Download_Log_Data_Store', 'customer-session' => 'WC_Customer_Data_Store_Session', 'order' => 'WC_Order_Data_Store_CPT', 'order-refund' => 'WC_Order_Refund_Data_Store_CPT', 'order-item' => 'WC_Order_Item_Data_Store', 'order-item-coupon' => 'WC_Order_Item_Coupon_Data_Store', 'order-item-fee' => 'WC_Order_Item_Fee_Data_Store', 'order-item-product' => 'WC_Order_Item_Product_Data_Store', 'order-item-shipping' => 'WC_Order_Item_Shipping_Data_Store', 'order-item-tax' => 'WC_Order_Item_Tax_Data_Store', 'payment-token' => 'WC_Payment_Token_Data_Store', 'product' => 'WC_Product_Data_Store_CPT', 'product-grouped' => 'WC_Product_Grouped_Data_Store_CPT', 'product-variable' => 'WC_Product_Variable_Data_Store_CPT', 'product-variation' => 'WC_Product_Variation_Data_Store_CPT', 'shipping-zone' => 'WC_Shipping_Zone_Data_Store', 'webhook' => 'WC_Webhook_Data_Store', ); /** * Contains the name of the current data store's class name. * * @var string */ private $current_class_name = ''; /** * The object type this store works with. * * @var string */ private $object_type = ''; /** * Tells WC_Data_Store which object (coupon, product, order, etc) * store we want to work with. * * @throws Exception When validation fails. * @param string $object_type Name of object. */ public function __construct( $object_type ) { $this->object_type = $object_type; $this->stores = apply_filters( 'woocommerce_data_stores', $this->stores ); // If this object type can't be found, check to see if we can load one // level up (so if product-type isn't found, we try product). if ( ! array_key_exists( $object_type, $this->stores ) ) { $pieces = explode( '-', $object_type ); $object_type = $pieces[0]; } if ( array_key_exists( $object_type, $this->stores ) ) { $store = apply_filters( 'woocommerce_' . $object_type . '_data_store', $this->stores[ $object_type ] ); if ( is_object( $store ) ) { if ( ! $store instanceof WC_Object_Data_Store_Interface ) { throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); } $this->current_class_name = get_class( $store ); $this->instance = $store; } else { if ( ! class_exists( $store ) ) { throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); } $this->current_class_name = $store; $this->instance = new $store(); } } else { throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); } } /** * Only store the object type to avoid serializing the data store instance. * * @return array */ public function __sleep() { return array( 'object_type' ); } /** * Re-run the constructor with the object type. * * @throws Exception When validation fails. */ public function __wakeup() { $this->__construct( $this->object_type ); } /** * Loads a data store. * * @param string $object_type Name of object. * * @since 3.0.0 * @throws Exception When validation fails. * @return WC_Data_Store */ public static function load( $object_type ) { return new WC_Data_Store( $object_type ); } /** * Returns the class name of the current data store. * * @since 3.0.0 * @return string */ public function get_current_class_name() { return $this->current_class_name; } /** * Reads an object from the data store. * * @since 3.0.0 * @param WC_Data $data WooCommerce data instance. */ public function read( &$data ) { $this->instance->read( $data ); } /** * Create an object in the data store. * * @since 3.0.0 * @param WC_Data $data WooCommerce data instance. */ public function create( &$data ) { $this->instance->create( $data ); } /** * Update an object in the data store. * * @since 3.0.0 * @param WC_Data $data WooCommerce data instance. */ public function update( &$data ) { $this->instance->update( $data ); } /** * Delete an object from the data store. * * @since 3.0.0 * @param WC_Data $data WooCommerce data instance. * @param array $args Array of args to pass to the delete method. */ public function delete( &$data, $args = array() ) { $this->instance->delete( $data, $args ); } /** * Data stores can define additional functions (for example, coupons have * some helper methods for increasing or decreasing usage). This passes * through to the instance if that function exists. * * @since 3.0.0 * @param string $method Method. * @param mixed $parameters Parameters. * @return mixed */ public function __call( $method, $parameters ) { if ( is_callable( array( $this->instance, $method ) ) ) { $object = array_shift( $parameters ); $parameters = array_merge( array( &$object ), $parameters ); return $this->instance->$method( ...$parameters ); } } } includes/class-wc-cart-fees.php 0000644 00000006644 15132754524 0012470 0 ustar 00 <?php /** * Cart fees API. * * Developers can add fees to the cart via WC()->cart->fees_api() which will reference this class. * * We suggest using the action woocommerce_cart_calculate_fees hook for adding fees. * * @package WooCommerce\Classes * @version 3.2.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Cart_Fees class. * * @since 3.2.0 */ final class WC_Cart_Fees { /** * An array of fee objects. * * @var object[] */ private $fees = array(); /** * Reference to cart object. * * @since 3.2.0 * @var WC_Cart */ private $cart; /** * New fees are made out of these props. * * @var array */ private $default_fee_props = array( 'id' => '', 'name' => '', 'tax_class' => '', 'taxable' => false, 'amount' => 0, 'total' => 0, ); /** * Constructor. Reference to the cart. * * @since 3.2.0 * @throws Exception If missing WC_Cart object. * @param WC_Cart $cart Cart object. */ public function __construct( &$cart ) { if ( ! is_a( $cart, 'WC_Cart' ) ) { throw new Exception( 'A valid WC_Cart object is required' ); } $this->cart = $cart; } /** * Register methods for this object on the appropriate WordPress hooks. */ public function init() {} /** * Add a fee. Fee IDs must be unique. * * @since 3.2.0 * @param array $args Array of fee properties. * @return object Either a fee object if added, or a WP_Error if it failed. */ public function add_fee( $args = array() ) { $fee_props = (object) wp_parse_args( $args, $this->default_fee_props ); $fee_props->name = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' ); $fee_props->tax_class = in_array( $fee_props->tax_class, array_merge( WC_Tax::get_tax_classes(), WC_Tax::get_tax_class_slugs() ), true ) ? $fee_props->tax_class : ''; $fee_props->taxable = wc_string_to_bool( $fee_props->taxable ); $fee_props->amount = wc_format_decimal( $fee_props->amount ); if ( empty( $fee_props->id ) ) { $fee_props->id = $this->generate_id( $fee_props ); } if ( array_key_exists( $fee_props->id, $this->fees ) ) { return new WP_Error( 'fee_exists', __( 'Fee has already been added.', 'woocommerce' ) ); } $this->fees[ $fee_props->id ] = $fee_props; return $this->fees[ $fee_props->id ]; } /** * Get fees. * * @return array */ public function get_fees() { uasort( $this->fees, array( $this, 'sort_fees_callback' ) ); return $this->fees; } /** * Set fees. * * @param object[] $raw_fees Array of fees. */ public function set_fees( $raw_fees = array() ) { $this->fees = array(); foreach ( $raw_fees as $raw_fee ) { $this->add_fee( $raw_fee ); } } /** * Remove all fees. * * @since 3.2.0 */ public function remove_all_fees() { $this->set_fees(); } /** * Sort fees by amount. * * @param stdClass $a Fee object. * @param stdClass $b Fee object. * @return int */ protected function sort_fees_callback( $a, $b ) { /** * Filter sort fees callback. * * @since 3.8.0 * @param int Sort order, -1 or 1. * @param stdClass $a Fee object. * @param stdClass $b Fee object. */ return apply_filters( 'woocommerce_sort_fees_callback', $a->amount > $b->amount ? -1 : 1, $a, $b ); } /** * Generate a unique ID for the fee being added. * * @param string $fee Fee object. * @return string fee key. */ private function generate_id( $fee ) { return sanitize_title( $fee->name ); } } includes/class-wc-tax.php 0000644 00000112001 15132754524 0011374 0 ustar 00 <?php /** * Tax calculation and rate finding class. * * @package WooCommerce\Classes */ use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * Performs tax calculations and loads tax rates * * @class WC_Tax */ class WC_Tax { /** * Precision. * * @var int */ public static $precision; /** * Round at subtotal. * * @var bool */ public static $round_at_subtotal = false; /** * Load options. */ public static function init() { self::$precision = wc_get_rounding_precision(); self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); } /** * When the woocommerce_tax_classes option is changed, remove any orphan rates. * * @deprecated 3.7.0 * @param string $old_value Old rates value. * @param string $value New rates value. */ public static function maybe_remove_tax_class_rates( $old_value, $value ) { wc_deprecated_function( 'WC_Tax::maybe_remove_tax_class_rates', '3.7', 'WC_Tax::delete_tax_class_by' ); $tax_classes = array_filter( array_map( 'trim', explode( "\n", $value ) ) ); $existing_tax_classes = self::get_tax_classes(); $removed = array_diff( $existing_tax_classes, $tax_classes ); foreach ( $removed as $name ) { self::delete_tax_class_by( 'name', $name ); } } /** * Calculate tax for a line. * * @param float $price Price to calc tax on. * @param array $rates Rates to apply. * @param boolean $price_includes_tax Whether the passed price has taxes included. * @param boolean $deprecated Whether to suppress any rounding from taking place. No longer used here. * @return array Array of rates + prices after tax. */ public static function calc_tax( $price, $rates, $price_includes_tax = false, $deprecated = false ) { if ( $price_includes_tax ) { $taxes = self::calc_inclusive_tax( $price, $rates ); } else { $taxes = self::calc_exclusive_tax( $price, $rates ); } return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $deprecated ); } /** * Calculate the shipping tax using a passed array of rates. * * @param float $price Shipping cost. * @param array $rates Taxation Rate. * @return array */ public static function calc_shipping_tax( $price, $rates ) { $taxes = self::calc_exclusive_tax( $price, $rates ); return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates ); } /** * Round to precision. * * Filter example: to return rounding to .5 cents you'd use: * * function euro_5cent_rounding( $in ) { * return round( $in / 5, 2 ) * 5; * } * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' ); * * @param float|int $in Value to round. * @return float */ public static function round( $in ) { return apply_filters( 'woocommerce_tax_round', NumberUtil::round( $in, wc_get_rounding_precision() ), $in ); } /** * Calc tax from inclusive price. * * @param float $price Price to calculate tax for. * @param array $rates Array of tax rates. * @return array */ public static function calc_inclusive_tax( $price, $rates ) { $taxes = array(); $compound_rates = array(); $regular_rates = array(); // Index array so taxes are output in correct order and see what compound/regular rates we have to calculate. foreach ( $rates as $key => $rate ) { $taxes[ $key ] = 0; if ( 'yes' === $rate['compound'] ) { $compound_rates[ $key ] = $rate['rate']; } else { $regular_rates[ $key ] = $rate['rate']; } } $compound_rates = array_reverse( $compound_rates, true ); // Working backwards. $non_compound_price = $price; foreach ( $compound_rates as $key => $compound_rate ) { $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price ); $taxes[ $key ] += $tax_amount; $non_compound_price = $non_compound_price - $tax_amount; } // Regular taxes. $regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 ); foreach ( $regular_rates as $key => $regular_rate ) { $the_rate = ( $regular_rate / 100 ) / $regular_tax_rate; $net_price = $price - ( $the_rate * $non_compound_price ); $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price ); $taxes[ $key ] += $tax_amount; } /** * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding * as in the cart calculation class which, depending on settings, will round to 2DP when calculating * final totals. Also unlike that class, this rounds .5 up for all cases. */ $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); return $taxes; } /** * Calc tax from exclusive price. * * @param float $price Price to calculate tax for. * @param array $rates Array of tax rates. * @return array */ public static function calc_exclusive_tax( $price, $rates ) { $taxes = array(); if ( ! empty( $rates ) ) { foreach ( $rates as $key => $rate ) { if ( 'yes' === $rate['compound'] ) { continue; } $tax_amount = $price * ( $rate['rate'] / 100 ); $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate. if ( ! isset( $taxes[ $key ] ) ) { $taxes[ $key ] = $tax_amount; } else { $taxes[ $key ] += $tax_amount; } } $pre_compound_total = array_sum( $taxes ); // Compound taxes. foreach ( $rates as $key => $rate ) { if ( 'no' === $rate['compound'] ) { continue; } $the_price_inc_tax = $price + ( $pre_compound_total ); $tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 ); $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate. if ( ! isset( $taxes[ $key ] ) ) { $taxes[ $key ] = $tax_amount; } else { $taxes[ $key ] += $tax_amount; } $pre_compound_total = array_sum( $taxes ); } } /** * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding * as in the cart calculation class which, depending on settings, will round to 2DP when calculating * final totals. Also unlike that class, this rounds .5 up for all cases. */ $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); return $taxes; } /** * Searches for all matching country/state/postcode tax rates. * * @param array $args Args that determine the rate to find. * @return array */ public static function find_rates( $args = array() ) { $args = wp_parse_args( $args, array( 'country' => '', 'state' => '', 'city' => '', 'postcode' => '', 'tax_class' => '', ) ); $country = $args['country']; $state = $args['state']; $city = $args['city']; $postcode = wc_normalize_postcode( wc_clean( $args['postcode'] ) ); $tax_class = $args['tax_class']; if ( ! $country ) { return array(); } $cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); if ( false === $matched_tax_rates ) { $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); } return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); } /** * Searches for all matching country/state/postcode tax rates. * * @param array $args Args that determine the rate to find. * @return array */ public static function find_shipping_rates( $args = array() ) { $rates = self::find_rates( $args ); $shipping_rates = array(); if ( is_array( $rates ) ) { foreach ( $rates as $key => $rate ) { if ( 'yes' === $rate['shipping'] ) { $shipping_rates[ $key ] = $rate; } } } return $shipping_rates; } /** * Does the sort comparison. Compares (in this order): * - Priority * - Country * - State * - Number of postcodes * - Number of cities * - ID * * @param object $rate1 First rate to compare. * @param object $rate2 Second rate to compare. * @return int */ private static function sort_rates_callback( $rate1, $rate2 ) { if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) { return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC. } if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) { if ( '' === $rate1->tax_rate_country ) { return 1; } if ( '' === $rate2->tax_rate_country ) { return -1; } return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1; } if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) { if ( '' === $rate1->tax_rate_state ) { return 1; } if ( '' === $rate2->tax_rate_state ) { return -1; } return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1; } if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) { return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1; } if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) { return $rate1->city_count < $rate2->city_count ? 1 : -1; } return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; } /** * Logical sort order for tax rates based on the following in order of priority. * * @param array $rates Rates to be sorted. * @return array */ private static function sort_rates( $rates ) { uasort( $rates, __CLASS__ . '::sort_rates_callback' ); $i = 0; foreach ( $rates as $key => $rate ) { $rates[ $key ]->tax_rate_order = $i++; } return $rates; } /** * Loop through a set of tax rates and get the matching rates (1 per priority). * * @param string $country Country code to match against. * @param string $state State code to match against. * @param string $postcode Postcode to match against. * @param string $city City to match against. * @param string $tax_class Tax class to match against. * @return array */ private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { global $wpdb; // Query criteria - these will be ANDed. $criteria = array(); $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); $criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) ); // Pre-query postcode ranges for PHP based matching. $postcode_search = wc_get_wildcard_postcodes( $postcode, $country ); $postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); if ( $postcode_ranges ) { $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country ); if ( ! empty( $matches ) ) { foreach ( $matches as $matched_postcodes ) { $postcode_search = array_merge( $postcode_search, $matched_postcodes ); } } } $postcode_search = array_unique( $postcode_search ); /** * Location matching criteria - ORed * Needs to match: * - rates with no postcodes and cities * - rates with a matching postcode and city * - rates with matching postcode, no city * - rates with matching city, no postcode */ $locations_criteria = array(); $locations_criteria[] = 'locations.location_type IS NULL'; $locations_criteria[] = " locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') AND ( ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) OR NOT EXISTS ( SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub WHERE sub.location_type = 'city' AND sub.tax_rate_id = tax_rates.tax_rate_id ) ) "; $locations_criteria[] = " locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' AND NOT EXISTS ( SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub WHERE sub.location_type = 'postcode' AND sub.tax_rate_id = tax_rates.tax_rate_id ) "; $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; $criteria_string = implode( ' AND ', $criteria ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $found_rates = $wpdb->get_results( " SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id WHERE 1=1 AND {$criteria_string} GROUP BY tax_rates.tax_rate_id ORDER BY tax_rates.tax_rate_priority " ); // phpcs:enable $found_rates = self::sort_rates( $found_rates ); $matched_tax_rates = array(); $found_priority = array(); foreach ( $found_rates as $found_rate ) { if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) { continue; } $matched_tax_rates[ $found_rate->tax_rate_id ] = array( 'rate' => (float) $found_rate->tax_rate, 'label' => $found_rate->tax_rate_name, 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no', ); $found_priority[] = $found_rate->tax_rate_priority; } return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); } /** * Get the customer tax location based on their status and the current page. * * Used by get_rates(), get_shipping_rates(). * * @param string $tax_class string Optional, passed to the filter for advanced tax setups. * @param object $customer Override the customer object to get their location. * @return array */ public static function get_tax_location( $tax_class = '', $customer = null ) { $location = array(); if ( is_null( $customer ) && WC()->customer ) { $customer = WC()->customer; } if ( ! empty( $customer ) ) { $location = $customer->get_taxable_address(); } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { $location = array( WC()->countries->get_base_country(), WC()->countries->get_base_state(), WC()->countries->get_base_postcode(), WC()->countries->get_base_city(), ); } return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer ); } /** * Get's an array of matching rates for a tax class. * * @param string $tax_class Tax class to get rates for. * @param object $customer Override the customer object to get their location. * @return array */ public static function get_rates( $tax_class = '', $customer = null ) { $tax_class = sanitize_title( $tax_class ); $location = self::get_tax_location( $tax_class, $customer ); return self::get_rates_from_location( $tax_class, $location, $customer ); } /** * Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter. * * @param string $tax_class Tax class to get rates for. * @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city). * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. * * @return mixed|void Tax rates. */ public static function get_rates_from_location( $tax_class, $location, $customer = null ) { $tax_class = sanitize_title( $tax_class ); $matched_tax_rates = array(); if ( count( $location ) === 4 ) { list( $country, $state, $postcode, $city ) = $location; $matched_tax_rates = self::find_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'tax_class' => $tax_class, ) ); } return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer ); } /** * Get's an array of matching rates for the shop's base country. * * @param string $tax_class Tax Class. * @return array */ public static function get_base_tax_rates( $tax_class = '' ) { return apply_filters( 'woocommerce_base_tax_rates', self::find_rates( array( 'country' => WC()->countries->get_base_country(), 'state' => WC()->countries->get_base_state(), 'postcode' => WC()->countries->get_base_postcode(), 'city' => WC()->countries->get_base_city(), 'tax_class' => $tax_class, ) ), $tax_class ); } /** * Alias for get_base_tax_rates(). * * @deprecated 2.3 * @param string $tax_class Tax Class. * @return array */ public static function get_shop_base_rate( $tax_class = '' ) { return self::get_base_tax_rates( $tax_class ); } /** * Gets an array of matching shipping tax rates for a given class. * * @param string $tax_class Tax class to get rates for. * @param object $customer Override the customer object to get their location. * @return mixed */ public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) { // See if we have an explicitly set shipping tax class. $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); if ( 'inherit' !== $shipping_tax_class ) { $tax_class = $shipping_tax_class; } $location = self::get_tax_location( $tax_class, $customer ); $matched_tax_rates = array(); if ( 4 === count( $location ) ) { list( $country, $state, $postcode, $city ) = $location; if ( ! is_null( $tax_class ) ) { // This will be per item shipping. $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'tax_class' => $tax_class, ) ); } elseif ( WC()->cart->get_cart() ) { // This will be per order shipping - loop through the order and find the highest tax class rate. $cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping(); // No tax classes = no taxable items. if ( empty( $cart_tax_classes ) ) { return array(); } // If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section. if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) { $tax_classes = self::get_tax_class_slugs(); foreach ( $tax_classes as $tax_class ) { if ( in_array( $tax_class, $cart_tax_classes, true ) ) { $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'tax_class' => $tax_class, ) ); break; } } } elseif ( 1 === count( $cart_tax_classes ) ) { // If a single tax class is found, use it. $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'tax_class' => $cart_tax_classes[0], ) ); } } // Get standard rate if no taxes were found. if ( ! count( $matched_tax_rates ) ) { $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, ) ); } } return $matched_tax_rates; } /** * Return true/false depending on if a rate is a compound rate. * * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. * @return bool */ public static function is_compound( $key_or_rate ) { global $wpdb; if ( is_object( $key_or_rate ) ) { $key = $key_or_rate->tax_rate_id; $compound = $key_or_rate->tax_rate_compound; } else { $key = $key_or_rate; $compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); } return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key ); } /** * Return a given rates label. * * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. * @return string */ public static function get_rate_label( $key_or_rate ) { global $wpdb; if ( is_object( $key_or_rate ) ) { $key = $key_or_rate->tax_rate_id; $rate_name = $key_or_rate->tax_rate_name; } else { $key = $key_or_rate; $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); } if ( ! $rate_name ) { $rate_name = WC()->countries->tax_or_vat(); } return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); } /** * Return a given rates percent. * * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. * @return string */ public static function get_rate_percent( $key_or_rate ) { $rate_percent_value = self::get_rate_percent_value( $key_or_rate ); $tax_rate_id = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate; return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id ); } /** * Return a given rates percent. * * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. * @return float */ public static function get_rate_percent_value( $key_or_rate ) { global $wpdb; if ( is_object( $key_or_rate ) ) { $tax_rate = $key_or_rate->tax_rate; } else { $key = $key_or_rate; $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); } return floatval( $tax_rate ); } /** * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. * * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. * @return string */ public static function get_rate_code( $key_or_rate ) { global $wpdb; if ( is_object( $key_or_rate ) ) { $key = $key_or_rate->tax_rate_id; $rate = $key_or_rate; } else { $key = $key_or_rate; $rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); } $code_string = ''; if ( null !== $rate ) { $code = array(); $code[] = $rate->tax_rate_country; $code[] = $rate->tax_rate_state; $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; $code[] = absint( $rate->tax_rate_priority ); $code_string = strtoupper( implode( '-', array_filter( $code ) ) ); } return apply_filters( 'woocommerce_rate_code', $code_string, $key ); } /** * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0. * * @param array $taxes Array of taxes. * @return float */ public static function get_tax_total( $taxes ) { return array_sum( $taxes ); } /** * Gets all tax rate classes from the database. * * @since 3.7.0 * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug. */ public static function get_tax_rate_classes() { global $wpdb; $cache_key = 'tax-rate-classes'; $tax_rate_classes = wp_cache_get( $cache_key, 'taxes' ); if ( ! is_array( $tax_rate_classes ) ) { $tax_rate_classes = $wpdb->get_results( " SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name; " ); wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' ); } return $tax_rate_classes; } /** * Get store tax class names. * * @return array Array of class names ("Reduced rate", "Zero rate", etc). */ public static function get_tax_classes() { return wp_list_pluck( self::get_tax_rate_classes(), 'name' ); } /** * Get store tax classes as slugs. * * @since 3.0.0 * @return array Array of class slugs ("reduced-rate", "zero-rate", etc). */ public static function get_tax_class_slugs() { return wp_list_pluck( self::get_tax_rate_classes(), 'slug' ); } /** * Create a new tax class. * * @since 3.7.0 * @param string $name Name of the tax class to add. * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name. * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong. */ public static function create_tax_class( $name, $slug = '' ) { global $wpdb; if ( empty( $name ) ) { return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) ); } $existing = self::get_tax_classes(); $existing_slugs = self::get_tax_class_slugs(); $name = wc_clean( $name ); if ( in_array( $name, $existing, true ) ) { return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) ); } if ( ! $slug ) { $slug = sanitize_title( $name ); } // Stop if there's no slug. if ( ! $slug ) { return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) ); } if ( in_array( $slug, $existing_slugs, true ) ) { return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) ); } $insert = $wpdb->insert( $wpdb->wc_tax_rate_classes, array( 'name' => $name, 'slug' => $slug, ) ); if ( is_wp_error( $insert ) ) { return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() ); } wp_cache_delete( 'tax-rate-classes', 'taxes' ); return array( 'name' => $name, 'slug' => $slug, ); } /** * Get an existing tax class. * * @since 3.7.0 * @param string $field Field to get by. Valid values are id, name, or slug. * @param string|int $item Item to get. * @return array|bool Returns the tax class as an array. False if not found. */ public static function get_tax_class_by( $field, $item ) { if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); } if ( 'id' === $field ) { $field = 'tax_rate_class_id'; } $matches = wp_list_filter( self::get_tax_rate_classes(), array( $field => $item, ) ); if ( ! $matches ) { return false; } $tax_class = current( $matches ); return array( 'name' => $tax_class->name, 'slug' => $tax_class->slug, ); } /** * Delete an existing tax class. * * @since 3.7.0 * @param string $field Field to delete by. Valid values are id, name, or slug. * @param string|int $item Item to delete. * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request. */ public static function delete_tax_class_by( $field, $item ) { global $wpdb; if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); } $tax_class = self::get_tax_class_by( $field, $item ); if ( ! $tax_class ) { return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); } if ( 'id' === $field ) { $field = 'tax_rate_class_id'; } $delete = $wpdb->delete( $wpdb->wc_tax_rate_classes, array( $field => $item, ) ); if ( $delete ) { // Delete associated tax rates. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) ); $wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" ); } wp_cache_delete( 'tax-rate-classes', 'taxes' ); WC_Cache_Helper::invalidate_cache_group( 'taxes' ); return (bool) $delete; } /** * Format the city. * * @param string $city Value to format. * @return string */ private static function format_tax_rate_city( $city ) { return strtoupper( trim( $city ) ); } /** * Format the state. * * @param string $state Value to format. * @return string */ private static function format_tax_rate_state( $state ) { $state = strtoupper( $state ); return ( '*' === $state ) ? '' : $state; } /** * Format the country. * * @param string $country Value to format. * @return string */ private static function format_tax_rate_country( $country ) { $country = strtoupper( $country ); return ( '*' === $country ) ? '' : $country; } /** * Format the tax rate name. * * @param string $name Value to format. * @return string */ private static function format_tax_rate_name( $name ) { return $name ? $name : __( 'Tax', 'woocommerce' ); } /** * Format the rate. * * @param float $rate Value to format. * @return string */ private static function format_tax_rate( $rate ) { return number_format( (float) $rate, 4, '.', '' ); } /** * Format the priority. * * @param string $priority Value to format. * @return int */ private static function format_tax_rate_priority( $priority ) { return absint( $priority ); } /** * Format the class. * * @param string $class Value to format. * @return string */ public static function format_tax_rate_class( $class ) { $class = sanitize_title( $class ); $classes = self::get_tax_class_slugs(); if ( ! in_array( $class, $classes, true ) ) { $class = ''; } return ( 'standard' === $class ) ? '' : $class; } /** * Prepare and format tax rate for DB insertion. * * @param array $tax_rate Tax rate to format. * @return array */ private static function prepare_tax_rate( $tax_rate ) { foreach ( $tax_rate as $key => $value ) { if ( method_exists( __CLASS__, 'format_' . $key ) ) { if ( 'tax_rate_state' === $key ) { $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) ); } else { $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); } } } return $tax_rate; } /** * Insert a new tax rate. * * Internal use only. * * @since 2.3.0 * * @param array $tax_rate Tax rate to insert. * @return int tax rate id */ public static function _insert_tax_rate( $tax_rate ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); $tax_rate_id = $wpdb->insert_id; WC_Cache_Helper::invalidate_cache_group( 'taxes' ); do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate ); return $tax_rate_id; } /** * Get tax rate. * * Internal use only. * * @since 2.5.0 * * @param int $tax_rate_id Tax rate ID. * @param string $output_type Type of output. * @return array|object */ public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d ", $tax_rate_id ), $output_type ); } /** * Update a tax rate. * * Internal use only. * * @since 2.3.0 * * @param int $tax_rate_id Tax rate to update. * @param array $tax_rate Tax rate values. */ public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { global $wpdb; $tax_rate_id = absint( $tax_rate_id ); $wpdb->update( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ), array( 'tax_rate_id' => $tax_rate_id, ) ); WC_Cache_Helper::invalidate_cache_group( 'taxes' ); do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); } /** * Delete a tax rate from the database. * * Internal use only. * * @since 2.3.0 * @param int $tax_rate_id Tax rate to delete. */ public static function _delete_tax_rate( $tax_rate_id ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); WC_Cache_Helper::invalidate_cache_group( 'taxes' ); do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); } /** * Update postcodes for a tax rate in the DB. * * Internal use only. * * @since 2.3.0 * * @param int $tax_rate_id Tax rate to update. * @param string $postcodes String of postcodes separated by ; characters. */ public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { if ( ! is_array( $postcodes ) ) { $postcodes = explode( ';', $postcodes ); } // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. foreach ( $postcodes as $key => $postcode ) { $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) ); } self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' ); } /** * Update cities for a tax rate in the DB. * * Internal use only. * * @since 2.3.0 * * @param int $tax_rate_id Tax rate to update. * @param string $cities Cities to set. */ public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { if ( ! is_array( $cities ) ) { $cities = explode( ';', $cities ); } $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); } /** * Updates locations (postcode and city). * * Internal use only. * * @since 2.3.0 * * @param int $tax_rate_id Tax rate ID to update. * @param array $values Values to set. * @param string $type Location type. */ private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) { global $wpdb; $tax_rate_id = absint( $tax_rate_id ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;", $tax_rate_id, $type ) ); if ( count( $values ) > 0 ) { $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine. } WC_Cache_Helper::invalidate_cache_group( 'taxes' ); } /** * Used by admin settings page. * * @param string $tax_class Tax class slug. * * @return array|null|object */ public static function get_rates_for_tax_class( $tax_class ) { global $wpdb; $tax_class = self::format_tax_rate_class( $tax_class ); // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. $rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) ); $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); if ( ! empty( $rates ) ) { // Set the rates keys equal to their ids. $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); } // Drop the locations into the rates array. foreach ( $locations as $location ) { // Don't set them for unexistent rates. if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { continue; } // If the rate exists, initialize the array before appending to it. if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { $rates[ $location->tax_rate_id ]->{$location->location_type} = array(); } $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; } foreach ( $rates as $rate_id => $rate ) { $rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0; $rates[ $rate_id ]->city_count = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0; } $rates = self::sort_rates( $rates ); return $rates; } } WC_Tax::init(); includes/wc-webhook-functions.php 0000644 00000013143 15132754524 0013150 0 ustar 00 <?php /** * WooCommerce Webhook functions * * @package WooCommerce\Functions * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Process the web hooks at the end of the request. * * @since 4.4.0 */ function wc_webhook_execute_queue() { global $wc_queued_webhooks; if ( empty( $wc_queued_webhooks ) ) { return; } foreach ( $wc_queued_webhooks as $data ) { // Webhooks are processed in the background by default // so as to avoid delays or failures in delivery from affecting the // user who triggered it. if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $data['webhook'], $data['arg'] ) ) { $queue_args = array( 'webhook_id' => $data['webhook']->get_id(), 'arg' => $data['arg'], ); $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); // Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0. if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) { WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); } } else { // Deliver immediately. $data['webhook']->deliver( $data['arg'] ); } } } add_action( 'shutdown', 'wc_webhook_execute_queue' ); /** * Process webhook delivery. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. * @param array $arg Delivery arguments. */ function wc_webhook_process_delivery( $webhook, $arg ) { // We need to queue the webhook so that it can be ran after the request has finished processing. global $wc_queued_webhooks; if ( ! isset( $wc_queued_webhooks ) ) { $wc_queued_webhooks = array(); } $wc_queued_webhooks[] = array( 'webhook' => $webhook, 'arg' => $arg, ); } add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 ); /** * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron. * hook, see WC_Webhook::process(). * * @since 2.2.0 * @param int $webhook_id Webhook ID to deliver. * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @param mixed $arg Hook argument. */ function wc_deliver_webhook_async( $webhook_id, $arg ) { $webhook = new WC_Webhook( $webhook_id ); $webhook->deliver( $arg ); } add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 ); /** * Check if the given topic is a valid webhook topic, a topic is valid if: * * + starts with `action.woocommerce_` or `action.wc_`. * + it has a valid resource & event. * * @since 2.2.0 * @param string $topic Webhook topic. * @return bool */ function wc_is_webhook_valid_topic( $topic ) { $invalid_topics = array( 'action.woocommerce_login_credentials', 'action.woocommerce_product_csv_importer_check_import_file_path', 'action.woocommerce_webhook_should_deliver', ); if ( in_array( $topic, $invalid_topics, true ) ) { return false; } // Custom topics are prefixed with woocommerce_ or wc_ are valid. if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) { return true; } $data = explode( '.', $topic ); if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) { return false; } $valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) ); $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) ); if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) { return true; } return false; } /** * Check if given status is a valid webhook status. * * @since 3.5.3 * @param string $status Status to check. * @return bool */ function wc_is_webhook_valid_status( $status ) { return in_array( $status, array_keys( wc_get_webhook_statuses() ), true ); } /** * Get Webhook statuses. * * @since 2.3.0 * @return array */ function wc_get_webhook_statuses() { return apply_filters( 'woocommerce_webhook_statuses', array( 'active' => __( 'Active', 'woocommerce' ), 'paused' => __( 'Paused', 'woocommerce' ), 'disabled' => __( 'Disabled', 'woocommerce' ), ) ); } /** * Load webhooks. * * @since 3.3.0 * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0. * @param null|int $limit Limit number of webhooks loaded. @since 3.6.0. * @return bool */ function wc_load_webhooks( $status = '', $limit = null ) { $data_store = WC_Data_Store::load( 'webhook' ); $webhooks = $data_store->get_webhooks_ids( $status ); $loaded = 0; foreach ( $webhooks as $webhook_id ) { $webhook = new WC_Webhook( $webhook_id ); $webhook->enqueue(); $loaded ++; if ( ! is_null( $limit ) && $loaded >= $limit ) { break; } } return 0 < $loaded; } /** * Get webhook. * * @param int|WC_Webhook $id Webhook ID or object. * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @return WC_Webhook|null */ function wc_get_webhook( $id ) { $webhook = new WC_Webhook( $id ); return 0 !== $webhook->get_id() ? $webhook : null; } /** * Get webhoook REST API versions. * * @since 3.5.1 * @return array */ function wc_get_webhook_rest_api_versions() { return array( 'wp_api_v1', 'wp_api_v2', 'wp_api_v3', ); } includes/class-wc-order-item-shipping.php 0000644 00000017323 15132754524 0014501 0 ustar 00 <?php /** * Order Line Item (shipping) * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item shipping class. */ class WC_Order_Item_Shipping extends WC_Order_Item { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $extra_data = array( 'method_title' => '', 'method_id' => '', 'instance_id' => '', 'total' => 0, 'total_tax' => 0, 'taxes' => array( 'total' => array(), ), ); /** * Calculate item taxes. * * @since 3.2.0 * @param array $calculate_tax_for Location data to get taxes for. Required. * @return bool True if taxes were calculated. */ public function calculate_taxes( $calculate_tax_for = array() ) { if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) { return false; } if ( wc_tax_enabled() ) { $tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for ); $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); $this->set_taxes( array( 'total' => $taxes ) ); } else { $this->set_taxes( false ); } do_action( 'woocommerce_order_item_shipping_after_calculate_taxes', $this, $calculate_tax_for ); return true; } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set order item name. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_name( $value ) { $this->set_method_title( $value ); } /** * Set method title. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_method_title( $value ) { $this->set_prop( 'name', wc_clean( $value ) ); $this->set_prop( 'method_title', wc_clean( $value ) ); } /** * Set shipping method id. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_method_id( $value ) { $this->set_prop( 'method_id', wc_clean( $value ) ); } /** * Set shipping instance id. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_instance_id( $value ) { $this->set_prop( 'instance_id', wc_clean( $value ) ); } /** * Set total. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_total( $value ) { $this->set_prop( 'total', wc_format_decimal( $value ) ); } /** * Set total tax. * * @param string $value Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ protected function set_total_tax( $value ) { $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); } /** * Set taxes. * * This is an array of tax ID keys with total amount values. * * @param array $raw_tax_data Value to set. * @throws WC_Data_Exception May throw exception if data is invalid. */ public function set_taxes( $raw_tax_data ) { $raw_tax_data = maybe_unserialize( $raw_tax_data ); $tax_data = array( 'total' => array(), ); if ( isset( $raw_tax_data['total'] ) ) { $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); } elseif ( ! empty( $raw_tax_data ) && is_array( $raw_tax_data ) ) { // Older versions just used an array. $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data ); } $this->set_prop( 'taxes', $tax_data ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $this->set_total_tax( array_sum( $tax_data['total'] ) ); } else { $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); } } /** * Set properties based on passed in shipping rate object. * * @param WC_Shipping_Rate $shipping_rate Shipping rate to set. */ public function set_shipping_rate( $shipping_rate ) { $this->set_method_title( $shipping_rate->get_label() ); $this->set_method_id( $shipping_rate->get_method_id() ); $this->set_instance_id( $shipping_rate->get_instance_id() ); $this->set_total( $shipping_rate->get_cost() ); $this->set_taxes( $shipping_rate->get_taxes() ); $this->set_meta_data( $shipping_rate->get_meta_data() ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order item type. * * @return string */ public function get_type() { return 'shipping'; } /** * Get order item name. * * @param string $context View or edit context. * @return string */ public function get_name( $context = 'view' ) { return $this->get_method_title( $context ); } /** * Get title. * * @param string $context View or edit context. * @return string */ public function get_method_title( $context = 'view' ) { $method_title = $this->get_prop( 'method_title', $context ); if ( 'view' === $context ) { return $method_title ? $method_title : __( 'Shipping', 'woocommerce' ); } else { return $method_title; } } /** * Get method ID. * * @param string $context View or edit context. * @return string */ public function get_method_id( $context = 'view' ) { return $this->get_prop( 'method_id', $context ); } /** * Get instance ID. * * @param string $context View or edit context. * @return string */ public function get_instance_id( $context = 'view' ) { return $this->get_prop( 'instance_id', $context ); } /** * Get total cost. * * @param string $context View or edit context. * @return string */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax. * * @param string $context View or edit context. * @return string */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /** * Get taxes. * * @param string $context View or edit context. * @return array */ public function get_taxes( $context = 'view' ) { return $this->get_prop( 'taxes', $context ); } /** * Get tax class. * * @param string $context View or edit context. * @return string */ public function get_tax_class( $context = 'view' ) { return get_option( 'woocommerce_shipping_tax_class' ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * Offset get: for ArrayAccess/Backwards compatibility. * * @param string $offset Key. * @return mixed */ public function offsetGet( $offset ) { if ( 'cost' === $offset ) { $offset = 'total'; } return parent::offsetGet( $offset ); } /** * Offset set: for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Key. * @param mixed $value Value to set. */ public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Shipping::offsetSet', '4.4.0', '' ); if ( 'cost' === $offset ) { $offset = 'total'; } parent::offsetSet( $offset, $value ); } /** * Offset exists: for ArrayAccess. * * @param string $offset Key. * @return bool */ public function offsetExists( $offset ) { if ( in_array( $offset, array( 'cost' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } includes/walkers/class-wc-product-cat-dropdown-walker.php 0000644 00000006004 15132754524 0017617 0 ustar 00 <?php /** * WC_Product_Cat_Dropdown_Walker class * * @package WooCommerce\Classes\Walkers * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { return; } /** * Product category dropdown walker class. */ class WC_Product_Cat_Dropdown_Walker extends Walker { /** * What the class handles. * * @var string */ public $tree_type = 'category'; /** * DB fields to use. * * @var array */ public $db_fields = array( 'parent' => 'parent', 'id' => 'term_id', 'slug' => 'slug', ); /** * Starts the list before the elements are added. * * @see Walker::start_el() * @since 2.1.0 * * @param string $output Passed by reference. Used to append additional content. * @param object $cat Category. * @param int $depth Depth of category in reference to parents. * @param array $args Arguments. * @param int $current_object_id Current object ID. */ public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) { if ( ! empty( $args['hierarchical'] ) ) { $pad = str_repeat( ' ', $depth * 3 ); } else { $pad = ''; } $cat_name = apply_filters( 'list_product_cats', $cat->name, $cat ); $value = ( isset( $args['value'] ) && 'id' === $args['value'] ) ? $cat->term_id : $cat->slug; $output .= "\t<option class=\"level-$depth\" value=\"" . esc_attr( $value ) . '"'; if ( $value === $args['selected'] || ( is_array( $args['selected'] ) && in_array( $value, $args['selected'], true ) ) ) { $output .= ' selected="selected"'; } $output .= '>'; $output .= esc_html( $pad . $cat_name ); if ( ! empty( $args['show_count'] ) ) { $output .= ' (' . absint( $cat->count ) . ')'; } $output .= "</option>\n"; } /** * Traverse elements to create list from elements. * * Display one element if the element doesn't have any children otherwise, * display the element and its children. Will only traverse up to the max. * depth and no ignore elements under that depth. It is possible to set the. * max depth to include all depths, see walk() method. * * This method shouldn't be called directly, use the walk() method instead. * * @since 2.5.0 * * @param object $element Data object. * @param array $children_elements List of elements to continue traversing. * @param int $max_depth Max depth to traverse. * @param int $depth Depth of current element. * @param array $args Arguments. * @param string $output Passed by reference. Used to append additional content. * @return null Null on failure with no changes to parameters. */ public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) { return; } parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); } } includes/walkers/class-wc-product-cat-list-walker.php 0000644 00000011047 15132754524 0016741 0 ustar 00 <?php /** * WC_Product_Cat_List_Walker class * * @package WooCommerce\Classes\Walkers * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Product_Cat_List_Walker', false ) ) { return; } /** * Product cat list walker class. */ class WC_Product_Cat_List_Walker extends Walker { /** * What the class handles. * * @var string */ public $tree_type = 'product_cat'; /** * DB fields to use. * * @var array */ public $db_fields = array( 'parent' => 'parent', 'id' => 'term_id', 'slug' => 'slug', ); /** * Starts the list before the elements are added. * * @see Walker::start_lvl() * @since 2.1.0 * * @param string $output Passed by reference. Used to append additional content. * @param int $depth Depth of category. Used for tab indentation. * @param array $args Will only append content if style argument value is 'list'. */ public function start_lvl( &$output, $depth = 0, $args = array() ) { if ( 'list' !== $args['style'] ) { return; } $indent = str_repeat( "\t", $depth ); $output .= "$indent<ul class='children'>\n"; } /** * Ends the list of after the elements are added. * * @see Walker::end_lvl() * @since 2.1.0 * * @param string $output Passed by reference. Used to append additional content. * @param int $depth Depth of category. Used for tab indentation. * @param array $args Will only append content if style argument value is 'list'. */ public function end_lvl( &$output, $depth = 0, $args = array() ) { if ( 'list' !== $args['style'] ) { return; } $indent = str_repeat( "\t", $depth ); $output .= "$indent</ul>\n"; } /** * Start the element output. * * @see Walker::start_el() * @since 2.1.0 * * @param string $output Passed by reference. Used to append additional content. * @param object $cat Category. * @param int $depth Depth of category in reference to parents. * @param array $args Arguments. * @param integer $current_object_id Current object ID. */ public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) { $cat_id = intval( $cat->term_id ); $output .= '<li class="cat-item cat-item-' . $cat_id; if ( $args['current_category'] === $cat_id ) { $output .= ' current-cat'; } if ( $args['has_children'] && $args['hierarchical'] && ( empty( $args['max_depth'] ) || $args['max_depth'] > $depth + 1 ) ) { $output .= ' cat-parent'; } if ( $args['current_category_ancestors'] && $args['current_category'] && in_array( $cat_id, $args['current_category_ancestors'], true ) ) { $output .= ' current-cat-parent'; } $output .= '"><a href="' . get_term_link( $cat_id, $this->tree_type ) . '">' . apply_filters( 'list_product_cats', $cat->name, $cat ) . '</a>'; if ( $args['show_count'] ) { $output .= ' <span class="count">(' . $cat->count . ')</span>'; } } /** * Ends the element output, if needed. * * @see Walker::end_el() * @since 2.1.0 * * @param string $output Passed by reference. Used to append additional content. * @param object $cat Category. * @param int $depth Depth of category. Not used. * @param array $args Only uses 'list' for whether should append to output. */ public function end_el( &$output, $cat, $depth = 0, $args = array() ) { $output .= "</li>\n"; } /** * Traverse elements to create list from elements. * * Display one element if the element doesn't have any children otherwise, * display the element and its children. Will only traverse up to the max. * depth and no ignore elements under that depth. It is possible to set the. * max depth to include all depths, see walk() method. * * This method shouldn't be called directly, use the walk() method instead. * * @since 2.5.0 * * @param object $element Data object. * @param array $children_elements List of elements to continue traversing. * @param int $max_depth Max depth to traverse. * @param int $depth Depth of current element. * @param array $args Arguments. * @param string $output Passed by reference. Used to append additional content. * @return null Null on failure with no changes to parameters. */ public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) { return; } parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); } } includes/walkers/class-product-cat-dropdown-walker.php 0000644 00000000347 15132754524 0017214 0 ustar 00 <?php /** * Legacy WC_Product_Cat_Dropdown_Walker file * * @package WooCommerce\Classes\Walkers * @deprecated 3.4.0 */ defined( 'ABSPATH' ) || exit; require dirname( __FILE__ ) . '/class-wc-product-cat-dropdown-walker.php'; includes/walkers/class-product-cat-list-walker.php 0000644 00000000337 15132754524 0016332 0 ustar 00 <?php /** * Legacy WC_Product_Cat_List_Walker file * * @package WooCommerce\Classes\Walkers * @deprecated 3.4.0 */ defined( 'ABSPATH' ) || exit; require dirname( __FILE__ ) . '/class-wc-product-cat-list-walker.php'; includes/class-wc-shortcodes.php 0000644 00000042360 15132754524 0012767 0 ustar 00 <?php /** * Shortcodes * * @package WooCommerce\Classes * @version 3.2.0 */ defined( 'ABSPATH' ) || exit; /** * WooCommerce Shortcodes class. */ class WC_Shortcodes { /** * Init shortcodes. */ public static function init() { $shortcodes = array( 'product' => __CLASS__ . '::product', 'product_page' => __CLASS__ . '::product_page', 'product_category' => __CLASS__ . '::product_category', 'product_categories' => __CLASS__ . '::product_categories', 'add_to_cart' => __CLASS__ . '::product_add_to_cart', 'add_to_cart_url' => __CLASS__ . '::product_add_to_cart_url', 'products' => __CLASS__ . '::products', 'recent_products' => __CLASS__ . '::recent_products', 'sale_products' => __CLASS__ . '::sale_products', 'best_selling_products' => __CLASS__ . '::best_selling_products', 'top_rated_products' => __CLASS__ . '::top_rated_products', 'featured_products' => __CLASS__ . '::featured_products', 'product_attribute' => __CLASS__ . '::product_attribute', 'related_products' => __CLASS__ . '::related_products', 'shop_messages' => __CLASS__ . '::shop_messages', 'woocommerce_order_tracking' => __CLASS__ . '::order_tracking', 'woocommerce_cart' => __CLASS__ . '::cart', 'woocommerce_checkout' => __CLASS__ . '::checkout', 'woocommerce_my_account' => __CLASS__ . '::my_account', ); foreach ( $shortcodes as $shortcode => $function ) { add_shortcode( apply_filters( "{$shortcode}_shortcode_tag", $shortcode ), $function ); } // Alias for pre 2.1 compatibility. add_shortcode( 'woocommerce_messages', __CLASS__ . '::shop_messages' ); } /** * Shortcode Wrapper. * * @param string[] $function Callback function. * @param array $atts Attributes. Default to empty array. * @param array $wrapper Customer wrapper data. * * @return string */ public static function shortcode_wrapper( $function, $atts = array(), $wrapper = array( 'class' => 'woocommerce', 'before' => null, 'after' => null, ) ) { ob_start(); // @codingStandardsIgnoreStart echo empty( $wrapper['before'] ) ? '<div class="' . esc_attr( $wrapper['class'] ) . '">' : $wrapper['before']; call_user_func( $function, $atts ); echo empty( $wrapper['after'] ) ? '</div>' : $wrapper['after']; // @codingStandardsIgnoreEnd return ob_get_clean(); } /** * Cart page shortcode. * * @return string */ public static function cart() { return is_null( WC()->cart ) ? '' : self::shortcode_wrapper( array( 'WC_Shortcode_Cart', 'output' ) ); } /** * Checkout page shortcode. * * @param array $atts Attributes. * @return string */ public static function checkout( $atts ) { return self::shortcode_wrapper( array( 'WC_Shortcode_Checkout', 'output' ), $atts ); } /** * Order tracking page shortcode. * * @param array $atts Attributes. * @return string */ public static function order_tracking( $atts ) { return self::shortcode_wrapper( array( 'WC_Shortcode_Order_Tracking', 'output' ), $atts ); } /** * My account page shortcode. * * @param array $atts Attributes. * @return string */ public static function my_account( $atts ) { return self::shortcode_wrapper( array( 'WC_Shortcode_My_Account', 'output' ), $atts ); } /** * List products in a category shortcode. * * @param array $atts Attributes. * @return string */ public static function product_category( $atts ) { if ( empty( $atts['category'] ) ) { return ''; } $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'menu_order title', 'order' => 'ASC', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $shortcode = new WC_Shortcode_Products( $atts, 'product_category' ); return $shortcode->get_content(); } /** * List all (or limited) product categories. * * @param array $atts Attributes. * @return string */ public static function product_categories( $atts ) { if ( isset( $atts['number'] ) ) { $atts['limit'] = $atts['number']; } $atts = shortcode_atts( array( 'limit' => '-1', 'orderby' => 'name', 'order' => 'ASC', 'columns' => '4', 'hide_empty' => 1, 'parent' => '', 'ids' => '', ), $atts, 'product_categories' ); $ids = array_filter( array_map( 'trim', explode( ',', $atts['ids'] ) ) ); $hide_empty = ( true === $atts['hide_empty'] || 'true' === $atts['hide_empty'] || 1 === $atts['hide_empty'] || '1' === $atts['hide_empty'] ) ? 1 : 0; // Get terms and workaround WP bug with parents/pad counts. $args = array( 'orderby' => $atts['orderby'], 'order' => $atts['order'], 'hide_empty' => $hide_empty, 'include' => $ids, 'pad_counts' => true, 'child_of' => $atts['parent'], ); $product_categories = apply_filters( 'woocommerce_product_categories', get_terms( 'product_cat', $args ) ); if ( '' !== $atts['parent'] ) { $product_categories = wp_list_filter( $product_categories, array( 'parent' => $atts['parent'], ) ); } if ( $hide_empty ) { foreach ( $product_categories as $key => $category ) { if ( 0 === $category->count ) { unset( $product_categories[ $key ] ); } } } $atts['limit'] = '-1' === $atts['limit'] ? null : intval( $atts['limit'] ); if ( $atts['limit'] ) { $product_categories = array_slice( $product_categories, 0, $atts['limit'] ); } $columns = absint( $atts['columns'] ); wc_set_loop_prop( 'columns', $columns ); wc_set_loop_prop( 'is_shortcode', true ); ob_start(); if ( $product_categories ) { woocommerce_product_loop_start(); foreach ( $product_categories as $category ) { wc_get_template( 'content-product_cat.php', array( 'category' => $category, ) ); } woocommerce_product_loop_end(); } wc_reset_loop(); return '<div class="woocommerce columns-' . $columns . '">' . ob_get_clean() . '</div>'; } /** * Recent Products shortcode. * * @param array $atts Attributes. * @return string */ public static function recent_products( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'date', 'order' => 'DESC', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $shortcode = new WC_Shortcode_Products( $atts, 'recent_products' ); return $shortcode->get_content(); } /** * List multiple products shortcode. * * @param array $atts Attributes. * @return string */ public static function products( $atts ) { $atts = (array) $atts; $type = 'products'; // Allow list product based on specific cases. if ( isset( $atts['on_sale'] ) && wc_string_to_bool( $atts['on_sale'] ) ) { $type = 'sale_products'; } elseif ( isset( $atts['best_selling'] ) && wc_string_to_bool( $atts['best_selling'] ) ) { $type = 'best_selling_products'; } elseif ( isset( $atts['top_rated'] ) && wc_string_to_bool( $atts['top_rated'] ) ) { $type = 'top_rated_products'; } $shortcode = new WC_Shortcode_Products( $atts, $type ); return $shortcode->get_content(); } /** * Display a single product. * * @param array $atts Attributes. * @return string */ public static function product( $atts ) { if ( empty( $atts ) ) { return ''; } $atts['skus'] = isset( $atts['sku'] ) ? $atts['sku'] : ''; $atts['ids'] = isset( $atts['id'] ) ? $atts['id'] : ''; $atts['limit'] = '1'; $shortcode = new WC_Shortcode_Products( (array) $atts, 'product' ); return $shortcode->get_content(); } /** * Display a single product price + cart button. * * @param array $atts Attributes. * @return string */ public static function product_add_to_cart( $atts ) { global $post; if ( empty( $atts ) ) { return ''; } $atts = shortcode_atts( array( 'id' => '', 'class' => '', 'quantity' => '1', 'sku' => '', 'style' => 'border:4px solid #ccc; padding: 12px;', 'show_price' => 'true', ), $atts, 'product_add_to_cart' ); if ( ! empty( $atts['id'] ) ) { $product_data = get_post( $atts['id'] ); } elseif ( ! empty( $atts['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $atts['sku'] ); $product_data = get_post( $product_id ); } else { return ''; } $product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false; if ( ! $product ) { return ''; } ob_start(); echo '<p class="product woocommerce add_to_cart_inline ' . esc_attr( $atts['class'] ) . '" style="' . ( empty( $atts['style'] ) ? '' : esc_attr( $atts['style'] ) ) . '">'; if ( wc_string_to_bool( $atts['show_price'] ) ) { // @codingStandardsIgnoreStart echo $product->get_price_html(); // @codingStandardsIgnoreEnd } woocommerce_template_loop_add_to_cart( array( 'quantity' => $atts['quantity'], ) ); echo '</p>'; // Restore Product global in case this is shown inside a product post. wc_setup_product_data( $post ); return ob_get_clean(); } /** * Get the add to cart URL for a product. * * @param array $atts Attributes. * @return string */ public static function product_add_to_cart_url( $atts ) { if ( empty( $atts ) ) { return ''; } if ( isset( $atts['id'] ) ) { $product_data = get_post( $atts['id'] ); } elseif ( isset( $atts['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $atts['sku'] ); $product_data = get_post( $product_id ); } else { return ''; } $product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false; if ( ! $product ) { return ''; } $_product = wc_get_product( $product_data ); return esc_url( $_product->add_to_cart_url() ); } /** * List all products on sale. * * @param array $atts Attributes. * @return string */ public static function sale_products( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'title', 'order' => 'ASC', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $shortcode = new WC_Shortcode_Products( $atts, 'sale_products' ); return $shortcode->get_content(); } /** * List best selling products on sale. * * @param array $atts Attributes. * @return string */ public static function best_selling_products( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $shortcode = new WC_Shortcode_Products( $atts, 'best_selling_products' ); return $shortcode->get_content(); } /** * List top rated products on sale. * * @param array $atts Attributes. * @return string */ public static function top_rated_products( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'title', 'order' => 'ASC', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $shortcode = new WC_Shortcode_Products( $atts, 'top_rated_products' ); return $shortcode->get_content(); } /** * Output featured products. * * @param array $atts Attributes. * @return string */ public static function featured_products( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'date', 'order' => 'DESC', 'category' => '', 'cat_operator' => 'IN', ), (array) $atts ); $atts['visibility'] = 'featured'; $shortcode = new WC_Shortcode_Products( $atts, 'featured_products' ); return $shortcode->get_content(); } /** * Show a single product page. * * @param array $atts Attributes. * @return string */ public static function product_page( $atts ) { if ( empty( $atts ) ) { return ''; } if ( ! isset( $atts['id'] ) && ! isset( $atts['sku'] ) ) { return ''; } $args = array( 'posts_per_page' => 1, 'post_type' => 'product', 'post_status' => ( ! empty( $atts['status'] ) ) ? $atts['status'] : 'publish', 'ignore_sticky_posts' => 1, 'no_found_rows' => 1, ); if ( isset( $atts['sku'] ) ) { $args['meta_query'][] = array( 'key' => '_sku', 'value' => sanitize_text_field( $atts['sku'] ), 'compare' => '=', ); $args['post_type'] = array( 'product', 'product_variation' ); } if ( isset( $atts['id'] ) ) { $args['p'] = absint( $atts['id'] ); } // Don't render titles if desired. if ( isset( $atts['show_title'] ) && ! $atts['show_title'] ) { remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 ); } // Change form action to avoid redirect. add_filter( 'woocommerce_add_to_cart_form_action', '__return_empty_string' ); $single_product = new WP_Query( $args ); $preselected_id = '0'; // Check if sku is a variation. if ( isset( $atts['sku'] ) && $single_product->have_posts() && 'product_variation' === $single_product->post->post_type ) { $variation = wc_get_product_object( 'variation', $single_product->post->ID ); $attributes = $variation->get_attributes(); // Set preselected id to be used by JS to provide context. $preselected_id = $single_product->post->ID; // Get the parent product object. $args = array( 'posts_per_page' => 1, 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => 1, 'no_found_rows' => 1, 'p' => $single_product->post->post_parent, ); $single_product = new WP_Query( $args ); ?> <script type="text/javascript"> jQuery( function( $ ) { var $variations_form = $( '[data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>"]' ).find( 'form.variations_form' ); <?php foreach ( $attributes as $attr => $value ) { ?> $variations_form.find( 'select[name="<?php echo esc_attr( $attr ); ?>"]' ).val( '<?php echo esc_js( $value ); ?>' ); <?php } ?> }); </script> <?php } // For "is_single" to always make load comments_template() for reviews. $single_product->is_single = true; ob_start(); global $wp_query; // Backup query object so following loops think this is a product page. $previous_wp_query = $wp_query; // @codingStandardsIgnoreStart $wp_query = $single_product; // @codingStandardsIgnoreEnd wp_enqueue_script( 'wc-single-product' ); while ( $single_product->have_posts() ) { $single_product->the_post() ?> <div class="single-product" data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>"> <?php wc_get_template_part( 'content', 'single-product' ); ?> </div> <?php } // Restore $previous_wp_query and reset post data. // @codingStandardsIgnoreStart $wp_query = $previous_wp_query; // @codingStandardsIgnoreEnd wp_reset_postdata(); // Re-enable titles if they were removed. if ( isset( $atts['show_title'] ) && ! $atts['show_title'] ) { add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 ); } remove_filter( 'woocommerce_add_to_cart_form_action', '__return_empty_string' ); return '<div class="woocommerce">' . ob_get_clean() . '</div>'; } /** * Show messages. * * @return string */ public static function shop_messages() { if ( ! function_exists( 'wc_print_notices' ) ) { return ''; } return '<div class="woocommerce">' . wc_print_notices( true ) . '</div>'; } /** * Order by rating. * * @deprecated 3.2.0 Use WC_Shortcode_Products::order_by_rating_post_clauses(). * @param array $args Query args. * @return array */ public static function order_by_rating_post_clauses( $args ) { return WC_Shortcode_Products::order_by_rating_post_clauses( $args ); } /** * List products with an attribute shortcode. * Example [product_attribute attribute="color" filter="black"]. * * @param array $atts Attributes. * @return string */ public static function product_attribute( $atts ) { $atts = array_merge( array( 'limit' => '12', 'columns' => '4', 'orderby' => 'title', 'order' => 'ASC', 'attribute' => '', 'terms' => '', ), (array) $atts ); if ( empty( $atts['attribute'] ) ) { return ''; } $shortcode = new WC_Shortcode_Products( $atts, 'product_attribute' ); return $shortcode->get_content(); } /** * List related products. * * @param array $atts Attributes. * @return string */ public static function related_products( $atts ) { if ( isset( $atts['per_page'] ) ) { $atts['limit'] = $atts['per_page']; } // @codingStandardsIgnoreStart $atts = shortcode_atts( array( 'limit' => '4', 'columns' => '4', 'orderby' => 'rand', ), $atts, 'related_products' ); // @codingStandardsIgnoreEnd ob_start(); // Rename arg. $atts['posts_per_page'] = absint( $atts['limit'] ); woocommerce_related_products( $atts ); return ob_get_clean(); } } includes/queue/class-wc-queue.php 0000644 00000003645 15132754524 0013065 0 ustar 00 <?php /** * WC Queue * * @version 3.5.0 * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC Queue * * Singleton for managing the WC queue instance. * * @version 3.5.0 */ class WC_Queue { /** * The single instance of the queue. * * @var WC_Queue_Interface|null */ protected static $instance = null; /** * The default queue class to initialize * * @var string */ protected static $default_cass = 'WC_Action_Queue'; /** * Single instance of WC_Queue_Interface * * @return WC_Queue_Interface */ final public static function instance() { if ( is_null( self::$instance ) ) { $class = self::get_class(); self::$instance = new $class(); self::$instance = self::validate_instance( self::$instance ); } return self::$instance; } /** * Get class to instantiate * * And make sure 3rd party code has the chance to attach a custom queue class. * * @return string */ protected static function get_class() { if ( ! did_action( 'plugins_loaded' ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before plugins_loaded.', 'woocommerce' ), '3.5.0' ); } return apply_filters( 'woocommerce_queue_class', self::$default_cass ); } /** * Enforce a WC_Queue_Interface * * @param WC_Queue_Interface $instance Instance class. * @return WC_Queue_Interface */ protected static function validate_instance( $instance ) { if ( false === ( $instance instanceof WC_Queue_Interface ) ) { $default_class = self::$default_cass; /* translators: %s: Default class name */ wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'The class attached to the "woocommerce_queue_class" does not implement the WC_Queue_Interface interface. The default %s class will be used instead.', 'woocommerce' ), $default_class ), '3.5.0' ); $instance = new $default_class(); } return $instance; } } includes/queue/class-wc-action-queue.php 0000644 00000015434 15132754524 0014337 0 ustar 00 <?php /** * Action Queue * * @version 3.5.0 * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC Action Queue * * A job queue using WordPress actions. * * @version 3.5.0 */ class WC_Action_Queue implements WC_Queue_Interface { /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID. */ public function add( $hook, $args = array(), $group = '' ) { return $this->schedule_single( time(), $hook, $args, $group ); } /** * Schedule an action to run once at some time in the future * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID. */ public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) { return as_schedule_single_action( $timestamp, $hook, $args, $group ); } /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID. */ public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $cron_schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID */ public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) { return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $group ); } /** * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). * * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. * * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled * only after the former action is run. As the next instance is never run, because it's unscheduled by this function, * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled * by this method also. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to (if any). */ public function cancel( $hook, $args = array(), $group = '' ) { as_unschedule_action( $hook, $args, $group ); } /** * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to (if any). */ public function cancel_all( $hook, $args = array(), $group = '' ) { as_unschedule_all_actions( $hook, $args, $group ); } /** * Get the date and time for the next scheduled occurence of an action with a given hook * (an optionally that matches certain args and group), if any. * * @param string $hook The hook that the job will trigger. * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. * @param string $group Filter to only actions assigned to a specific group. * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook. */ public function get_next( $hook, $args = null, $group = '' ) { $next_timestamp = as_next_scheduled_action( $hook, $args, $group ); if ( is_numeric( $next_timestamp ) ) { return new WC_DateTime( "@{$next_timestamp}", new DateTimeZone( 'UTC' ) ); } return null; } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values: * 'hook' => '' - the name of the action that will be triggered * 'args' => null - the args array that will be passed with the action * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'group' => '' - the group the action belongs to * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID * 'per_page' => 5 - Number of results to return * 'offset' => 0 * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' * 'order' => 'ASC'. * * @param string $return_format OBJECT, ARRAY_A, or ids. * @return array */ public function search( $args = array(), $return_format = OBJECT ) { return as_get_scheduled_actions( $args, $return_format ); } } includes/class-wc-privacy-background-process.php 0000644 00000003253 15132754524 0016056 0 ustar 00 <?php /** * Order cleanup background process. * * @package WooCommerce\Classes * @version 3.4.0 * @since 3.4.0 */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Background_Process', false ) ) { include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php'; } /** * WC_Privacy_Background_Process class. */ class WC_Privacy_Background_Process extends WC_Background_Process { /** * Initiate new background process. */ public function __construct() { // Uses unique prefix per blog so each blog has separate queue. $this->prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_privacy_cleanup'; parent::__construct(); } /** * Code to execute for each item in the queue * * @param string $item Queue item to iterate over. * @return bool */ protected function task( $item ) { if ( ! $item || empty( $item['task'] ) ) { return false; } $process_count = 0; $process_limit = 20; switch ( $item['task'] ) { case 'trash_pending_orders': $process_count = WC_Privacy::trash_pending_orders( $process_limit ); break; case 'trash_failed_orders': $process_count = WC_Privacy::trash_failed_orders( $process_limit ); break; case 'trash_cancelled_orders': $process_count = WC_Privacy::trash_cancelled_orders( $process_limit ); break; case 'anonymize_completed_orders': $process_count = WC_Privacy::anonymize_completed_orders( $process_limit ); break; case 'delete_inactive_accounts': $process_count = WC_Privacy::delete_inactive_accounts( $process_limit ); break; } if ( $process_limit === $process_count ) { // Needs to run again. return $item; } return false; } } includes/emails/class-wc-email-customer-note.php 0000644 00000007725 15132754524 0015763 0 ustar 00 <?php /** * Class WC_Email_Customer_Note file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Note', false ) ) : /** * Customer Note Order Email. * * Customer note emails are sent when you add a note to an order. * * @class WC_Email_Customer_Note * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Note extends WC_Email { /** * Customer note. * * @var string */ public $customer_note; /** * Constructor. */ public function __construct() { $this->id = 'customer_note'; $this->customer_email = true; $this->title = __( 'Customer note', 'woocommerce' ); $this->description = __( 'Customer note emails are sent when you add a note to an order.', 'woocommerce' ); $this->template_html = 'emails/customer-note.php'; $this->template_plain = 'emails/plain/customer-note.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers. add_action( 'woocommerce_new_customer_note_notification', array( $this, 'trigger' ) ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Note added to your {site_title} order from {order_date}', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'A note has been added to your order', 'woocommerce' ); } /** * Trigger. * * @param array $args Email arguments. */ public function trigger( $args ) { $this->setup_locale(); if ( ! empty( $args ) ) { $defaults = array( 'order_id' => '', 'customer_note' => '', ); $args = wp_parse_args( $args, $defaults ); $order_id = $args['order_id']; $customer_note = $args['customer_note']; if ( $order_id ) { $this->object = wc_get_order( $order_id ); if ( $this->object ) { $this->recipient = $this->object->get_billing_email(); $this->customer_note = $customer_note; $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } } } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'customer_note' => $this->customer_note, 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'customer_note' => $this->customer_note, 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for reading.', 'woocommerce' ); } } endif; return new WC_Email_Customer_Note(); includes/emails/class-wc-email-customer-on-hold-order.php 0000644 00000010024 15132754524 0017451 0 ustar 00 <?php /** * Class WC_Email_Customer_On_Hold_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_On_Hold_Order', false ) ) : /** * Customer On-hold Order Email. * * An email sent to the customer when a new order is on-hold for. * * @class WC_Email_Customer_On_Hold_Order * @version 2.6.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_On_Hold_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'customer_on_hold_order'; $this->customer_email = true; $this->title = __( 'Order on-hold', 'woocommerce' ); $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold.', 'woocommerce' ); $this->template_html = 'emails/customer-on-hold-order.php'; $this->template_plain = 'emails/plain/customer-on-hold-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Your {site_title} order has been received!', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Thank you for your order', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'We look forward to fulfilling your order soon.', 'woocommerce' ); } } endif; return new WC_Email_Customer_On_Hold_Order(); includes/emails/class-wc-email-customer-refunded-order.php 0000644 00000022070 15132754524 0017711 0 ustar 00 <?php /** * Class WC_Email_Customer_Refunded_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Refunded_Order', false ) ) : /** * Customer Refunded Order Email. * * Order refunded emails are sent to the customer when the order is marked refunded. * * @class WC_Email_Customer_Refunded_Order * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Refunded_Order extends WC_Email { /** * Refund order. * * @var WC_Order|bool */ public $refund; /** * Is the order partial refunded? * * @var bool */ public $partial_refund; /** * Constructor. */ public function __construct() { $this->customer_email = true; $this->id = 'customer_refunded_order'; $this->title = __( 'Refunded order', 'woocommerce' ); $this->description = __( 'Order refunded emails are sent to customers when their orders are refunded.', 'woocommerce' ); $this->template_html = 'emails/customer-refunded-order.php'; $this->template_plain = 'emails/plain/customer-refunded-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_fully_refunded_notification', array( $this, 'trigger_full' ), 10, 2 ); add_action( 'woocommerce_order_partially_refunded_notification', array( $this, 'trigger_partial' ), 10, 2 ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @param bool $partial Whether it is a partial refund or a full refund. * @since 3.1.0 * @return string */ public function get_default_subject( $partial = false ) { if ( $partial ) { return __( 'Your {site_title} order #{order_number} has been partially refunded', 'woocommerce' ); } else { return __( 'Your {site_title} order #{order_number} has been refunded', 'woocommerce' ); } } /** * Get email heading. * * @param bool $partial Whether it is a partial refund or a full refund. * @since 3.1.0 * @return string */ public function get_default_heading( $partial = false ) { if ( $partial ) { return __( 'Partial Refund: Order {order_number}', 'woocommerce' ); } else { return __( 'Order Refunded: {order_number}', 'woocommerce' ); } } /** * Get email subject. * * @return string */ public function get_subject() { if ( $this->partial_refund ) { $subject = $this->get_option( 'subject_partial', $this->get_default_subject( true ) ); } else { $subject = $this->get_option( 'subject_full', $this->get_default_subject() ); } return apply_filters( 'woocommerce_email_subject_customer_refunded_order', $this->format_string( $subject ), $this->object, $this ); } /** * Get email heading. * * @return string */ public function get_heading() { if ( $this->partial_refund ) { $heading = $this->get_option( 'heading_partial', $this->get_default_heading( true ) ); } else { $heading = $this->get_option( 'heading_full', $this->get_default_heading() ); } return apply_filters( 'woocommerce_email_heading_customer_refunded_order', $this->format_string( $heading ), $this->object, $this ); } /** * Set email strings. * * @param bool $partial_refund Whether it is a partial refund or a full refund. * @deprecated 3.1.0 Unused. */ public function set_email_strings( $partial_refund = false ) {} /** * Full refund notification. * * @param int $order_id Order ID. * @param int $refund_id Refund ID. */ public function trigger_full( $order_id, $refund_id = null ) { $this->trigger( $order_id, false, $refund_id ); } /** * Partial refund notification. * * @param int $order_id Order ID. * @param int $refund_id Refund ID. */ public function trigger_partial( $order_id, $refund_id = null ) { $this->trigger( $order_id, true, $refund_id ); } /** * Trigger. * * @param int $order_id Order ID. * @param bool $partial_refund Whether it is a partial refund or a full refund. * @param int $refund_id Refund ID. */ public function trigger( $order_id, $partial_refund = false, $refund_id = null ) { $this->setup_locale(); $this->partial_refund = $partial_refund; $this->id = $this->partial_refund ? 'customer_partially_refunded_order' : 'customer_refunded_order'; if ( $order_id ) { $this->object = wc_get_order( $order_id ); $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( ! empty( $refund_id ) ) { $this->refund = wc_get_order( $refund_id ); } else { $this->refund = false; } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'refund' => $this->refund, 'partial_refund' => $this->partial_refund, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'refund' => $this->refund, 'partial_refund' => $this->partial_refund, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'We hope to see you again soon.', 'woocommerce' ); } /** * Initialise settings form fields. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' ); $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce' ), 'default' => 'yes', ), 'subject_full' => array( 'title' => __( 'Full refund subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'subject_partial' => array( 'title' => __( 'Partial refund subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject( true ), 'default' => '', ), 'heading_full' => array( 'title' => __( 'Full refund email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'heading_partial' => array( 'title' => __( 'Partial refund email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading( true ), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } } endif; return new WC_Email_Customer_Refunded_Order(); includes/emails/class-wc-email-cancelled-order.php 0000644 00000014643 15132754524 0016177 0 ustar 00 <?php /** * Class WC_Email_Cancelled_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Email_Cancelled_Order', false ) ) : /** * Cancelled Order Email. * * An email sent to the admin when an order is cancelled. * * @class WC_Email_Cancelled_Order * @version 2.2.7 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Cancelled_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'cancelled_order'; $this->title = __( 'Cancelled order', 'woocommerce' ); $this->description = __( 'Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).', 'woocommerce' ); $this->template_html = 'emails/admin-cancelled-order.php'; $this->template_plain = 'emails/plain/admin-cancelled-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', '{order_billing_full_name}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_processing_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_on-hold_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); // Other settings. $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( '[{site_title}]: Order #{order_number} has been cancelled', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Order Cancelled: #{order_number}', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); $this->placeholders['{order_billing_full_name}'] = $this->object->get_formatted_billing_full_name(); } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for reading.', 'woocommerce' ); } /** * Initialise settings form fields. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' ); $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce' ), 'default' => 'yes', ), 'recipient' => array( 'title' => __( 'Recipient(s)', 'woocommerce' ), 'type' => 'text', /* translators: %s: admin email */ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ), 'placeholder' => '', 'default' => '', 'desc_tip' => true, ), 'subject' => array( 'title' => __( 'Subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => __( 'Email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } } endif; return new WC_Email_Cancelled_Order(); includes/emails/class-wc-email-new-order.php 0000644 00000016433 15132754524 0015055 0 ustar 00 <?php /** * Class WC_Email_New_Order file * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Email_New_Order' ) ) : /** * New Order Email. * * An email sent to the admin when a new order is received/paid for. * * @class WC_Email_New_Order * @version 2.0.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_New_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'new_order'; $this->title = __( 'New order', 'woocommerce' ); $this->description = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' ); $this->template_html = 'emails/admin-new-order.php'; $this->template_plain = 'emails/plain/admin-new-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_cancelled_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); // Other settings. $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( '[{site_title}]: New order #{order_number}', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'New Order: #{order_number}', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); $email_already_sent = $order->get_meta( '_new_order_email_sent' ); } /** * Controls if new order emails can be resend multiple times. * * @since 5.0.0 * @param bool $allows Defaults to false. */ if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) { return; } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); $order->update_meta_data( '_new_order_email_sent', 'true' ); $order->save(); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Congratulations on the sale.', 'woocommerce' ); } /** * Initialise settings form fields. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . implode( '</code>, <code>', array_keys( $this->placeholders ) ) . '</code>' ); $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce' ), 'default' => 'yes', ), 'recipient' => array( 'title' => __( 'Recipient(s)', 'woocommerce' ), 'type' => 'text', /* translators: %s: WP admin email */ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ), 'placeholder' => '', 'default' => '', 'desc_tip' => true, ), 'subject' => array( 'title' => __( 'Subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => __( 'Email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } } endif; return new WC_Email_New_Order(); includes/emails/class-wc-email-customer-completed-order.php 0000644 00000007627 15132754524 0020104 0 ustar 00 <?php /** * Class WC_Email_Customer_Completed_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Completed_Order', false ) ) : /** * Customer Completed Order Email. * * Order complete emails are sent to the customer when the order is marked complete and usual indicates that the order has been shipped. * * @class WC_Email_Customer_Completed_Order * @version 2.0.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Completed_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'customer_completed_order'; $this->customer_email = true; $this->title = __( 'Completed order', 'woocommerce' ); $this->description = __( 'Order complete emails are sent to customers when their orders are marked completed and usually indicate that their orders have been shipped.', 'woocommerce' ); $this->template_html = 'emails/customer-completed-order.php'; $this->template_plain = 'emails/plain/customer-completed-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Your {site_title} order is now complete', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Thanks for shopping with us', 'woocommerce' ); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for shopping with us.', 'woocommerce' ); } } endif; return new WC_Email_Customer_Completed_Order(); includes/emails/class-wc-email-customer-reset-password.php 0000644 00000010061 15132754524 0017763 0 ustar 00 <?php /** * Class WC_Email_Customer_Reset_Password file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Reset_Password', false ) ) : /** * Customer Reset Password. * * An email sent to the customer when they reset their password. * * @class WC_Email_Customer_Reset_Password * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Reset_Password extends WC_Email { /** * User ID. * * @var integer */ public $user_id; /** * User login name. * * @var string */ public $user_login; /** * User email. * * @var string */ public $user_email; /** * Reset key. * * @var string */ public $reset_key; /** * Constructor. */ public function __construct() { $this->id = 'customer_reset_password'; $this->customer_email = true; $this->title = __( 'Reset password', 'woocommerce' ); $this->description = __( 'Customer "reset password" emails are sent when customers reset their passwords.', 'woocommerce' ); $this->template_html = 'emails/customer-reset-password.php'; $this->template_plain = 'emails/plain/customer-reset-password.php'; // Trigger. add_action( 'woocommerce_reset_password_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Password Reset Request for {site_title}', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Password Reset Request', 'woocommerce' ); } /** * Trigger. * * @param string $user_login User login. * @param string $reset_key Password reset key. */ public function trigger( $user_login = '', $reset_key = '' ) { $this->setup_locale(); if ( $user_login && $reset_key ) { $this->object = get_user_by( 'login', $user_login ); $this->user_id = $this->object->ID; $this->user_login = $user_login; $this->reset_key = $reset_key; $this->user_email = stripslashes( $this->object->user_email ); $this->recipient = $this->user_email; } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'email_heading' => $this->get_heading(), 'user_id' => $this->user_id, 'user_login' => $this->user_login, 'reset_key' => $this->reset_key, 'blogname' => $this->get_blogname(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'email_heading' => $this->get_heading(), 'user_id' => $this->user_id, 'user_login' => $this->user_login, 'reset_key' => $this->reset_key, 'blogname' => $this->get_blogname(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for reading.', 'woocommerce' ); } } endif; return new WC_Email_Customer_Reset_Password(); includes/emails/class-wc-email-customer-invoice.php 0000644 00000016636 15132754524 0016453 0 ustar 00 <?php /** * Class WC_Email_Customer_Invoice file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Invoice', false ) ) : /** * Customer Invoice. * * An email sent to the customer via admin. * * @class WC_Email_Customer_Invoice * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Invoice extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'customer_invoice'; $this->customer_email = true; $this->title = __( 'Customer invoice / Order details', 'woocommerce' ); $this->description = __( 'Customer invoice emails can be sent to customers containing their order information and payment links.', 'woocommerce' ); $this->template_html = 'emails/customer-invoice.php'; $this->template_plain = 'emails/plain/customer-invoice.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Call parent constructor. parent::__construct(); $this->manual = true; } /** * Get email subject. * * @param bool $paid Whether the order has been paid or not. * @since 3.1.0 * @return string */ public function get_default_subject( $paid = false ) { if ( $paid ) { return __( 'Invoice for order #{order_number} on {site_title}', 'woocommerce' ); } else { return __( 'Your latest {site_title} invoice', 'woocommerce' ); } } /** * Get email heading. * * @param bool $paid Whether the order has been paid or not. * @since 3.1.0 * @return string */ public function get_default_heading( $paid = false ) { if ( $paid ) { return __( 'Invoice for order #{order_number}', 'woocommerce' ); } else { return __( 'Your invoice for order #{order_number}', 'woocommerce' ); } } /** * Get email subject. * * @return string */ public function get_subject() { if ( $this->object->has_status( array( 'completed', 'processing' ) ) ) { $subject = $this->get_option( 'subject_paid', $this->get_default_subject( true ) ); return apply_filters( 'woocommerce_email_subject_customer_invoice_paid', $this->format_string( $subject ), $this->object, $this ); } $subject = $this->get_option( 'subject', $this->get_default_subject() ); return apply_filters( 'woocommerce_email_subject_customer_invoice', $this->format_string( $subject ), $this->object, $this ); } /** * Get email heading. * * @return string */ public function get_heading() { if ( $this->object->has_status( wc_get_is_paid_statuses() ) ) { $heading = $this->get_option( 'heading_paid', $this->get_default_heading( true ) ); return apply_filters( 'woocommerce_email_heading_customer_invoice_paid', $this->format_string( $heading ), $this->object, $this ); } $heading = $this->get_option( 'heading', $this->get_default_heading() ); return apply_filters( 'woocommerce_email_heading_customer_invoice', $this->format_string( $heading ), $this->object, $this ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for using {site_url}!', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Initialise settings form fields. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' ); $this->form_fields = array( 'subject' => array( 'title' => __( 'Subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => __( 'Email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'subject_paid' => array( 'title' => __( 'Subject (paid)', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject( true ), 'default' => '', ), 'heading_paid' => array( 'title' => __( 'Email heading (paid)', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading( true ), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } } endif; return new WC_Email_Customer_Invoice(); includes/emails/class-wc-email-customer-new-account.php 0000644 00000010237 15132754524 0017231 0 ustar 00 <?php /** * Class WC_Email_Customer_New_Account file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_New_Account', false ) ) : /** * Customer New Account. * * An email sent to the customer when they create an account. * * @class WC_Email_Customer_New_Account * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_New_Account extends WC_Email { /** * User login name. * * @var string */ public $user_login; /** * User email. * * @var string */ public $user_email; /** * User password. * * @var string */ public $user_pass; /** * Is the password generated? * * @var bool */ public $password_generated; /** * Constructor. */ public function __construct() { $this->id = 'customer_new_account'; $this->customer_email = true; $this->title = __( 'New account', 'woocommerce' ); $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' ); $this->template_html = 'emails/customer-new-account.php'; $this->template_plain = 'emails/plain/customer-new-account.php'; // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Your {site_title} account has been created!', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Welcome to {site_title}', 'woocommerce' ); } /** * Trigger. * * @param int $user_id User ID. * @param string $user_pass User password. * @param bool $password_generated Whether the password was generated automatically or not. */ public function trigger( $user_id, $user_pass = '', $password_generated = false ) { $this->setup_locale(); if ( $user_id ) { $this->object = new WP_User( $user_id ); $this->user_pass = $user_pass; $this->user_login = stripslashes( $this->object->user_login ); $this->user_email = stripslashes( $this->object->user_email ); $this->recipient = $this->user_email; $this->password_generated = $password_generated; } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'user_login' => $this->user_login, 'user_pass' => $this->user_pass, 'blogname' => $this->get_blogname(), 'password_generated' => $this->password_generated, 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'user_login' => $this->user_login, 'user_pass' => $this->user_pass, 'blogname' => $this->get_blogname(), 'password_generated' => $this->password_generated, 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'We look forward to seeing you soon.', 'woocommerce' ); } } endif; return new WC_Email_Customer_New_Account(); includes/emails/class-wc-email-customer-processing-order.php 0000644 00000010206 15132754524 0020267 0 ustar 00 <?php /** * Class WC_Email_Customer_Processing_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Email_Customer_Processing_Order', false ) ) : /** * Customer Processing Order Email. * * An email sent to the customer when a new order is paid for. * * @class WC_Email_Customer_Processing_Order * @version 3.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Processing_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'customer_processing_order'; $this->customer_email = true; $this->title = __( 'Processing order', 'woocommerce' ); $this->description = __( 'This is an order notification sent to customers containing order details after payment.', 'woocommerce' ); $this->template_html = 'emails/customer-processing-order.php'; $this->template_plain = 'emails/plain/customer-processing-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_on-hold_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Your {site_title} order has been received!', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Thank you for your order', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Thanks for using {site_url}!', 'woocommerce' ); } } endif; return new WC_Email_Customer_Processing_Order(); includes/emails/class-wc-email.php 0000644 00000101335 15132754524 0013151 0 ustar 00 <?php /** * Class WC_Email file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Email', false ) ) { return; } /** * Email Class * * WooCommerce Email Class which is extended by specific email template classes to add emails to WooCommerce * * @class WC_Email * @version 2.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Settings_API */ class WC_Email extends WC_Settings_API { /** * Email method ID. * * @var String */ public $id; /** * Email method title. * * @var string */ public $title; /** * 'yes' if the method is enabled. * * @var string yes, no */ public $enabled; /** * Description for the email. * * @var string */ public $description; /** * Default heading. * * Supported for backwards compatibility but we recommend overloading the * get_default_x methods instead so localization can be done when needed. * * @var string */ public $heading = ''; /** * Default subject. * * Supported for backwards compatibility but we recommend overloading the * get_default_x methods instead so localization can be done when needed. * * @var string */ public $subject = ''; /** * Plain text template path. * * @var string */ public $template_plain; /** * HTML template path. * * @var string */ public $template_html; /** * Template path. * * @var string */ public $template_base; /** * Recipients for the email. * * @var string */ public $recipient; /** * Object this email is for, for example a customer, product, or email. * * @var object|bool */ public $object; /** * Mime boundary (for multipart emails). * * @var string */ public $mime_boundary; /** * Mime boundary header (for multipart emails). * * @var string */ public $mime_boundary_header; /** * True when email is being sent. * * @var bool */ public $sending; /** * True when the email notification is sent manually only. * * @var bool */ protected $manual = false; /** * True when the email notification is sent to customers. * * @var bool */ protected $customer_email = false; /** * List of preg* regular expression patterns to search for, * used in conjunction with $plain_replace. * https://raw.github.com/ushahidi/wp-silcc/master/class.html2text.inc * * @var array $plain_search * @see $plain_replace */ public $plain_search = array( "/\r/", // Non-legal carriage return. '/&(nbsp|#0*160);/i', // Non-breaking space. '/&(quot|rdquo|ldquo|#0*8220|#0*8221|#0*147|#0*148);/i', // Double quotes. '/&(apos|rsquo|lsquo|#0*8216|#0*8217);/i', // Single quotes. '/>/i', // Greater-than. '/</i', // Less-than. '/�*38;/i', // Ampersand. '/&/i', // Ampersand. '/&(copy|#0*169);/i', // Copyright. '/&(trade|#0*8482|#0*153);/i', // Trademark. '/&(reg|#0*174);/i', // Registered. '/&(mdash|#0*151|#0*8212);/i', // mdash. '/&(ndash|minus|#0*8211|#0*8722);/i', // ndash. '/&(bull|#0*149|#0*8226);/i', // Bullet. '/&(pound|#0*163);/i', // Pound sign. '/&(euro|#0*8364);/i', // Euro sign. '/&(dollar|#0*36);/i', // Dollar sign. '/&[^&\s;]+;/i', // Unknown/unhandled entities. '/[ ]{2,}/', // Runs of spaces, post-handling. ); /** * List of pattern replacements corresponding to patterns searched. * * @var array $plain_replace * @see $plain_search */ public $plain_replace = array( '', // Non-legal carriage return. ' ', // Non-breaking space. '"', // Double quotes. "'", // Single quotes. '>', // Greater-than. '<', // Less-than. '&', // Ampersand. '&', // Ampersand. '(c)', // Copyright. '(tm)', // Trademark. '(R)', // Registered. '--', // mdash. '-', // ndash. '*', // Bullet. '£', // Pound sign. 'EUR', // Euro sign. € ?. '$', // Dollar sign. '', // Unknown/unhandled entities. ' ', // Runs of spaces, post-handling. ); /** * Strings to find/replace in subjects/headings. * * @var array */ protected $placeholders = array(); /** * Strings to find in subjects/headings. * * @deprecated 3.2.0 in favour of placeholders * @var array */ public $find = array(); /** * Strings to replace in subjects/headings. * * @deprecated 3.2.0 in favour of placeholders * @var array */ public $replace = array(); /** * Constructor. */ public function __construct() { // Find/replace. $this->placeholders = array_merge( array( '{site_title}' => $this->get_blogname(), '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ), '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ), ), $this->placeholders ); // Init settings. $this->init_form_fields(); $this->init_settings(); // Default template base if not declared in child constructor. if ( is_null( $this->template_base ) ) { $this->template_base = WC()->plugin_path() . '/templates/'; } $this->email_type = $this->get_option( 'email_type' ); $this->enabled = $this->get_option( 'enabled' ); add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) ); add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Handle multipart mail. * * @param PHPMailer $mailer PHPMailer object. * @return PHPMailer */ public function handle_multipart( $mailer ) { if ( $this->sending && 'multipart' === $this->get_email_type() ) { $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ) ); $this->sending = false; } return $mailer; } /** * Format email string. * * @param mixed $string Text to replace placeholders in. * @return string */ public function format_string( $string ) { $find = array_keys( $this->placeholders ); $replace = array_values( $this->placeholders ); // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0. $find = array_merge( (array) $this->find, $find ); $replace = array_merge( (array) $this->replace, $replace ); // Take care of blogname which is no longer defined as a valid placeholder. $find[] = '{blogname}'; $replace[] = $this->get_blogname(); // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0. if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) { $legacy_find = $this->find; $legacy_replace = $this->replace; foreach ( $this->placeholders as $find => $replace ) { $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) ); $legacy_find[ $legacy_key ] = $find; $legacy_replace[ $legacy_key ] = $replace; } $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string ); } /** * Filter for main find/replace. * * @since 3.2.0 */ return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this ); } /** * Set the locale to the store locale for customer emails to make sure emails are in the store language. */ public function setup_locale() { if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { wc_switch_to_site_locale(); } } /** * Restore the locale to the default locale. Use after finished with setup_locale. */ public function restore_locale() { if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { wc_restore_locale(); } } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return $this->subject; } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return $this->heading; } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return ''; } /** * Return content from the additional_content field. * * Displayed above the footer. * * @since 3.7.0 * @return string */ public function get_additional_content() { $content = $this->get_option( 'additional_content', '' ); return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); } /** * Get email subject. * * @return string */ public function get_subject() { return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this ); } /** * Get email heading. * * @return string */ public function get_heading() { return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this ); } /** * Get valid recipients. * * @return string */ public function get_recipient() { $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this ); $recipients = array_map( 'trim', explode( ',', $recipient ) ); $recipients = array_filter( $recipients, 'is_email' ); return implode( ', ', $recipients ); } /** * Get email headers. * * @return string */ public function get_headers() { $header = 'Content-Type: ' . $this->get_content_type() . "\r\n"; if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) { if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) { $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n"; } } elseif ( $this->get_from_address() && $this->get_from_name() ) { $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n"; } return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this ); } /** * Get email attachments. * * @return array */ public function get_attachments() { return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this ); } /** * Return email type. * * @return string */ public function get_email_type() { return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain'; } /** * Get email content type. * * @param string $default_content_type Default wp_mail() content type. * @return string */ public function get_content_type( $default_content_type = '' ) { switch ( $this->get_email_type() ) { case 'html': $content_type = 'text/html'; break; case 'multipart': $content_type = 'multipart/alternative'; break; default: $content_type = 'text/plain'; break; } return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type ); } /** * Return the email's title * * @return string */ public function get_title() { return apply_filters( 'woocommerce_email_title', $this->title, $this ); } /** * Return the email's description * * @return string */ public function get_description() { return apply_filters( 'woocommerce_email_description', $this->description, $this ); } /** * Proxy to parent's get_option and attempt to localize the result using gettext. * * @param string $key Option key. * @param mixed $empty_value Value to use when option is empty. * @return string */ public function get_option( $key, $empty_value = null ) { $value = parent::get_option( $key, $empty_value ); return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value ); } /** * Checks if this email is enabled and will be sent. * * @return bool */ public function is_enabled() { return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this ); } /** * Checks if this email is manually sent * * @return bool */ public function is_manual() { return $this->manual; } /** * Checks if this email is customer focussed. * * @return bool */ public function is_customer_email() { return $this->customer_email; } /** * Get WordPress blog name. * * @return string */ public function get_blogname() { return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); } /** * Get email content. * * @return string */ public function get_content() { $this->sending = true; if ( 'plain' === $this->get_email_type() ) { $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 ); } else { $email_content = $this->get_content_html(); } return $email_content; } /** * Apply inline styles to dynamic content. * * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported). * * @version 4.0.0 * @param string|null $content Content that will receive inline styles. * @return string */ public function style_inline( $content ) { if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) { ob_start(); wc_get_template( 'emails/email-styles.php' ); $css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this ); $emogrifier_class = 'Pelago\\Emogrifier'; if ( $this->supports_emogrifier() && class_exists( $emogrifier_class ) ) { try { $emogrifier = new $emogrifier_class( $content, $css ); do_action( 'woocommerce_emogrifier', $emogrifier, $this ); $content = $emogrifier->emogrify(); $html_prune = \Pelago\Emogrifier\HtmlProcessor\HtmlPruner::fromHtml( $content ); $html_prune->removeElementsWithDisplayNone(); $content = $html_prune->render(); } catch ( Exception $e ) { $logger = wc_get_logger(); $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) ); } } else { $content = '<style type="text/css">' . $css . '</style>' . $content; } } return $content; } /** * Return if emogrifier library is supported. * * @version 4.0.0 * @since 3.5.0 * @return bool */ protected function supports_emogrifier() { return class_exists( 'DOMDocument' ); } /** * Get the email content in plain text format. * * @return string */ public function get_content_plain() { return ''; } /** * Get the email content in HTML format. * * @return string */ public function get_content_html() { return ''; } /** * Get the from name for outgoing emails. * * @param string $from_name Default wp_mail() name associated with the "from" email address. * @return string */ public function get_from_name( $from_name = '' ) { $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name ); return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); } /** * Get the from address for outgoing emails. * * @param string $from_email Default wp_mail() email address to send from. * @return string */ public function get_from_address( $from_email = '' ) { $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email ); return sanitize_email( $from_email ); } /** * Send an email. * * @param string $to Email to. * @param string $subject Email subject. * @param string $message Email message. * @param string $headers Email headers. * @param array $attachments Email attachments. * @return bool success */ public function send( $to, $subject, $message, $headers, $attachments ) { add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this ); $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this ); $return = $mail_callback( ...$mail_callback_params ); remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); /** * Action hook fired when an email is sent. * * @since 5.6.0 * @param bool $return Whether the email was sent successfully. * @param int $id Email ID. * @param WC_Email $this WC_Email instance. */ do_action( 'woocommerce_email_sent', $return, $this->id, $this ); return $return; } /** * Initialise Settings Form Fields - these are generic email options most will use. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' ); $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce' ), 'default' => 'yes', ), 'subject' => array( 'title' => __( 'Subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => __( 'Email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } /** * Email type options. * * @return array */ public function get_email_type_options() { $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) ); if ( class_exists( 'DOMDocument' ) ) { $types['html'] = __( 'HTML', 'woocommerce' ); $types['multipart'] = __( 'Multipart', 'woocommerce' ); } return $types; } /** * Admin Panel Options Processing. */ public function process_admin_options() { // Save regular options. parent::process_admin_options(); $post_data = $this->get_post_data(); // Save templates. if ( isset( $post_data['template_html_code'] ) ) { $this->save_template( $post_data['template_html_code'], $this->template_html ); } if ( isset( $post_data['template_plain_code'] ) ) { $this->save_template( $post_data['template_plain_code'], $this->template_plain ); } } /** * Get template. * * @param string $type Template type. Can be either 'template_html' or 'template_plain'. * @return string */ public function get_template( $type ) { $type = basename( $type ); if ( 'template_html' === $type ) { return $this->template_html; } elseif ( 'template_plain' === $type ) { return $this->template_plain; } return ''; } /** * Save the email templates. * * @since 2.4.0 * @param string $template_code Template code. * @param string $template_path Template path. */ protected function save_template( $template_code, $template_path ) { if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) { $saved = false; $file = get_stylesheet_directory() . '/' . WC()->template_path() . $template_path; $code = wp_unslash( $template_code ); if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen if ( false !== $f ) { fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose $saved = true; } } if ( ! $saved ) { $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) ); wp_safe_redirect( $redirect ); exit; } } } /** * Get the template file in the current theme. * * @param string $template Template name. * * @return string */ public function get_theme_template_file( $template ) { return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template; } /** * Move template action. * * @param string $template_type Template type. */ protected function move_template_action( $template_type ) { $template = $this->get_template( $template_type ); if ( ! empty( $template ) ) { $theme_file = $this->get_theme_template_file( $template ); if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) { // Locate template file. $core_file = $this->template_base . $template; $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); // Copy template file. copy( $template_file, $theme_file ); /** * Action hook fired after copying email template file. * * @param string $template_type The copied template type * @param string $email The email object */ do_action( 'woocommerce_copy_email_template', $template_type, $this ); ?> <div class="updated"> <p><?php echo esc_html__( 'Template file copied to theme.', 'woocommerce' ); ?></p> </div> <?php } } } /** * Delete template action. * * @param string $template_type Template type. */ protected function delete_template_action( $template_type ) { $template = $this->get_template( $template_type ); if ( $template ) { if ( ! empty( $template ) ) { $theme_file = $this->get_theme_template_file( $template ); if ( file_exists( $theme_file ) ) { unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink /** * Action hook fired after deleting template file. * * @param string $template The deleted template type * @param string $email The email object */ do_action( 'woocommerce_delete_email_template', $template_type, $this ); ?> <div class="updated"> <p><?php echo esc_html__( 'Template file deleted from theme.', 'woocommerce' ); ?></p> </div> <?php } } } } /** * Admin actions. */ protected function admin_actions() { // Handle any actions. if ( ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) ) && 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated ) { if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) { wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } if ( ! current_user_can( 'edit_themes' ) ) { wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); } if ( ! empty( $_GET['move_template'] ) ) { $this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) ); } if ( ! empty( $_GET['delete_template'] ) ) { $this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) ); } } } /** * Admin Options. * * Setup the email settings screen. * Override this in your email. * * @since 1.0.0 */ public function admin_options() { // Do admin actions. $this->admin_actions(); ?> <h2><?php echo esc_html( $this->get_title() ); ?> <?php wc_back_link( __( 'Return to emails', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ); ?></h2> <?php echo wpautop( wp_kses_post( $this->get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> <?php /** * Action hook fired before displaying email settings. * * @param string $email The email object */ do_action( 'woocommerce_email_settings_before', $this ); ?> <table class="form-table"> <?php $this->generate_settings_html(); ?> </table> <?php /** * Action hook fired after displaying email settings. * * @param string $email The email object */ do_action( 'woocommerce_email_settings_after', $this ); ?> <?php if ( current_user_can( 'edit_themes' ) && ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) ) { ?> <div id="template"> <?php $templates = array( 'template_html' => __( 'HTML template', 'woocommerce' ), 'template_plain' => __( 'Plain text template', 'woocommerce' ), ); foreach ( $templates as $template_type => $title ) : $template = $this->get_template( $template_type ); if ( empty( $template ) ) { continue; } $local_file = $this->get_theme_template_file( $template ); $core_file = $this->template_base . $template; $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); $template_dir = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ); ?> <div class="template <?php echo esc_attr( $template_type ); ?>"> <h4><?php echo wp_kses_post( $title ); ?></h4> <?php if ( file_exists( $local_file ) ) : ?> <p> <a href="#" class="button toggle_editor"></a> <?php if ( is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?> <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'move_template', 'saved' ), add_query_arg( 'delete_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="delete_template button"> <?php esc_html_e( 'Delete template file', 'woocommerce' ); ?> </a> <?php endif; ?> <?php /* translators: %s: Path to template file */ printf( esc_html__( 'This template has been overridden by your theme and can be found in: %s.', 'woocommerce' ), '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' ); ?> </p> <div class="editor" style="display:none"> <textarea class="code" cols="25" rows="20" <?php if ( ! is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?> readonly="readonly" disabled="disabled" <?php else : ?> data-name="<?php echo esc_attr( $template_type ) . '_code'; ?>"<?php endif; ?>><?php echo esc_html( file_get_contents( $local_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea> </div> <?php elseif ( file_exists( $template_file ) ) : ?> <p> <a href="#" class="button toggle_editor"></a> <?php $emails_dir = get_stylesheet_directory() . '/' . $template_dir . '/emails'; $templates_dir = get_stylesheet_directory() . '/' . $template_dir; $theme_dir = get_stylesheet_directory(); if ( is_dir( $emails_dir ) ) { $target_dir = $emails_dir; } elseif ( is_dir( $templates_dir ) ) { $target_dir = $templates_dir; } else { $target_dir = $theme_dir; } if ( is_writable( $target_dir ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?> <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'delete_template', 'saved' ), add_query_arg( 'move_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="button"> <?php esc_html_e( 'Copy file to theme', 'woocommerce' ); ?> </a> <?php endif; ?> <?php /* translators: 1: Path to template file 2: Path to theme folder */ printf( esc_html__( 'To override and edit this email template copy %1$s to your theme folder: %2$s.', 'woocommerce' ), '<code>' . esc_html( plugin_basename( $template_file ) ) . '</code>', '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' ); ?> </p> <div class="editor" style="display:none"> <textarea class="code" readonly="readonly" disabled="disabled" cols="25" rows="20"><?php echo esc_html( file_get_contents( $template_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea> </div> <?php else : ?> <p><?php esc_html_e( 'File was not found.', 'woocommerce' ); ?></p> <?php endif; ?> </div> <?php endforeach; ?> </div> <?php wc_enqueue_js( "jQuery( 'select.email_type' ).on( 'change', function() { var val = jQuery( this ).val(); jQuery( '.template_plain, .template_html' ).show(); if ( val != 'multipart' && val != 'html' ) { jQuery('.template_html').hide(); } if ( val != 'multipart' && val != 'plain' ) { jQuery('.template_plain').hide(); } }).trigger( 'change' ); var view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "'; var hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "'; jQuery( 'a.toggle_editor' ).text( view ).on( 'click', function() { var label = hide; if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) { var label = view; } jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle(); return false; } ); jQuery( 'a.delete_template' ).on( 'click', function() { if ( window.confirm('" . esc_js( __( 'Are you sure you want to delete this template file?', 'woocommerce' ) ) . "') ) { return true; } return false; }); jQuery( '.editor textarea' ).on( 'change', function() { var name = jQuery( this ).attr( 'data-name' ); if ( name ) { jQuery( this ).attr( 'name', name ); } });" ); } } } includes/emails/class-wc-email-failed-order.php 0000644 00000014451 15132754524 0015506 0 ustar 00 <?php /** * Class WC_Email_Failed_Order file. * * @package WooCommerce\Emails */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Email_Failed_Order', false ) ) : /** * Failed Order Email. * * An email sent to the admin when payment fails to go through. * * @class WC_Email_Failed_Order * @version 2.5.0 * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Failed_Order extends WC_Email { /** * Constructor. */ public function __construct() { $this->id = 'failed_order'; $this->title = __( 'Failed order', 'woocommerce' ); $this->description = __( 'Failed order emails are sent to chosen recipient(s) when orders have been marked failed (if they were previously pending or on-hold).', 'woocommerce' ); $this->template_html = 'emails/admin-failed-order.php'; $this->template_plain = 'emails/plain/admin-failed-order.php'; $this->placeholders = array( '{order_date}' => '', '{order_number}' => '', ); // Triggers for this email. add_action( 'woocommerce_order_status_pending_to_failed_notification', array( $this, 'trigger' ), 10, 2 ); add_action( 'woocommerce_order_status_on-hold_to_failed_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor. parent::__construct(); // Other settings. $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( '[{site_title}]: Order #{order_number} has failed', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Order Failed: #{order_number}', 'woocommerce' ); } /** * Trigger the sending of this email. * * @param int $order_id The order ID. * @param WC_Order|false $order Order object. */ public function trigger( $order_id, $order = false ) { $this->setup_locale(); if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); } if ( is_a( $order, 'WC_Order' ) ) { $this->object = $order; $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => false, 'email' => $this, ) ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => true, 'plain_text' => true, 'email' => $this, ) ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'Hopefully they’ll be back. Read more about <a href="https://docs.woocommerce.com/document/managing-orders/">troubleshooting failed payments</a>.', 'woocommerce' ); } /** * Initialise settings form fields. */ public function init_form_fields() { /* translators: %s: list of placeholders */ $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' ); $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce' ), 'default' => 'yes', ), 'recipient' => array( 'title' => __( 'Recipient(s)', 'woocommerce' ), 'type' => 'text', /* translators: %s: WP admin email */ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ), 'placeholder' => '', 'default' => '', 'desc_tip' => true, ), 'subject' => array( 'title' => __( 'Subject', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => __( 'Email heading', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => $placeholder_text, 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'additional_content' => array( 'title' => __( 'Additional content', 'woocommerce' ), 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => $this->get_default_additional_content(), 'desc_tip' => true, ), 'email_type' => array( 'title' => __( 'Email type', 'woocommerce' ), 'type' => 'select', 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), 'default' => 'html', 'class' => 'email_type wc-enhanced-select', 'options' => $this->get_email_type_options(), 'desc_tip' => true, ), ); } } endif; return new WC_Email_Failed_Order(); includes/class-wc-countries.php 0000644 00000131415 15132754524 0012625 0 ustar 00 <?php /** * WooCommerce countries * * @package WooCommerce\l10n * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * The WooCommerce countries class stores country/state data. */ class WC_Countries { /** * Locales list. * * @var array */ public $locale = array(); /** * List of address formats for locales. * * @var array */ public $address_formats = array(); /** * Auto-load in-accessible properties on demand. * * @param mixed $key Key. * @return mixed */ public function __get( $key ) { if ( 'countries' === $key ) { return $this->get_countries(); } elseif ( 'states' === $key ) { return $this->get_states(); } } /** * Get all countries. * * @return array */ public function get_countries() { if ( empty( $this->countries ) ) { $this->countries = apply_filters( 'woocommerce_countries', include WC()->plugin_path() . '/i18n/countries.php' ); if ( apply_filters( 'woocommerce_sort_countries', true ) ) { wc_asort_by_locale( $this->countries ); } } return $this->countries; } /** * Check if a given code represents a valid ISO 3166-1 alpha-2 code for a country known to us. * * @since 5.1.0 * @param string $country_code The country code to check as a ISO 3166-1 alpha-2 code. * @return bool True if the country is known to us, false otherwise. */ public function country_exists( $country_code ) { return isset( $this->get_countries()[ $country_code ] ); } /** * Get all continents. * * @return array */ public function get_continents() { if ( empty( $this->continents ) ) { $this->continents = apply_filters( 'woocommerce_continents', include WC()->plugin_path() . '/i18n/continents.php' ); } return $this->continents; } /** * Get continent code for a country code. * * @since 2.6.0 * @param string $cc Country code. * @return string */ public function get_continent_code_for_country( $cc ) { $cc = trim( strtoupper( $cc ) ); $continents = $this->get_continents(); $continents_and_ccs = wp_list_pluck( $continents, 'countries' ); foreach ( $continents_and_ccs as $continent_code => $countries ) { if ( false !== array_search( $cc, $countries, true ) ) { return $continent_code; } } return ''; } /** * Get calling code for a country code. * * @since 3.6.0 * @param string $cc Country code. * @return string|array Some countries have multiple. The code will be stripped of - and spaces and always be prefixed with +. */ public function get_country_calling_code( $cc ) { $codes = wp_cache_get( 'calling-codes', 'countries' ); if ( ! $codes ) { $codes = include WC()->plugin_path() . '/i18n/phone.php'; wp_cache_set( 'calling-codes', $codes, 'countries' ); } $calling_code = isset( $codes[ $cc ] ) ? $codes[ $cc ] : ''; if ( is_array( $calling_code ) ) { $calling_code = $calling_code[0]; } return $calling_code; } /** * Get continents that the store ships to. * * @since 3.6.0 * @return array */ public function get_shipping_continents() { $continents = $this->get_continents(); $shipping_countries = $this->get_shipping_countries(); $shipping_country_codes = array_keys( $shipping_countries ); $shipping_continents = array(); foreach ( $continents as $continent_code => $continent ) { if ( count( array_intersect( $continent['countries'], $shipping_country_codes ) ) ) { $shipping_continents[ $continent_code ] = $continent; } } return $shipping_continents; } /** * Load the states. * * @deprecated 3.6.0 This method was used to load state files, but is no longer needed. @see get_states(). */ public function load_country_states() { global $states; $states = include WC()->plugin_path() . '/i18n/states.php'; $this->states = apply_filters( 'woocommerce_states', $states ); } /** * Get the states for a country. * * @param string $cc Country code. * @return false|array of states */ public function get_states( $cc = null ) { if ( ! isset( $this->states ) ) { $this->states = apply_filters( 'woocommerce_states', include WC()->plugin_path() . '/i18n/states.php' ); } if ( ! is_null( $cc ) ) { return isset( $this->states[ $cc ] ) ? $this->states[ $cc ] : false; } else { return $this->states; } } /** * Get the base address (first line) for the store. * * @since 3.1.1 * @return string */ public function get_base_address() { $base_address = get_option( 'woocommerce_store_address', '' ); return apply_filters( 'woocommerce_countries_base_address', $base_address ); } /** * Get the base address (second line) for the store. * * @since 3.1.1 * @return string */ public function get_base_address_2() { $base_address_2 = get_option( 'woocommerce_store_address_2', '' ); return apply_filters( 'woocommerce_countries_base_address_2', $base_address_2 ); } /** * Get the base country for the store. * * @return string */ public function get_base_country() { $default = wc_get_base_location(); return apply_filters( 'woocommerce_countries_base_country', $default['country'] ); } /** * Get the base state for the store. * * @return string */ public function get_base_state() { $default = wc_get_base_location(); return apply_filters( 'woocommerce_countries_base_state', $default['state'] ); } /** * Get the base city for the store. * * @version 3.1.1 * @return string */ public function get_base_city() { $base_city = get_option( 'woocommerce_store_city', '' ); return apply_filters( 'woocommerce_countries_base_city', $base_city ); } /** * Get the base postcode for the store. * * @since 3.1.1 * @return string */ public function get_base_postcode() { $base_postcode = get_option( 'woocommerce_store_postcode', '' ); return apply_filters( 'woocommerce_countries_base_postcode', $base_postcode ); } /** * Get countries that the store sells to. * * @return array */ public function get_allowed_countries() { if ( 'all' === get_option( 'woocommerce_allowed_countries' ) ) { return apply_filters( 'woocommerce_countries_allowed_countries', $this->countries ); } if ( 'all_except' === get_option( 'woocommerce_allowed_countries' ) ) { $except_countries = get_option( 'woocommerce_all_except_countries', array() ); if ( ! $except_countries ) { return $this->countries; } else { $all_except_countries = $this->countries; foreach ( $except_countries as $country ) { unset( $all_except_countries[ $country ] ); } return apply_filters( 'woocommerce_countries_allowed_countries', $all_except_countries ); } } $countries = array(); $raw_countries = get_option( 'woocommerce_specific_allowed_countries', array() ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { $countries[ $country ] = $this->countries[ $country ]; } } return apply_filters( 'woocommerce_countries_allowed_countries', $countries ); } /** * Get countries that the store ships to. * * @return array */ public function get_shipping_countries() { if ( '' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->get_allowed_countries(); } if ( 'all' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->countries; } $countries = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { $countries[ $country ] = $this->countries[ $country ]; } } return apply_filters( 'woocommerce_countries_shipping_countries', $countries ); } /** * Get allowed country states. * * @return array */ public function get_allowed_country_states() { if ( get_option( 'woocommerce_allowed_countries' ) !== 'specific' ) { return $this->states; } $states = array(); $raw_countries = get_option( 'woocommerce_specific_allowed_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { if ( isset( $this->states[ $country ] ) ) { $states[ $country ] = $this->states[ $country ]; } } } return apply_filters( 'woocommerce_countries_allowed_country_states', $states ); } /** * Get shipping country states. * * @return array */ public function get_shipping_country_states() { if ( get_option( 'woocommerce_ship_to_countries' ) === '' ) { return $this->get_allowed_country_states(); } if ( get_option( 'woocommerce_ship_to_countries' ) !== 'specific' ) { return $this->states; } $states = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { if ( ! empty( $this->states[ $country ] ) ) { $states[ $country ] = $this->states[ $country ]; } } } return apply_filters( 'woocommerce_countries_shipping_country_states', $states ); } /** * Gets an array of countries in the EU. * * @param string $type Type of countries to retrieve. Blank for EU member countries. eu_vat for EU VAT countries. * @return string[] */ public function get_european_union_countries( $type = '' ) { $countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); if ( 'eu_vat' === $type ) { $countries[] = 'MC'; } return apply_filters( 'woocommerce_european_union_countries', $countries, $type ); } /** * Gets an array of Non-EU countries that use VAT as the Local name for their taxes based on this list - https://en.wikipedia.org/wiki/Value-added_tax#Non-European_Union_countries * * @deprecated 4.0.0 * @since 3.9.0 * @return string[] */ public function countries_using_vat() { wc_deprecated_function( 'countries_using_vat', '4.0', 'WC_Countries::get_vat_countries' ); $countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GH', 'GM', 'GT', 'IL', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' ); return apply_filters( 'woocommerce_countries_using_vat', $countries ); } /** * Gets an array of countries using VAT. * * @since 4.0.0 * @return string[] of country codes. */ public function get_vat_countries() { $eu_countries = $this->get_european_union_countries(); $vat_countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GB', 'GH', 'GM', 'GT', 'IL', 'IM', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MC', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NO', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' ); return apply_filters( 'woocommerce_vat_countries', array_merge( $eu_countries, $vat_countries ) ); } /** * Gets the correct string for shipping - either 'to the' or 'to'. * * @param string $country_code Country code. * @return string */ public function shipping_to_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : WC()->customer->get_shipping_country(); $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? __( 'to the', 'woocommerce' ) : __( 'to', 'woocommerce' ); return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code ); } /** * Prefix certain countries with 'the'. * * @param string $country_code Country code. * @return string */ public function estimated_for_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : $this->get_base_country(); $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? __( 'the', 'woocommerce' ) . ' ' : ''; return apply_filters( 'woocommerce_countries_estimated_for_prefix', $return, $country_code ); } /** * Correctly name tax in some countries VAT on the frontend. * * @return string */ public function tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( 'VAT', 'woocommerce' ) : __( 'Tax', 'woocommerce' ); return apply_filters( 'woocommerce_countries_tax_or_vat', $return ); } /** * Include the Inc Tax label. * * @return string */ public function inc_tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(incl. VAT)', 'woocommerce' ) : __( '(incl. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_inc_tax_or_vat', $return ); } /** * Include the Ex Tax label. * * @return string */ public function ex_tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(ex. VAT)', 'woocommerce' ) : __( '(ex. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_ex_tax_or_vat', $return ); } /** * Outputs the list of countries and states for use in dropdown boxes. * * @param string $selected_country Selected country. * @param string $selected_state Selected state. * @param bool $escape If we should escape HTML. */ public function country_dropdown_options( $selected_country = '', $selected_state = '', $escape = false ) { if ( $this->countries ) { foreach ( $this->countries as $key => $value ) { $states = $this->get_states( $key ); if ( $states ) { echo '<optgroup label="' . esc_attr( $value ) . '">'; foreach ( $states as $state_key => $state_value ) { echo '<option value="' . esc_attr( $key ) . ':' . esc_attr( $state_key ) . '"'; if ( $selected_country === $key && $selected_state === $state_key ) { echo ' selected="selected"'; } echo '>' . esc_html( $value ) . ' — ' . ( $escape ? esc_html( $state_value ) : $state_value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo '</optgroup>'; } else { echo '<option'; if ( $selected_country === $key && '*' === $selected_state ) { echo ' selected="selected"'; } echo ' value="' . esc_attr( $key ) . '">' . ( $escape ? esc_html( $value ) : $value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } } } /** * Get country address formats. * * These define how addresses are formatted for display in various countries. * * @return array */ public function get_address_formats() { if ( empty( $this->address_formats ) ) { $this->address_formats = apply_filters( 'woocommerce_localisation_address_formats', array( 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}", 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", ) ); } return $this->address_formats; } /** * Get country address format. * * @param array $args Arguments. * @param string $separator How to separate address lines. @since 3.5.0. * @return string */ public function get_formatted_address( $args = array(), $separator = '<br/>' ) { $default_args = array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', ); $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); $state = $args['state']; $country = $args['country']; // Get all formats. $formats = $this->get_address_formats(); // Get format for the address' country. $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; // Handle full country name. $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; // Country is not needed if the same as base. if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { $format = str_replace( '{country}', '', $format ); } // Handle full state name. $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; // Substitute address parts into the string. $replace = array_map( 'esc_html', apply_filters( 'woocommerce_formatted_address_replacements', array( '{first_name}' => $args['first_name'], '{last_name}' => $args['last_name'], '{name}' => sprintf( /* translators: 1: first name 2: last name */ _x( '%1$s %2$s', 'full name', 'woocommerce' ), $args['first_name'], $args['last_name'] ), '{company}' => $args['company'], '{address_1}' => $args['address_1'], '{address_2}' => $args['address_2'], '{city}' => $args['city'], '{state}' => $full_state, '{postcode}' => $args['postcode'], '{country}' => $full_country, '{first_name_upper}' => wc_strtoupper( $args['first_name'] ), '{last_name_upper}' => wc_strtoupper( $args['last_name'] ), '{name_upper}' => wc_strtoupper( sprintf( /* translators: 1: first name 2: last name */ _x( '%1$s %2$s', 'full name', 'woocommerce' ), $args['first_name'], $args['last_name'] ) ), '{company_upper}' => wc_strtoupper( $args['company'] ), '{address_1_upper}' => wc_strtoupper( $args['address_1'] ), '{address_2_upper}' => wc_strtoupper( $args['address_2'] ), '{city_upper}' => wc_strtoupper( $args['city'] ), '{state_upper}' => wc_strtoupper( $full_state ), '{state_code}' => wc_strtoupper( $state ), '{postcode_upper}' => wc_strtoupper( $args['postcode'] ), '{country_upper}' => wc_strtoupper( $full_country ), ), $args ) ); $formatted_address = str_replace( array_keys( $replace ), $replace, $format ); // Clean up white space. $formatted_address = preg_replace( '/ +/', ' ', trim( $formatted_address ) ); $formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address ); // Break newlines apart and remove empty lines/trim commas and white space. $formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) ); // Add html breaks. $formatted_address = implode( $separator, $formatted_address ); // We're done! return $formatted_address; } /** * Trim white space and commas off a line. * * @param string $line Line. * @return string */ private function trim_formatted_address_line( $line ) { return trim( $line, ', ' ); } /** * Returns the fields we show by default. This can be filtered later on. * * @return array */ public function get_default_address_fields() { $address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' ); // If necessary, append '(optional)' to the placeholder: we don't need to worry about the // label, though, as woocommerce_form_field() takes care of that. if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { $address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' ); } else { $address_2_placeholder = $address_2_label; } $fields = array( 'first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-first' ), 'autocomplete' => 'given-name', 'priority' => 10, ), 'last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-last' ), 'autocomplete' => 'family-name', 'priority' => 20, ), 'company' => array( 'label' => __( 'Company name', 'woocommerce' ), 'class' => array( 'form-row-wide' ), 'autocomplete' => 'organization', 'priority' => 30, 'required' => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ), ), 'country' => array( 'type' => 'country', 'label' => __( 'Country / Region', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), 'autocomplete' => 'country', 'priority' => 40, ), 'address_1' => array( 'label' => __( 'Street address', 'woocommerce' ), /* translators: use local order of street name and house number. */ 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-line1', 'priority' => 50, ), 'address_2' => array( 'label' => $address_2_label, 'label_class' => array( 'screen-reader-text' ), 'placeholder' => esc_attr( $address_2_placeholder ), 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-line2', 'priority' => 60, 'required' => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ), ), 'city' => array( 'label' => __( 'Town / City', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-level2', 'priority' => 70, ), 'state' => array( 'type' => 'state', 'label' => __( 'State / County', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'validate' => array( 'state' ), 'autocomplete' => 'address-level1', 'priority' => 80, ), 'postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'validate' => array( 'postcode' ), 'autocomplete' => 'postal-code', 'priority' => 90, ), ); if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) { unset( $fields['company'] ); } if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { unset( $fields['address_2'] ); } $default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields ); // Sort each of the fields based on priority. uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' ); return $default_address_fields; } /** * Get JS selectors for fields which are shown/hidden depending on the locale. * * @return array */ public function get_country_locale_field_selectors() { $locale_fields = array( 'address_1' => '#billing_address_1_field, #shipping_address_1_field', 'address_2' => '#billing_address_2_field, #shipping_address_2_field', 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', ); return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); } /** * Get country locale settings. * * These locales override the default country selections after a country is chosen. * * @return array */ public function get_country_locale() { if ( empty( $this->locale ) ) { $this->locale = apply_filters( 'woocommerce_get_country_locale', array( 'AE' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'required' => false, ), ), 'AF' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'AO' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'AT' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'AU' => array( 'city' => array( 'label' => __( 'Suburb', 'woocommerce' ), ), 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'AX' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BA' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Canton', 'woocommerce' ), 'required' => false, 'hidden' => true, ), ), 'BD' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), ), ), 'BE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BH' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BI' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BO' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'BS' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'CA' => array( 'postcode' => array( 'label' => __( 'Postal code', 'woocommerce' ), ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'CH' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Canton', 'woocommerce' ), 'required' => false, ), ), 'CL' => array( 'city' => array( 'required' => true, ), 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'CN' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'CO' => array( 'postcode' => array( 'required' => false, ), ), 'CW' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'required' => false, ), ), 'CZ' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'DE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'DK' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'EE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'FI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'FR' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GH' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'GP' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GF' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GR' => array( 'state' => array( 'required' => false, ), ), 'GT' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'HK' => array( 'postcode' => array( 'required' => false, ), 'city' => array( 'label' => __( 'Town / District', 'woocommerce' ), ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'HU' => array( 'last_name' => array( 'class' => array( 'form-row-first' ), 'priority' => 10, ), 'first_name' => array( 'class' => array( 'form-row-last' ), 'priority' => 20, ), 'postcode' => array( 'class' => array( 'form-row-first', 'address-field' ), 'priority' => 65, ), 'city' => array( 'class' => array( 'form-row-last', 'address-field' ), ), 'address_1' => array( 'priority' => 71, ), 'address_2' => array( 'priority' => 72, ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), ), ), 'ID' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'IE' => array( 'postcode' => array( 'required' => false, 'label' => __( 'Eircode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), ), ), 'IS' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IM' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IN' => array( 'postcode' => array( 'label' => __( 'PIN', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'IT' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => true, 'label' => __( 'Province', 'woocommerce' ), ), ), 'JM' => array( 'city' => array( 'label' => __( 'Town / City / Post Office', 'woocommerce' ), ), 'postcode' => array( 'required' => false, 'label' => __( 'Postal Code', 'woocommerce' ), ), 'state' => array( 'required' => true, 'label' => __( 'Parish', 'woocommerce' ), ), ), 'JP' => array( 'last_name' => array( 'class' => array( 'form-row-first' ), 'priority' => 10, ), 'first_name' => array( 'class' => array( 'form-row-last' ), 'priority' => 20, ), 'postcode' => array( 'class' => array( 'form-row-first', 'address-field' ), 'priority' => 65, ), 'state' => array( 'label' => __( 'Prefecture', 'woocommerce' ), 'class' => array( 'form-row-last', 'address-field' ), 'priority' => 66, ), 'city' => array( 'priority' => 67, ), 'address_1' => array( 'priority' => 68, ), 'address_2' => array( 'priority' => 69, ), ), 'KR' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'KW' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'LV' => array( 'state' => array( 'label' => __( 'Municipality', 'woocommerce' ), 'required' => false, ), ), 'LB' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MQ' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MZ' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'NL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'NG' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'NZ' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'required' => false, 'label' => __( 'Region', 'woocommerce' ), ), ), 'NO' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'NP' => array( 'state' => array( 'label' => __( 'State / Zone', 'woocommerce' ), ), 'postcode' => array( 'required' => false, ), ), 'PL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'PR' => array( 'city' => array( 'label' => __( 'Municipality', 'woocommerce' ), ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'PT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'RE' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'RO' => array( 'state' => array( 'label' => __( 'County', 'woocommerce' ), 'required' => true, ), ), 'RS' => array( 'city' => array( 'required' => true, ), 'postcode' => array( 'required' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), 'required' => false, ), ), 'SG' => array( 'state' => array( 'required' => false, 'hidden' => true, ), 'city' => array( 'required' => false, ), ), 'SK' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'SI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'SR' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'ES' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'LI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Municipality', 'woocommerce' ), 'required' => false, ), ), 'LK' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'LU' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MD' => array( 'state' => array( 'label' => __( 'Municipality / District', 'woocommerce' ), ), ), 'SE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'TR' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'UG' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'city' => array( 'label' => __( 'Town / Village', 'woocommerce' ), 'required' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), 'required' => true, ), ), 'US' => array( 'postcode' => array( 'label' => __( 'ZIP Code', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'GB' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), 'required' => false, ), ), 'ST' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), ), ), 'VN' => array( 'state' => array( 'required' => false, 'hidden' => true, ), 'postcode' => array( 'priority' => 65, 'required' => false, 'hidden' => false, ), 'address_2' => array( 'required' => false, 'hidden' => true, ), ), 'WS' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'YT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'ZA' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'ZW' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), ) ); $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); // Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout. if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { $this->locale[ $this->get_base_country() ] = $this->locale['default']; } $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); } return $this->locale; } /** * Apply locale and get address fields. * * @param mixed $country Country. * @param string $type Address type, defaults to 'billing_'. * @return array */ public function get_address_fields( $country = '', $type = 'billing_' ) { if ( ! $country ) { $country = $this->get_base_country(); } $fields = $this->get_default_address_fields(); $locale = $this->get_country_locale(); if ( isset( $locale[ $country ] ) ) { $fields = wc_array_overlay( $fields, $locale[ $country ] ); } // Prepend field keys. $address_fields = array(); foreach ( $fields as $key => $value ) { if ( 'state' === $key ) { $value['country_field'] = $type . 'country'; $value['country'] = $country; } $address_fields[ $type . $key ] = $value; } // Add email and phone fields. if ( 'billing_' === $type ) { if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) { $address_fields['billing_phone'] = array( 'label' => __( 'Phone', 'woocommerce' ), 'required' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), 'type' => 'tel', 'class' => array( 'form-row-wide' ), 'validate' => array( 'phone' ), 'autocomplete' => 'tel', 'priority' => 100, ); } $address_fields['billing_email'] = array( 'label' => __( 'Email address', 'woocommerce' ), 'required' => true, 'type' => 'email', 'class' => array( 'form-row-wide' ), 'validate' => array( 'email' ), 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', 'priority' => 110, ); } /** * Important note on this filter: Changes to address fields can and will be overridden by * the woocommerce_default_address_fields. The locales/default locales apply on top based * on country selection. If you want to change things like the required status of an * address field, filter woocommerce_default_address_fields instead. */ $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); // Sort each of the fields based on priority. uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' ); return $address_fields; } } includes/class-wc-order-refund.php 0000644 00000011610 15132754524 0013200 0 ustar 00 <?php /** * Order refund. Refunds are based on orders (essentially negative orders) and * contain much of the same data. * * @version 3.0.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * Order refund class. */ class WC_Order_Refund extends WC_Abstract_Order { /** * Which data store to load. * * @var string */ protected $data_store_name = 'order-refund'; /** * This is the name of this object type. * * @var string */ protected $object_type = 'order_refund'; /** * Stores product data. * * @var array */ protected $extra_data = array( 'amount' => '', 'reason' => '', 'refunded_by' => 0, 'refunded_payment' => false, ); /** * Get internal type (post type.) * * @return string */ public function get_type() { return 'shop_order_refund'; } /** * Get status - always completed for refunds. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_status( $context = 'view' ) { return 'completed'; } /** * Get a title for the new post type. */ public function get_post_title() { // @codingStandardsIgnoreStart return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreEnd } /** * Get refunded amount. * * @param string $context What the value is for. Valid values are view and edit. * @return int|float */ public function get_amount( $context = 'view' ) { return $this->get_prop( 'amount', $context ); } /** * Get refund reason. * * @since 2.2 * @param string $context What the value is for. Valid values are view and edit. * @return int|float */ public function get_reason( $context = 'view' ) { return $this->get_prop( 'reason', $context ); } /** * Get ID of user who did the refund. * * @since 3.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_refunded_by( $context = 'view' ) { return $this->get_prop( 'refunded_by', $context ); } /** * Return if the payment was refunded via API. * * @since 3.3 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_refunded_payment( $context = 'view' ) { return $this->get_prop( 'refunded_payment', $context ); } /** * Get formatted refunded amount. * * @since 2.4 * @return string */ public function get_formatted_refund_amount() { return apply_filters( 'woocommerce_formatted_refund_amount', wc_price( $this->get_amount(), array( 'currency' => $this->get_currency() ) ), $this ); } /** * Set refunded amount. * * @param string $value Value to set. * @throws WC_Data_Exception Exception if the amount is invalid. */ public function set_amount( $value ) { $this->set_prop( 'amount', wc_format_decimal( $value ) ); } /** * Set refund reason. * * @param string $value Value to set. * @throws WC_Data_Exception Exception if the amount is invalid. */ public function set_reason( $value ) { $this->set_prop( 'reason', $value ); } /** * Set refunded by. * * @param int $value Value to set. * @throws WC_Data_Exception Exception if the amount is invalid. */ public function set_refunded_by( $value ) { $this->set_prop( 'refunded_by', absint( $value ) ); } /** * Set if the payment was refunded via API. * * @since 3.3 * @param bool $value Value to set. */ public function set_refunded_payment( $value ) { $this->set_prop( 'refunded_payment', (bool) $value ); } /** * Magic __get method for backwards compatibility. * * @param string $key Value to get. * @return mixed */ public function __get( $key ) { wc_doing_it_wrong( $key, 'Refund properties should not be accessed directly.', '3.0' ); /** * Maps legacy vars to new getters. */ if ( 'reason' === $key ) { return $this->get_reason(); } elseif ( 'refund_amount' === $key ) { return $this->get_amount(); } return parent::__get( $key ); } /** * Gets an refund from the database. * * @deprecated 3.0 * @param int $id (default: 0). * @return bool */ public function get_refund( $id = 0 ) { wc_deprecated_function( 'get_refund', '3.0', 'read' ); if ( ! $id ) { return false; } $result = get_post( $id ); if ( $result ) { $this->populate( $result ); return true; } return false; } /** * Get refund amount. * * @deprecated 3.0 * @return int|float */ public function get_refund_amount() { wc_deprecated_function( 'get_refund_amount', '3.0', 'get_amount' ); return $this->get_amount(); } /** * Get refund reason. * * @deprecated 3.0 * @return int|float */ public function get_refund_reason() { wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); return $this->get_reason(); } } includes/class-wc-geolite-integration.php 0000644 00000003764 15132754524 0014570 0 ustar 00 <?php /** * Wrapper for MaxMind GeoLite2 Reader * * This class provide an interface to handle geolocation and error handling. * * Requires PHP 5.4+. * * @package WooCommerce\Classes * @since 3.4.0 * @deprecated 3.9.0 */ defined( 'ABSPATH' ) || exit; /** * Geolite integration class. * * @deprecated 3.9.0 */ class WC_Geolite_Integration { /** * MaxMind GeoLite2 database path. * * @var string */ private $database = ''; /** * Logger instance. * * @var WC_Logger */ private $log = null; /** * Constructor. * * @param string $database MaxMind GeoLite2 database path. */ public function __construct( $database ) { $this->database = $database; } /** * Get country 2-letters ISO by IP address. * Returns empty string when not able to find any ISO code. * * @param string $ip_address User IP address. * @return string * @deprecated 3.9.0 */ public function get_country_iso( $ip_address ) { wc_deprecated_function( 'get_country_iso', '3.9.0' ); $iso_code = ''; try { $reader = new MaxMind\Db\Reader( $this->database ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound $data = $reader->get( $ip_address ); if ( isset( $data['country']['iso_code'] ) ) { $iso_code = $data['country']['iso_code']; } $reader->close(); } catch ( Exception $e ) { $this->log( $e->getMessage(), 'warning' ); } return sanitize_text_field( strtoupper( $iso_code ) ); } /** * Logging method. * * @param string $message Log message. * @param string $level Log level. * Available options: 'emergency', 'alert', * 'critical', 'error', 'warning', 'notice', * 'info' and 'debug'. * Defaults to 'info'. */ private function log( $message, $level = 'info' ) { if ( is_null( $this->log ) ) { $this->log = wc_get_logger(); } $this->log->log( $level, $message, array( 'source' => 'geoip' ) ); } } includes/class-wc-order-item-product.php 0000644 00000032077 15132754524 0014343 0 ustar 00 <?php /** * Order Line Item (product) * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item product class. */ class WC_Order_Item_Product extends WC_Order_Item { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $extra_data = array( 'product_id' => 0, 'variation_id' => 0, 'quantity' => 1, 'tax_class' => '', 'subtotal' => 0, 'subtotal_tax' => 0, 'total' => 0, 'total_tax' => 0, 'taxes' => array( 'subtotal' => array(), 'total' => array(), ), ); /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set quantity. * * @param int $value Quantity. */ public function set_quantity( $value ) { $this->set_prop( 'quantity', wc_stock_amount( $value ) ); } /** * Set tax class. * * @param string $value Tax class. */ public function set_tax_class( $value ) { if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { $this->error( 'order_item_product_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); } $this->set_prop( 'tax_class', $value ); } /** * Set Product ID * * @param int $value Product ID. */ public function set_product_id( $value ) { if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) { $this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) ); } $this->set_prop( 'product_id', absint( $value ) ); } /** * Set variation ID. * * @param int $value Variation ID. */ public function set_variation_id( $value ) { if ( $value > 0 && 'product_variation' !== get_post_type( $value ) ) { $this->error( 'order_item_product_invalid_variation_id', __( 'Invalid variation ID', 'woocommerce' ) ); } $this->set_prop( 'variation_id', absint( $value ) ); } /** * Line subtotal (before discounts). * * @param string $value Subtotal. */ public function set_subtotal( $value ) { $value = wc_format_decimal( $value ); if ( ! is_numeric( $value ) ) { $value = 0; } $this->set_prop( 'subtotal', $value ); } /** * Line total (after discounts). * * @param string $value Total. */ public function set_total( $value ) { $value = wc_format_decimal( $value ); if ( ! is_numeric( $value ) ) { $value = 0; } $this->set_prop( 'total', $value ); // Subtotal cannot be less than total. if ( '' === $this->get_subtotal() || $this->get_subtotal() < $this->get_total() ) { $this->set_subtotal( $value ); } } /** * Line subtotal tax (before discounts). * * @param string $value Subtotal tax. */ public function set_subtotal_tax( $value ) { $this->set_prop( 'subtotal_tax', wc_format_decimal( $value ) ); } /** * Line total tax (after discounts). * * @param string $value Total tax. */ public function set_total_tax( $value ) { $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); } /** * Set line taxes and totals for passed in taxes. * * @param array $raw_tax_data Raw tax data. */ public function set_taxes( $raw_tax_data ) { $raw_tax_data = maybe_unserialize( $raw_tax_data ); $tax_data = array( 'total' => array(), 'subtotal' => array(), ); if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) { $tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] ); $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); // Subtotal cannot be less than total! if ( array_sum( $tax_data['subtotal'] ) < array_sum( $tax_data['total'] ) ) { $tax_data['subtotal'] = $tax_data['total']; } } $this->set_prop( 'taxes', $tax_data ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $this->set_total_tax( array_sum( $tax_data['total'] ) ); $this->set_subtotal_tax( array_sum( $tax_data['subtotal'] ) ); } else { $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); $this->set_subtotal_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['subtotal'] ) ) ); } } /** * Set variation data (stored as meta data - write only). * * @param array $data Key/Value pairs. */ public function set_variation( $data = array() ) { if ( is_array( $data ) ) { foreach ( $data as $key => $value ) { $this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true ); } } } /** * Set properties based on passed in product object. * * @param WC_Product $product Product instance. */ public function set_product( $product ) { if ( ! is_a( $product, 'WC_Product' ) ) { $this->error( 'order_item_product_invalid_product', __( 'Invalid product', 'woocommerce' ) ); } if ( $product->is_type( 'variation' ) ) { $this->set_product_id( $product->get_parent_id() ); $this->set_variation_id( $product->get_id() ); $this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() ); } else { $this->set_product_id( $product->get_id() ); } $this->set_name( $product->get_name() ); $this->set_tax_class( $product->get_tax_class() ); } /** * Set meta data for backordered products. */ public function set_backorder_meta() { $product = $this->get_product(); if ( $product && $product->backorders_require_notification() && $product->is_on_backorder( $this->get_quantity() ) ) { $this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $this ), $this->get_quantity() - max( 0, $product->get_stock_quantity() ), true ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order item type. * * @return string */ public function get_type() { return 'line_item'; } /** * Get product ID. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return int */ public function get_product_id( $context = 'view' ) { return $this->get_prop( 'product_id', $context ); } /** * Get variation ID. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return int */ public function get_variation_id( $context = 'view' ) { return $this->get_prop( 'variation_id', $context ); } /** * Get quantity. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return int */ public function get_quantity( $context = 'view' ) { return $this->get_prop( 'quantity', $context ); } /** * Get tax class. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_class( $context = 'view' ) { return $this->get_prop( 'tax_class', $context ); } /** * Get subtotal. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_subtotal( $context = 'view' ) { return $this->get_prop( 'subtotal', $context ); } /** * Get subtotal tax. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_subtotal_tax( $context = 'view' ) { return $this->get_prop( 'subtotal_tax', $context ); } /** * Get total. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /** * Get taxes. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_taxes( $context = 'view' ) { return $this->get_prop( 'taxes', $context ); } /** * Get the associated product. * * @return WC_Product|bool */ public function get_product() { if ( $this->get_variation_id() ) { $product = wc_get_product( $this->get_variation_id() ); } else { $product = wc_get_product( $this->get_product_id() ); } // Backwards compatible filter from WC_Order::get_product_from_item(). if ( has_filter( 'woocommerce_get_product_from_item' ) ) { $product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, $this->get_order() ); } return apply_filters( 'woocommerce_order_item_product', $product, $this ); } /** * Get the Download URL. * * @param int $download_id Download ID. * @return string */ public function get_item_download_url( $download_id ) { $order = $this->get_order(); return $order ? add_query_arg( array( 'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(), 'order' => $order->get_order_key(), 'email' => rawurlencode( $order->get_billing_email() ), 'key' => $download_id, ), trailingslashit( home_url() ) ) : ''; } /** * Get any associated downloadable files. * * @return array */ public function get_item_downloads() { $files = array(); $product = $this->get_product(); $order = $this->get_order(); $product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(); if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) { $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $order->get_billing_email() ) : sha1( $order->get_billing_email() ); $data_store = WC_Data_Store::load( 'customer-download' ); $customer_downloads = $data_store->get_downloads( array( 'user_email' => $order->get_billing_email(), 'order_id' => $order->get_id(), 'product_id' => $product_id, ) ); foreach ( $customer_downloads as $customer_download ) { $download_id = $customer_download->get_download_id(); if ( $product->has_file( $download_id ) ) { $file = $product->get_file( $download_id ); $files[ $download_id ] = $file->get_data(); $files[ $download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining(); $files[ $download_id ]['access_expires'] = $customer_download->get_access_expires(); $files[ $download_id ]['download_url'] = add_query_arg( array( 'download_file' => $product_id, 'order' => $order->get_order_key(), 'uid' => $email_hash, 'key' => $download_id, ), trailingslashit( home_url() ) ); } } } return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order ); } /** * Get tax status. * * @return string */ public function get_tax_status() { $product = $this->get_product(); return $product ? $product->get_tax_status() : 'taxable'; } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * OffsetGet for ArrayAccess/Backwards compatibility. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { if ( 'line_subtotal' === $offset ) { $offset = 'subtotal'; } elseif ( 'line_subtotal_tax' === $offset ) { $offset = 'subtotal_tax'; } elseif ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } elseif ( 'qty' === $offset ) { $offset = 'quantity'; } return parent::offsetGet( $offset ); } /** * OffsetSet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Product::offsetSet', '4.4.0', '' ); if ( 'line_subtotal' === $offset ) { $offset = 'subtotal'; } elseif ( 'line_subtotal_tax' === $offset ) { $offset = 'subtotal_tax'; } elseif ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } elseif ( 'qty' === $offset ) { $offset = 'quantity'; } parent::offsetSet( $offset, $value ); } /** * OffsetExists for ArrayAccess. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta', 'qty' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } includes/libraries/wp-background-process.php 0000644 00000024261 15132754524 0015273 0 ustar 00 <?php // @codingStandardsIgnoreLine. /** * Abstract WP_Background_Process class. * * @package WP-Background-Processing * @extends WP_Async_Request */ defined( 'ABSPATH' ) || exit; /** * Abstract WP_Background_Process class. */ abstract class WP_Background_Process extends WP_Async_Request { /** * Action * * (default value: 'background_process') * * @var string * @access protected */ protected $action = 'background_process'; /** * Start time of current process. * * (default value: 0) * * @var int * @access protected */ protected $start_time = 0; /** * Cron_hook_identifier * * @var mixed * @access protected */ protected $cron_hook_identifier; /** * Cron_interval_identifier * * @var mixed * @access protected */ protected $cron_interval_identifier; /** * Initiate new background process */ public function __construct() { parent::__construct(); $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } /** * Dispatch * * @access public * @return void */ public function dispatch() { // Schedule the cron healthcheck. $this->schedule_event(); // Perform remote post. return parent::dispatch(); } /** * Push to queue * * @param mixed $data Data. * * @return $this */ public function push_to_queue( $data ) { $this->data[] = $data; return $this; } /** * Save queue * * @return $this */ public function save() { $key = $this->generate_key(); if ( ! empty( $this->data ) ) { update_site_option( $key, $this->data ); } return $this; } /** * Update queue * * @param string $key Key. * @param array $data Data. * * @return $this */ public function update( $key, $data ) { if ( ! empty( $data ) ) { update_site_option( $key, $data ); } return $this; } /** * Delete queue * * @param string $key Key. * * @return $this */ public function delete( $key ) { delete_site_option( $key ); return $this; } /** * Generate key * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * * @param int $length Length. * * @return string */ protected function generate_key( $length = 64 ) { $unique = md5( microtime() . rand() ); $prepend = $this->identifier . '_batch_'; return substr( $prepend . $unique, 0, $length ); } /** * Maybe process queue * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); if ( $this->is_process_running() ) { // Background process already running. wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Is queue empty * * @return bool */ protected function is_queue_empty() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $this->identifier . '_batch_%'; $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s ", $key ) ); return ! ( $count > 0 ); } /** * Is process running * * Check whether the current process is already running * in a background process. */ protected function is_process_running() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; } return false; } /** * Lock process * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. */ protected function lock_process() { $this->start_time = time(); // Set start time of current process. $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); } /** * Unlock process * * Unlock the process so that other instances can spawn. * * @return $this */ protected function unlock_process() { delete_site_transient( $this->identifier . '_process_lock' ); return $this; } /** * Get batch * * @return stdClass Return the first batch from the queue */ protected function get_batch() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $this->identifier . '_batch_%'; $query = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1 ", $key ) ); $batch = new stdClass(); $batch->key = $query->$column; $batch->data = maybe_unserialize( $query->$value_column ); return $batch; } /** * Handle * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } if ( $this->time_exceeded() || $this->memory_exceeded() ) { // Batch limits reached. break; } } // Update or delete current batch. if ( ! empty( $batch->data ) ) { $this->update( $batch->key, $batch->data ); } else { $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } wp_die(); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( $this->identifier . '_memory_exceeded', $return ); } /** * Get memory limit * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return intval( $memory_limit ) * 1024 * 1024; } /** * Time exceeded. * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( $this->identifier . '_time_exceeded', $return ); } /** * Complete. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { // Unschedule the cron healthcheck. $this->clear_scheduled_event(); } /** * Schedule cron healthcheck * * @access public * @param mixed $schedules Schedules. * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); if ( property_exists( $this, 'cron_interval' ) ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } // Adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ), ); return $schedules; } /** * Handle cron healthcheck * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_process_running() ) { // Background process already running. exit; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); exit; } $this->handle(); exit; } /** * Schedule event */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Clear scheduled event */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); } } /** * Cancel Process * * Stop processing queue items, clear cronjob and delete batch. * */ public function cancel_process() { if ( ! $this->is_queue_empty() ) { $batch = $this->get_batch(); $this->delete( $batch->key ); wp_clear_scheduled_hook( $this->cron_hook_identifier ); } } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param mixed $item Queue item to iterate over. * * @return mixed */ abstract protected function task( $item ); } includes/libraries/class-wc-eval-math.php 0000644 00000032221 15132754524 0014437 0 ustar 00 <?php use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Eval_Math', false ) ) { /** * Class WC_Eval_Math. Supports basic math only (removed eval function). * * Based on EvalMath by Miles Kaufman Copyright (C) 2005 Miles Kaufmann http://www.twmagic.com/. */ class WC_Eval_Math { /** * Last error. * * @var string */ public static $last_error = null; /** * Variables (and constants). * * @var array */ public static $v = array( 'e' => 2.71, 'pi' => 3.14 ); /** * User-defined functions. * * @var array */ public static $f = array(); /** * Constants. * * @var array */ public static $vb = array( 'e', 'pi' ); /** * Built-in functions. * * @var array */ public static $fb = array(); /** * Evaluate maths string. * * @param string $expr * @return mixed */ public static function evaluate( $expr ) { self::$last_error = null; $expr = trim( $expr ); if ( substr( $expr, -1, 1 ) == ';' ) { $expr = substr( $expr, 0, strlen( $expr ) -1 ); // strip semicolons at the end } // =============== // is it a variable assignment? if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) { if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant return self::trigger( "cannot assign to constant '$matches[1]'" ); } if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) { return false; // get the result and make sure it's good } self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array return self::$v[ $matches[1] ]; // and return the resulting value // =============== // is it a function assignment? } elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) { $fnn = $matches[1]; // get the function name if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in return self::trigger( "cannot redefine built-in function '$matches[1]()'" ); } $args = explode( ",", preg_replace( "/\s+/", "", $matches[2] ) ); // get the arguments if ( ( $stack = self::nfx( $matches[3] ) ) === false ) { return false; // see if it can be converted to postfix } $stack_size = count( $stack ); for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables $token = $stack[ $i ]; if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) { if ( array_key_exists( $token, self::$v ) ) { $stack[ $i ] = self::$v[ $token ]; } else { return self::trigger( "undefined variable '$token' in function definition" ); } } } self::$f[ $fnn ] = array( 'args' => $args, 'func' => $stack ); return true; // =============== } else { return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo } } /** * Convert infix to postfix notation. * * @param string $expr * * @return array|string */ private static function nfx( $expr ) { $index = 0; $stack = new WC_Eval_Math_Stack; $output = array(); // postfix form of expression, to be passed to pfx() $expr = trim( $expr ); $ops = array( '+', '-', '*', '/', '^', '_' ); $ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1 ); // right-associative operator? $ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2 ); // operator precedence $expecting_op = false; // we use this in syntax-checking the expression // and determining when a - is a negation if ( preg_match( "/[^\w\s+*^\/()\.,-]/", $expr, $matches ) ) { // make sure the characters are all good return self::trigger( "illegal character '{$matches[0]}'" ); } while ( 1 ) { // 1 Infinite Loop ;) $op = substr( $expr, $index, 1 ); // get the first character at the current index // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand $ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match ); // =============== if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus? $stack->push( '_' ); // put a negation on the stack $index++; } elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack return self::trigger( "illegal character '_'" ); // but not in the input expression // =============== } elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack? if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parenthesis? $op = '*'; $index--; // it's an implicit multiplication } // heart of the algorithm: while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) { $output[] = $stack->pop(); // pop stuff off the stack into the output } // many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail $stack->push( $op ); // finally put OUR operator onto the stack $index++; $expecting_op = false; // =============== } elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis? while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last ( if ( is_null( $o2 ) ) { return self::trigger( "unexpected ')'" ); } else { $output[] = $o2; } } if ( preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { // did we just close a function? $fnn = $matches[1]; // get the function name $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) $output[] = $stack->pop(); // pop the function and push onto the output if ( in_array( $fnn, self::$fb ) ) { // check the argument count if ( $arg_count > 1 ) { return self::trigger( "too many arguments ($arg_count given, 1 expected)" ); } } elseif ( array_key_exists( $fnn, self::$f ) ) { if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) { return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . " expected)" ); } } else { // did we somehow push a non-function on the stack? this should never happen return self::trigger( "internal error" ); } } $index++; // =============== } elseif ( ',' === $op and $expecting_op ) { // did we just finish a function argument? while ( ( $o2 = $stack->pop() ) != '(' ) { if ( is_null( $o2 ) ) { return self::trigger( "unexpected ','" ); // oops, never had a ( } else { $output[] = $o2; // pop the argument expression stuff and push onto the output } } // make sure there was a function if ( ! preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { return self::trigger( "unexpected ','" ); } $stack->push( $stack->pop() + 1 ); // increment the argument count $stack->push( '(' ); // put the ( back on, we'll need to pop back to it again $index++; $expecting_op = false; // =============== } elseif ( '(' === $op and ! $expecting_op ) { $stack->push( '(' ); // that was easy $index++; // =============== } elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number? $expecting_op = true; $val = $match[1]; if ( preg_match( "/^([A-Za-z]\w*)\($/", $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses... if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func $stack->push( $val ); $stack->push( 1 ); $stack->push( '(' ); $expecting_op = false; } else { // it's a var w/ implicit multiplication $val = $matches[1]; $output[] = $val; } } else { // it's a plain old var or num $output[] = $val; } $index += strlen( $val ); // =============== } elseif ( ')' === $op ) { // miscellaneous error checking return self::trigger( "unexpected ')'" ); } elseif ( in_array( $op, $ops ) and ! $expecting_op ) { return self::trigger( "unexpected operator '$op'" ); } else { // I don't even want to know what you did to get here return self::trigger( "an unexpected error occurred" ); } if ( strlen( $expr ) == $index ) { if ( in_array( $op, $ops ) ) { // did we end with an operator? bad. return self::trigger( "operator '$op' lacks operand" ); } else { break; } } while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace $index++; // into implicit multiplication if no operator is there) } } while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output if ( '(' === $op ) { return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced } $output[] = $op; } return $output; } /** * Evaluate postfix notation. * * @param mixed $tokens * @param array $vars * * @return mixed */ private static function pfx( $tokens, $vars = array() ) { if ( false == $tokens ) { return false; } $stack = new WC_Eval_Math_Stack; foreach ( $tokens as $token ) { // nice and easy // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) { if ( is_null( $op2 = $stack->pop() ) ) { return self::trigger( "internal error" ); } if ( is_null( $op1 = $stack->pop() ) ) { return self::trigger( "internal error" ); } switch ( $token ) { case '+': $stack->push( $op1 + $op2 ); break; case '-': $stack->push( $op1 - $op2 ); break; case '*': $stack->push( $op1 * $op2 ); break; case '/': if ( 0 == $op2 ) { return self::trigger( 'division by zero' ); } $stack->push( $op1 / $op2 ); break; case '^': $stack->push( pow( $op1, $op2 ) ); break; } // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif ( '_' === $token ) { $stack->push( -1 * $stack->pop() ); // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif ( ! preg_match( "/^([a-z]\w*)\($/", $token, $matches ) ) { if ( is_numeric( $token ) ) { $stack->push( $token ); } elseif ( array_key_exists( $token, self::$v ) ) { $stack->push( self::$v[ $token ] ); } elseif ( array_key_exists( $token, $vars ) ) { $stack->push( $vars[ $token ] ); } else { return self::trigger( "undefined variable '$token'" ); } } } // when we're out of tokens, the stack should have a single element, the final result if ( 1 != $stack->count ) { return self::trigger( "internal error" ); } return $stack->pop(); } /** * Trigger an error, but nicely, if need be. * * @param string $msg * * @return bool */ private static function trigger( $msg ) { self::$last_error = $msg; if ( ! Constants::is_true( 'DOING_AJAX' ) && Constants::is_true( 'WP_DEBUG' ) ) { echo "\nError found in:"; self::debugPrintCallingFunction(); trigger_error( $msg, E_USER_WARNING ); } return false; } /** * Prints the file name, function name, and * line number which called your function * (not this function, then one that called * it to begin with) */ private static function debugPrintCallingFunction() { $file = 'n/a'; $func = 'n/a'; $line = 'n/a'; $debugTrace = debug_backtrace(); if ( isset( $debugTrace[1] ) ) { $file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a'; $line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a'; } if ( isset( $debugTrace[2] ) ) { $func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a'; } echo "\n$file, $func, $line\n"; } } /** * Class WC_Eval_Math_Stack. */ class WC_Eval_Math_Stack { /** * Stack array. * * @var array */ public $stack = array(); /** * Stack counter. * * @var integer */ public $count = 0; /** * Push value into stack. * * @param mixed $val */ public function push( $val ) { $this->stack[ $this->count ] = $val; $this->count++; } /** * Pop value from stack. * * @return mixed */ public function pop() { if ( $this->count > 0 ) { $this->count--; return $this->stack[ $this->count ]; } return null; } /** * Get last value from stack. * * @param int $n * * @return mixed */ public function last( $n=1 ) { $key = $this->count - $n; return array_key_exists( $key, $this->stack ) ? $this->stack[ $key ] : null; } } } includes/libraries/wp-async-request.php 0000644 00000005244 15132754524 0014303 0 ustar 00 <?php /** * WP Async Request * * @package WP-Background-Processing */ defined( 'ABSPATH' ) || exit; /** * Abstract WP_Async_Request class. */ abstract class WP_Async_Request { /** * Prefix * * (default value: 'wp') * * @var string * @access protected */ protected $prefix = 'wp'; /** * Action * * (default value: 'async_request') * * @var string * @access protected */ protected $action = 'async_request'; /** * Identifier * * @var mixed * @access protected */ protected $identifier; /** * Data * * (default value: array()) * * @var array * @access protected */ protected $data = array(); /** * Initiate new async request */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request * * @return array|WP_Error */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } return array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); } /** * Get query URL * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } return admin_url( 'admin-ajax.php' ); } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } return array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); } /** * Maybe handle * * Check for correct nonce and pass to handler. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Handle * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } includes/class-wc-customer-download-log.php 0000644 00000006574 15132754524 0015046 0 ustar 00 <?php /** * Class for customer download logs. * * @package WooCommerce\Classes * @version 3.3.0 * @since 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Customer download log class. */ class WC_Customer_Download_Log extends WC_Data { /** * This is the name of this object type. * * @var string */ protected $object_type = 'customer_download_log'; /** * Download Log Data array. * * @var array */ protected $data = array( 'timestamp' => null, 'permission_id' => 0, 'user_id' => null, 'user_ip_address' => null, ); /** * Constructor. * * @param int|object|array $download_log Download log ID. */ public function __construct( $download_log = 0 ) { parent::__construct( $download_log ); if ( is_numeric( $download_log ) && $download_log > 0 ) { $this->set_id( $download_log ); } elseif ( $download_log instanceof self ) { $this->set_id( $download_log->get_id() ); } elseif ( is_object( $download_log ) && ! empty( $download_log->download_log_id ) ) { $this->set_id( $download_log->download_log_id ); $this->set_props( (array) $download_log ); $this->set_object_read( true ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'customer-download-log' ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get timestamp. * * @param string $context Get context. * @return WC_DateTime|null Object if the date is set or null if there is no date. */ public function get_timestamp( $context = 'view' ) { return $this->get_prop( 'timestamp', $context ); } /** * Get permission id. * * @param string $context Get context. * @return integer */ public function get_permission_id( $context = 'view' ) { return $this->get_prop( 'permission_id', $context ); } /** * Get user id. * * @param string $context Get context. * @return integer */ public function get_user_id( $context = 'view' ) { return $this->get_prop( 'user_id', $context ); } /** * Get user ip address. * * @param string $context Get context. * @return string */ public function get_user_ip_address( $context = 'view' ) { return $this->get_prop( 'user_ip_address', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set timestamp. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_timestamp( $date = null ) { $this->set_date_prop( 'timestamp', $date ); } /** * Set permission id. * * @param int $value Value to set. */ public function set_permission_id( $value ) { $this->set_prop( 'permission_id', absint( $value ) ); } /** * Set user id. * * @param int $value Value to set. */ public function set_user_id( $value ) { $this->set_prop( 'user_id', absint( $value ) ); } /** * Set user ip address. * * @param string $value Value to set. */ public function set_user_ip_address( $value ) { $this->set_prop( 'user_ip_address', $value ); } } includes/class-wc-meta-data.php 0000644 00000004272 15132754524 0012447 0 ustar 00 <?php /** * Wraps an array (meta data for now) and tells if there was any changes. * * The main idea behind this class is to avoid doing unneeded * SQL updates if nothing changed. * * @version 3.2.0 * @package WooCommerce */ defined( 'ABSPATH' ) || exit; /** * Meta data class. */ class WC_Meta_Data implements JsonSerializable { /** * Current data for metadata * * @since 3.2.0 * @var array */ protected $current_data; /** * Metadata data * * @since 3.2.0 * @var array */ protected $data; /** * Constructor. * * @param array $meta Data to wrap behind this function. */ public function __construct( $meta = array() ) { $this->current_data = $meta; $this->apply_changes(); } /** * When converted to JSON. * * @return object|array */ public function jsonSerialize() { return $this->get_data(); } /** * Merge changes with data and clear. */ public function apply_changes() { $this->data = $this->current_data; } /** * Creates or updates a property in the metadata object. * * @param string $key Key to set. * @param mixed $value Value to set. */ public function __set( $key, $value ) { $this->current_data[ $key ] = $value; } /** * Checks if a given key exists in our data. This is called internally * by `empty` and `isset`. * * @param string $key Key to check if set. * * @return bool */ public function __isset( $key ) { return array_key_exists( $key, $this->current_data ); } /** * Returns the value of any property. * * @param string $key Key to get. * @return mixed Property value or NULL if it does not exists */ public function __get( $key ) { if ( array_key_exists( $key, $this->current_data ) ) { return $this->current_data[ $key ]; } return null; } /** * Return data changes only. * * @return array */ public function get_changes() { $changes = array(); foreach ( $this->current_data as $id => $value ) { if ( ! array_key_exists( $id, $this->data ) || $value !== $this->data[ $id ] ) { $changes[ $id ] = $value; } } return $changes; } /** * Return all data as an array. * * @return array */ public function get_data() { return $this->data; } } includes/theme-support/class-wc-twenty-eleven.php 0000644 00000002453 15132754524 0016233 0 ustar 00 <?php /** * Twenty Eleven support. * * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Eleven class. */ class WC_Twenty_Eleven { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 150, 'single_image_width' => 300, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary"><div id="content" role="main" class="twentyeleven">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div>'; } } WC_Twenty_Eleven::init(); includes/theme-support/class-wc-twenty-twenty-one.php 0000644 00000004234 15132754524 0017065 0 ustar 00 <?php /** * Twenty Twenty One support. * * @since 4.7.0 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Twenty_One class. */ class WC_Twenty_Twenty_One { /** * Theme init. */ public static function init() { // Change WooCommerce wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); // This theme doesn't have a traditional sidebar. remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 ); // Enqueue theme compatibility styles. add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) ); // Enqueue wp-admin compatibility styles. add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_styles' ) ); // Register theme features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 450, 'single_image_width' => 600, ) ); } /** * Enqueue CSS for this theme. * * @param array $styles Array of registered styles. * @return array */ public static function enqueue_styles( $styles ) { unset( $styles['woocommerce-general'] ); $styles['woocommerce-general'] = array( 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one.css', 'deps' => '', 'version' => Constants::get_constant( 'WC_VERSION' ), 'media' => 'all', 'has_rtl' => true, ); return apply_filters( 'woocommerce_twenty_twenty_one_styles', $styles ); } /** * Enqueue the wp-admin CSS overrides for this theme. */ public static function enqueue_admin_styles() { wp_enqueue_style( 'woocommerce-twenty-twenty-one-admin', str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one-admin.css', '', Constants::get_constant( 'WC_VERSION' ), 'all' ); } } WC_Twenty_Twenty_One::init(); includes/theme-support/class-wc-twenty-seventeen.php 0000644 00000006425 15132754524 0016754 0 ustar 00 <?php /** * Twenty Seventeen support. * * @since 2.6.9 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Seventeen class. */ class WC_Twenty_Seventeen { /** * Theme init. */ public static function init() { remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 ); add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) ); add_filter( 'twentyseventeen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 ); add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 250, 'single_image_width' => 350, ) ); } /** * Enqueue CSS for this theme. * * @param array $styles Array of registered styles. * @return array */ public static function enqueue_styles( $styles ) { unset( $styles['woocommerce-general'] ); $styles['woocommerce-general'] = array( 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-seventeen.css', 'deps' => '', 'version' => Constants::get_constant( 'WC_VERSION' ), 'media' => 'all', 'has_rtl' => true, ); return apply_filters( 'woocommerce_twenty_seventeen_styles', $styles ); } /** * Open the Twenty Seventeen wrapper. */ public static function output_content_wrapper() { echo '<div class="wrap">'; echo '<div id="primary" class="content-area twentyseventeen">'; echo '<main id="main" class="site-main" role="main">'; } /** * Close the Twenty Seventeen wrapper. */ public static function output_content_wrapper_end() { echo '</main>'; echo '</div>'; get_sidebar(); echo '</div>'; } /** * Custom colors. * * @param string $css Styles. * @param string $hue Color. * @param string $saturation Saturation. * @return string */ public static function custom_colors_css( $css, $hue, $saturation ) { $css .= ' .colors-custom .select2-container--default .select2-selection--single { border-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); } .colors-custom .select2-container--default .select2-selection__rendered { color: hsl( ' . $hue . ', ' . $saturation . ', 40% ); } .colors-custom .select2-container--default .select2-selection--single .select2-selection__arrow b { border-color: hsl( ' . $hue . ', ' . $saturation . ', 40% ) transparent transparent transparent; } .colors-custom .select2-container--focus .select2-selection { border-color: #000; } .colors-custom .select2-container--focus .select2-selection--single .select2-selection__arrow b { border-color: #000 transparent transparent transparent; } .colors-custom .select2-container--focus .select2-selection .select2-selection__rendered { color: #000; } '; return $css; } } WC_Twenty_Seventeen::init(); includes/theme-support/class-wc-twenty-ten.php 0000644 00000002414 15132754524 0015540 0 ustar 00 <?php /** * Twenty Ten support. * * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Ten class. */ class WC_Twenty_Ten { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 200, 'single_image_width' => 300, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="container"><div id="content" role="main">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div>'; } } WC_Twenty_Ten::init(); includes/theme-support/class-wc-twenty-twenty.php 0000644 00000005471 15132754524 0016312 0 ustar 00 <?php /** * Twenty Twenty support. * * @since 3.8.1 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Twenty class. */ class WC_Twenty_Twenty { /** * Theme init. */ public static function init() { // Change WooCommerce wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 ); // This theme doesn't have a traditional sidebar. remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 ); // Enqueue theme compatibility styles. add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) ); // Register theme features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 450, 'single_image_width' => 600, ) ); // Background color change. add_action( 'after_setup_theme', array( __CLASS__, 'set_white_background' ), 10 ); } /** * Open the Twenty Twenty wrapper. */ public static function output_content_wrapper() { echo '<section id="primary" class="content-area">'; echo '<main id="main" class="site-main">'; } /** * Close the Twenty Twenty wrapper. */ public static function output_content_wrapper_end() { echo '</main>'; echo '</section>'; } /** * Set background color to white if it's default, otherwise don't touch it. */ public static function set_white_background() { $background = sanitize_hex_color_no_hash( get_theme_mod( 'background_color' ) ); $background_default = 'f5efe0'; // Don't change user's choice of background color. if ( ! empty( $background ) && $background !== $background_default ) { return; } // In case default background is found, change it to white. set_theme_mod( 'background_color', 'fff' ); } /** * Enqueue CSS for this theme. * * @param array $styles Array of registered styles. * @return array */ public static function enqueue_styles( $styles ) { unset( $styles['woocommerce-general'] ); $styles['woocommerce-general'] = array( 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty.css', 'deps' => '', 'version' => Constants::get_constant( 'WC_VERSION' ), 'media' => 'all', 'has_rtl' => true, ); return apply_filters( 'woocommerce_twenty_twenty_styles', $styles ); } } WC_Twenty_Twenty::init(); includes/theme-support/class-wc-twenty-fourteen.php 0000644 00000002652 15132754524 0016605 0 ustar 00 <?php /** * Twenty Fourteen support. * * @class WC_Twenty_Fourteen * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Fourteen class. */ class WC_Twenty_Fourteen { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 150, 'single_image_width' => 300, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary" class="content-area"><div id="content" role="main" class="site-content twentyfourteen"><div class="tfwc">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div></div>'; get_sidebar( 'content' ); } } WC_Twenty_Fourteen::init(); includes/theme-support/class-wc-twenty-thirteen.php 0000644 00000002567 15132754524 0016605 0 ustar 00 <?php /** * Twenty Thirteen support. * * @class WC_Twenty_Thirteen * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Thirteen class. */ class WC_Twenty_Thirteen { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 200, 'single_image_width' => 300, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary" class="site-content"><div id="content" role="main" class="entry-content twentythirteen">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div>'; } } WC_Twenty_Thirteen::init(); includes/theme-support/class-wc-twenty-twelve.php 0000644 00000002535 15132754524 0016264 0 ustar 00 <?php /** * Twenty Twelve support. * * @class WC_Twenty_Twelve * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Twelve class. */ class WC_Twenty_Twelve { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 200, 'single_image_width' => 300, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary" class="site-content"><div id="content" role="main" class="twentytwelve">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div>'; } } WC_Twenty_Twelve::init(); includes/theme-support/class-wc-twenty-sixteen.php 0000644 00000002516 15132754524 0016434 0 ustar 00 <?php /** * Twenty Sixteen support. * * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Sixteen class. */ class WC_Twenty_Sixteen { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 250, 'single_image_width' => 400, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary" class="content-area twentysixteen"><main id="main" class="site-main" role="main">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</main></div>'; } } WC_Twenty_Sixteen::init(); includes/theme-support/class-wc-twenty-fifteen.php 0000644 00000002560 15132754524 0016374 0 ustar 00 <?php /** * Twenty Fifteen support. * * @class WC_Twenty_Fifteen * @since 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Fifteen class. */ class WC_Twenty_Fifteen { /** * Theme init. */ public static function init() { // Remove default wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' ); // Add custom wrappers. add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) ); // Declare theme support for features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 200, 'single_image_width' => 350, ) ); } /** * Open wrappers. */ public static function output_content_wrapper() { echo '<div id="primary" role="main" class="content-area twentyfifteen"><div id="main" class="site-main t15wc">'; } /** * Close wrappers. */ public static function output_content_wrapper_end() { echo '</div></div>'; } } WC_Twenty_Fifteen::init(); includes/theme-support/class-wc-twenty-nineteen.php 0000644 00000007112 15132754524 0016557 0 ustar 00 <?php /** * Twenty Nineteen support. * * @since 3.5.X * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Twenty_Nineteen class. */ class WC_Twenty_Nineteen { /** * Theme init. */ public static function init() { // Change WooCommerce wrappers. remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 ); add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 ); // This theme doesn't have a traditional sidebar. remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 ); // Enqueue theme compatibility styles. add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) ); // Register theme features. add_theme_support( 'wc-product-gallery-zoom' ); add_theme_support( 'wc-product-gallery-lightbox' ); add_theme_support( 'wc-product-gallery-slider' ); add_theme_support( 'woocommerce', array( 'thumbnail_image_width' => 300, 'single_image_width' => 450, ) ); // Tweak Twenty Nineteen features. add_action( 'wp', array( __CLASS__, 'tweak_theme_features' ) ); // Color scheme CSS. add_filter( 'twentynineteen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 ); } /** * Open the Twenty Nineteen wrapper. */ public static function output_content_wrapper() { echo '<section id="primary" class="content-area">'; echo '<main id="main" class="site-main">'; } /** * Close the Twenty Nineteen wrapper. */ public static function output_content_wrapper_end() { echo '</main>'; echo '</section>'; } /** * Enqueue CSS for this theme. * * @param array $styles Array of registered styles. * @return array */ public static function enqueue_styles( $styles ) { unset( $styles['woocommerce-general'] ); $styles['woocommerce-general'] = array( 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-nineteen.css', 'deps' => '', 'version' => Constants::get_constant( 'WC_VERSION' ), 'media' => 'all', 'has_rtl' => true, ); return apply_filters( 'woocommerce_twenty_nineteen_styles', $styles ); } /** * Tweak Twenty Nineteen features. */ public static function tweak_theme_features() { if ( is_woocommerce() ) { add_filter( 'twentynineteen_can_show_post_thumbnail', '__return_false' ); } } /** * Filters Twenty Nineteen custom colors CSS. * * @param string $css Base theme colors CSS. * @param int $primary_color The user's selected color hue. * @param string $saturation Filtered theme color saturation level. */ public static function custom_colors_css( $css, $primary_color, $saturation ) { if ( function_exists( 'register_block_type' ) && is_admin() ) { return $css; } $lightness = absint( apply_filters( 'twentynineteen_custom_colors_lightness', 33 ) ); $lightness = $lightness . '%'; $css .= ' .onsale, .woocommerce-info, .woocommerce-store-notice { background-color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); } .woocommerce-tabs ul li.active a { color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); box-shadow: 0 2px 0 hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); } '; return $css; } } WC_Twenty_Nineteen::init(); includes/data-stores/class-wc-order-item-fee-data-store.php 0000644 00000004005 15132754524 0017677 0 ustar 00 <?php /** * Class WC_Order_Item_Fee_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Fee Data Store * * @version 3.0.0 */ class WC_Order_Item_Fee_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface { /** * Data stored in meta keys. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_fee_amount', '_tax_class', '_tax_status', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' ); /** * Read/populate data properties specific to this order item. * * @since 3.0.0 * @param WC_Order_Item_Fee $item Fee order item object. */ public function read( &$item ) { parent::read( $item ); $id = $item->get_id(); $item->set_props( array( 'amount' => get_metadata( 'order_item', $id, '_fee_amount', true ), 'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ), 'tax_status' => get_metadata( 'order_item', $id, '_tax_status', true ), 'total' => get_metadata( 'order_item', $id, '_line_total', true ), 'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ), ) ); $item->set_object_read( true ); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $id will be set. * * @since 3.0.0 * @param WC_Order_Item_Fee $item Fee order item object. */ public function save_item_data( &$item ) { $id = $item->get_id(); $save_values = array( '_fee_amount' => $item->get_amount( 'edit' ), '_tax_class' => $item->get_tax_class( 'edit' ), '_tax_status' => $item->get_tax_status( 'edit' ), '_line_total' => $item->get_total( 'edit' ), '_line_tax' => $item->get_total_tax( 'edit' ), '_line_tax_data' => $item->get_taxes( 'edit' ), ); foreach ( $save_values as $key => $value ) { update_metadata( 'order_item', $id, $key, $value ); } } } includes/data-stores/class-wc-customer-download-log-data-store.php 0000644 00000015052 15132754524 0021324 0 ustar 00 <?php /** * Class WC_Customer_Download_Log_Data_Store file. * * @version 3.3.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Customer_Download_Log_Data_Store class. */ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Data_Store_Interface { // Table name for download logs. const WC_DOWNLOAD_LOG_TABLE = 'wc_download_log'; /** * Get the table name for download logs. * * @return string */ public static function get_table_name() { return self::WC_DOWNLOAD_LOG_TABLE; } /** * Create download log entry. * * @param WC_Customer_Download_Log $download_log Customer download log object. */ public function create( WC_Customer_Download_Log &$download_log ) { global $wpdb; // Always set a timestamp. if ( is_null( $download_log->get_timestamp( 'edit' ) ) ) { $download_log->set_timestamp( time() ); } $data = array( 'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ), 'permission_id' => $download_log->get_permission_id( 'edit' ), 'user_id' => $download_log->get_user_id( 'edit' ), 'user_ip_address' => $download_log->get_user_ip_address( 'edit' ), ); $format = array( '%s', '%s', '%s', '%s', ); $result = $wpdb->insert( $wpdb->prefix . self::get_table_name(), apply_filters( 'woocommerce_downloadable_product_download_log_insert_data', $data ), apply_filters( 'woocommerce_downloadable_product_download_log_insert_format', $format, $data ) ); do_action( 'woocommerce_downloadable_product_download_log_insert', $data ); if ( $result ) { $download_log->set_id( $wpdb->insert_id ); $download_log->apply_changes(); } else { wp_die( esc_html__( 'Unable to insert download log entry in database.', 'woocommerce' ) ); } } /** * Method to read a download log from the database. * * @param WC_Customer_Download_Log $download_log Download log object. * @throws Exception Exception when read is not possible. */ public function read( &$download_log ) { global $wpdb; $download_log->set_defaults(); // Ensure we have an id to pull from the DB. if ( ! $download_log->get_id() ) { throw new Exception( __( 'Invalid download log: no ID.', 'woocommerce' ) ); } $table = $wpdb->prefix . self::get_table_name(); // Query the DB for the download log. $raw_download_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE download_log_id = %d", $download_log->get_id() ) ); // WPCS: unprepared SQL ok. if ( ! $raw_download_log ) { throw new Exception( __( 'Invalid download log: not found.', 'woocommerce' ) ); } $download_log->set_props( array( 'timestamp' => strtotime( $raw_download_log->timestamp ), 'permission_id' => $raw_download_log->permission_id, 'user_id' => $raw_download_log->user_id, 'user_ip_address' => $raw_download_log->user_ip_address, ) ); $download_log->set_object_read( true ); } /** * Method to update a download log in the database. * * @param WC_Customer_Download_Log $download_log Download log object. */ public function update( &$download_log ) { global $wpdb; $data = array( 'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ), 'permission_id' => $download_log->get_permission_id( 'edit' ), 'user_id' => $download_log->get_user_id( 'edit' ), 'user_ip_address' => $download_log->get_user_ip_address( 'edit' ), ); $format = array( '%s', '%s', '%s', '%s', ); $wpdb->update( $wpdb->prefix . self::get_table_name(), $data, array( 'download_log_id' => $download_log->get_id(), ), $format ); $download_log->apply_changes(); } /** * Get a download log object. * * @param array $data From the DB. * @return WC_Customer_Download_Log */ private function get_download_log( $data ) { return new WC_Customer_Download_Log( $data ); } /** * Get array of download log ids by specified args. * * @param array $args Arguments to define download logs to retrieve. * @return array */ public function get_download_logs( $args = array() ) { global $wpdb; $args = wp_parse_args( $args, array( 'permission_id' => '', 'user_id' => '', 'user_ip_address' => '', 'orderby' => 'download_log_id', 'order' => 'ASC', 'limit' => -1, 'page' => 1, 'return' => 'objects', ) ); $query = array(); $table = $wpdb->prefix . self::get_table_name(); $query[] = "SELECT * FROM {$table} WHERE 1=1"; if ( $args['permission_id'] ) { $query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] ); } if ( $args['user_id'] ) { $query[] = $wpdb->prepare( 'AND user_id = %d', $args['user_id'] ); } if ( $args['user_ip_address'] ) { $query[] = $wpdb->prepare( 'AND user_ip_address = %s', $args['user_ip_address'] ); } $allowed_orders = array( 'download_log_id', 'timestamp', 'permission_id', 'user_id' ); $orderby = in_array( $args['orderby'], $allowed_orders, true ) ? $args['orderby'] : 'download_log_id'; $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); $query[] = "ORDER BY {$orderby_sql}"; if ( 0 < $args['limit'] ) { $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); } $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok. switch ( $args['return'] ) { case 'ids': return wp_list_pluck( $raw_download_logs, 'download_log_id' ); default: return array_map( array( $this, 'get_download_log' ), $raw_download_logs ); } } /** * Get download logs for a given download permission. * * @param int $permission_id Permission to get logs for. * @return array */ public function get_download_logs_for_permission( $permission_id ) { // If no permission_id is passed, return an empty array. if ( empty( $permission_id ) ) { return array(); } return $this->get_download_logs( array( 'permission_id' => $permission_id, ) ); } /** * Method to delete download logs for a given permission ID. * * @since 3.4.0 * @param int $id download_id of the downloads that will be deleted. */ public function delete_by_permission_id( $id ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) ); } } includes/data-stores/abstract-wc-order-item-type-data-store.php 0000644 00000010657 15132754524 0020631 0 ustar 00 <?php /** * Class Abstract_WC_Order_Item_Type_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Data Store * * @version 3.0.0 */ abstract class Abstract_WC_Order_Item_Type_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface { /** * Meta type. This should match up with * the types available at https://developer.wordpress.org/reference/functions/add_metadata/. * WP defines 'post', 'user', 'comment', and 'term'. * * @var string */ protected $meta_type = 'order_item'; /** * This only needs set if you are using a custom metadata type (for example payment tokens. * This should be the name of the field your table uses for associating meta with objects. * For example, in payment_tokenmeta, this would be payment_token_id. * * @var string */ protected $object_id_field_for_meta = 'order_item_id'; /** * Create a new order item in the database. * * @since 3.0.0 * @param WC_Order_Item $item Order item object. */ public function create( &$item ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_name' => $item->get_name(), 'order_item_type' => $item->get_type(), 'order_id' => $item->get_order_id(), ) ); $item->set_id( $wpdb->insert_id ); $this->save_item_data( $item ); $item->save_meta_data(); $item->apply_changes(); $this->clear_cache( $item ); do_action( 'woocommerce_new_order_item', $item->get_id(), $item, $item->get_order_id() ); } /** * Update a order item in the database. * * @since 3.0.0 * @param WC_Order_Item $item Order item object. */ public function update( &$item ) { global $wpdb; $changes = $item->get_changes(); if ( array_intersect( array( 'name', 'order_id' ), array_keys( $changes ) ) ) { $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_name' => $item->get_name(), 'order_item_type' => $item->get_type(), 'order_id' => $item->get_order_id(), ), array( 'order_item_id' => $item->get_id() ) ); } $this->save_item_data( $item ); $item->save_meta_data(); $item->apply_changes(); $this->clear_cache( $item ); do_action( 'woocommerce_update_order_item', $item->get_id(), $item, $item->get_order_id() ); } /** * Remove an order item from the database. * * @since 3.0.0 * @param WC_Order_Item $item Order item object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$item, $args = array() ) { if ( $item->get_id() ) { global $wpdb; do_action( 'woocommerce_before_delete_order_item', $item->get_id() ); $wpdb->delete( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_id' => $item->get_id() ) ); $wpdb->delete( $wpdb->prefix . 'woocommerce_order_itemmeta', array( 'order_item_id' => $item->get_id() ) ); do_action( 'woocommerce_delete_order_item', $item->get_id() ); $this->clear_cache( $item ); } } /** * Read a order item from the database. * * @since 3.0.0 * * @param WC_Order_Item $item Order item object. * * @throws Exception If invalid order item. */ public function read( &$item ) { global $wpdb; $item->set_defaults(); // Get from cache if available. $data = wp_cache_get( 'item-' . $item->get_id(), 'order-items' ); if ( false === $data ) { $data = $wpdb->get_row( $wpdb->prepare( "SELECT order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item->get_id() ) ); wp_cache_set( 'item-' . $item->get_id(), $data, 'order-items' ); } if ( ! $data ) { throw new Exception( __( 'Invalid order item.', 'woocommerce' ) ); } $item->set_props( array( 'order_id' => $data->order_id, 'name' => $data->order_item_name, ) ); $item->read_meta_data(); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $item->get_id() will be set. * * @since 3.0.0 * @param WC_Order_Item $item Order item object. */ public function save_item_data( &$item ) {} /** * Clear meta cache. * * @param WC_Order_Item $item Order item object. */ public function clear_cache( &$item ) { wp_cache_delete( 'item-' . $item->get_id(), 'order-items' ); wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' ); wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' ); } } includes/data-stores/class-wc-webhook-data-store.php 0000644 00000034324 15132754524 0016540 0 ustar 00 <?php /** * Webhook Data Store * * @version 3.3.0 * @package WooCommerce\Classes\Data_Store */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Webhook data store class. */ class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface { /** * Create a new webhook in the database. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. */ public function create( &$webhook ) { global $wpdb; $changes = $webhook->get_changes(); if ( isset( $changes['date_created'] ) ) { $date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' ); $date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() ); } else { $date_created = current_time( 'mysql' ); $date_created_gmt = current_time( 'mysql', 1 ); $webhook->set_date_created( $date_created ); } // Pending delivery by default if not set while creating a new webhook. if ( ! isset( $changes['pending_delivery'] ) ) { $webhook->set_pending_delivery( true ); } $data = array( 'status' => $webhook->get_status( 'edit' ), 'name' => $webhook->get_name( 'edit' ), 'user_id' => $webhook->get_user_id( 'edit' ), 'delivery_url' => $webhook->get_delivery_url( 'edit' ), 'secret' => $webhook->get_secret( 'edit' ), 'topic' => $webhook->get_topic( 'edit' ), 'date_created' => $date_created, 'date_created_gmt' => $date_created_gmt, 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), 'failure_count' => $webhook->get_failure_count( 'edit' ), 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), ); $wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok. $webhook_id = $wpdb->insert_id; $webhook->set_id( $webhook_id ); $webhook->apply_changes(); $this->delete_transients( $webhook->get_status( 'edit' ) ); WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); do_action( 'woocommerce_new_webhook', $webhook_id, $webhook ); } /** * Read a webhook from the database. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. * @throws Exception When webhook is invalid. */ public function read( &$webhook ) { global $wpdb; $data = wp_cache_get( $webhook->get_id(), 'webhooks' ); if ( false === $data ) { $data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok. wp_cache_add( $webhook->get_id(), $data, 'webhooks' ); } if ( is_array( $data ) ) { $webhook->set_props( array( 'id' => $data['webhook_id'], 'status' => $data['status'], 'name' => $data['name'], 'user_id' => $data['user_id'], 'delivery_url' => $data['delivery_url'], 'secret' => $data['secret'], 'topic' => $data['topic'], 'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'], 'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'], 'api_version' => $data['api_version'], 'failure_count' => $data['failure_count'], 'pending_delivery' => $data['pending_delivery'], ) ); $webhook->set_object_read( true ); do_action( 'woocommerce_webhook_loaded', $webhook ); } else { throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) ); } } /** * Update a webhook. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. */ public function update( &$webhook ) { global $wpdb; $changes = $webhook->get_changes(); $trigger = isset( $changes['delivery_url'] ); if ( isset( $changes['date_modified'] ) ) { $date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ); $date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() ); } else { $date_modified = current_time( 'mysql' ); $date_modified_gmt = current_time( 'mysql', 1 ); $webhook->set_date_modified( $date_modified ); } $data = array( 'status' => $webhook->get_status( 'edit' ), 'name' => $webhook->get_name( 'edit' ), 'user_id' => $webhook->get_user_id( 'edit' ), 'delivery_url' => $webhook->get_delivery_url( 'edit' ), 'secret' => $webhook->get_secret( 'edit' ), 'topic' => $webhook->get_topic( 'edit' ), 'date_modified' => $date_modified, 'date_modified_gmt' => $date_modified_gmt, 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), 'failure_count' => $webhook->get_failure_count( 'edit' ), 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), ); $wpdb->update( $wpdb->prefix . 'wc_webhooks', $data, array( 'webhook_id' => $webhook->get_id(), ) ); // WPCS: DB call ok. $webhook->apply_changes(); if ( isset( $changes['status'] ) ) { // We need to delete all transients, because we can't be sure of the old status. $this->delete_transients( 'all' ); } wp_cache_delete( $webhook->get_id(), 'webhooks' ); WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) { $webhook->deliver_ping(); } do_action( 'woocommerce_webhook_updated', $webhook->get_id() ); } /** * Remove a webhook from the database. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. */ public function delete( &$webhook ) { global $wpdb; $wpdb->delete( $wpdb->prefix . 'wc_webhooks', array( 'webhook_id' => $webhook->get_id(), ), array( '%d' ) ); // WPCS: cache ok, DB call ok. $this->delete_transients( 'all' ); wp_cache_delete( $webhook->get_id(), 'webhooks' ); WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook ); } /** * Get API version number. * * @since 3.3.0 * @param string $api_version REST API version. * @return int */ public function get_api_version_number( $api_version ) { return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) ); } /** * Get webhooks IDs from the database. * * @since 3.3.0 * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys. * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0. * @return int[] */ public function get_webhooks_ids( $status = '' ) { if ( ! empty( $status ) ) { $this->validate_status( $status ); } $ids = get_transient( $this->get_transient_key( $status ) ); if ( false === $ids ) { $ids = $this->search_webhooks( array( 'limit' => -1, 'status' => $status, ) ); $ids = array_map( 'absint', $ids ); set_transient( $this->get_transient_key( $status ), $ids ); } return $ids; } /** * Search webhooks. * * @param array $args Search arguments. * @return array|object */ public function search_webhooks( $args ) { global $wpdb; $args = wp_parse_args( $args, array( 'limit' => 10, 'offset' => 0, 'order' => 'DESC', 'orderby' => 'id', 'paginate' => false, ) ); // Map post statuses. $statuses = array( 'publish' => 'active', 'draft' => 'paused', 'pending' => 'disabled', ); // Map orderby to support a few post keys. $orderby_mapping = array( 'ID' => 'webhook_id', 'id' => 'webhook_id', 'name' => 'name', 'title' => 'name', 'post_title' => 'name', 'post_name' => 'name', 'date_created' => 'date_created_gmt', 'date' => 'date_created_gmt', 'post_date' => 'date_created_gmt', 'date_modified' => 'date_modified_gmt', 'modified' => 'date_modified_gmt', 'post_modified' => 'date_modified_gmt', ); $orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id'; $sort = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; $order = "ORDER BY {$orderby} {$sort}"; $limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : ''; $offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : ''; $status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : ''; $search = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : ''; $include = ''; $exclude = ''; $date_created = ''; $date_modified = ''; if ( ! empty( $args['include'] ) ) { $args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) ); $include = 'AND webhook_id IN (' . $args['include'] . ')'; } if ( ! empty( $args['exclude'] ) ) { $args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) ); $exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')'; } if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) { $args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after']; $args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before']; $date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')"; } if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) { $args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after']; $args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before']; $date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')"; } // Check for cache. $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) ); $cache_value = wp_cache_get( $cache_key, 'webhook_search_results' ); if ( $cache_value ) { return $cache_value; } if ( $args['paginate'] ) { $query = trim( "SELECT SQL_CALC_FOUND_ROWS webhook_id FROM {$wpdb->prefix}wc_webhooks WHERE 1=1 {$status} {$search} {$include} {$exclude} {$date_created} {$date_modified} {$order} {$limit} {$offset}" ); $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' ); $return_value = (object) array( 'webhooks' => $webhook_ids, 'total' => $total, 'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1, ); } else { $query = trim( "SELECT webhook_id FROM {$wpdb->prefix}wc_webhooks WHERE 1=1 {$status} {$search} {$include} {$exclude} {$date_created} {$date_modified} {$order} {$limit} {$offset}" ); $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $return_value = $webhook_ids; } wp_cache_set( $cache_key, $return_value, 'webhook_search_results' ); return $return_value; } /** * Count webhooks. * * @since 3.6.0 * @param string $status Status to count. * @return int */ protected function get_webhook_count( $status = 'active' ) { global $wpdb; $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count'; $count = wp_cache_get( $cache_key, 'webhooks' ); if ( false === $count ) { $count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) ); wp_cache_add( $cache_key, $count, 'webhooks' ); } return $count; } /** * Get total webhook counts by status. * * @return array */ public function get_count_webhooks_by_status() { $statuses = array_keys( wc_get_webhook_statuses() ); $counts = array(); foreach ( $statuses as $status ) { $counts[ $status ] = $this->get_webhook_count( $status ); } return $counts; } /** * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses(). * * @since 3.6.0 * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys. * @param string $status Status to check. */ private function validate_status( $status ) { if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) ); } } /** * Get the transient key used to cache a set of webhook IDs, optionally filtered by status. * * @since 3.6.0 * @param string $status Optional - status of cache key. * @return string */ private function get_transient_key( $status = '' ) { return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status ); } /** * Delete the transients used to cache a set of webhook IDs, optionally filtered by status. * * @since 3.6.0 * @param string $status Optional - status of cache to delete, or 'all' to delete all caches. */ private function delete_transients( $status = '' ) { // Always delete the non-filtered cache. delete_transient( $this->get_transient_key( '' ) ); if ( ! empty( $status ) ) { if ( 'all' === $status ) { foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) { delete_transient( $this->get_transient_key( $status_key ) ); } } else { delete_transient( $this->get_transient_key( $status ) ); } } } } includes/data-stores/class-wc-order-item-coupon-data-store.php 0000644 00000003044 15132754524 0020445 0 ustar 00 <?php /** * Class WC_Order_Item_Coupon_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Coupon Data Store * * @version 3.0.0 */ class WC_Order_Item_Coupon_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface { /** * Data stored in meta keys. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( 'discount_amount', 'discount_amount_tax' ); /** * Read/populate data properties specific to this order item. * * @since 3.0.0 * @param WC_Order_Item_Coupon $item Coupon order item. */ public function read( &$item ) { parent::read( $item ); $id = $item->get_id(); $item->set_props( array( 'discount' => get_metadata( 'order_item', $id, 'discount_amount', true ), 'discount_tax' => get_metadata( 'order_item', $id, 'discount_amount_tax', true ), ) ); $item->set_object_read( true ); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $item->get_id() will be set. * * @since 3.0.0 * @param WC_Order_Item_Coupon $item Coupon order item. */ public function save_item_data( &$item ) { $id = $item->get_id(); $save_values = array( 'discount_amount' => $item->get_discount( 'edit' ), 'discount_amount_tax' => $item->get_discount_tax( 'edit' ), ); foreach ( $save_values as $key => $value ) { update_metadata( 'order_item', $id, $key, $value ); } } } includes/data-stores/class-wc-order-data-store-cpt.php 0000644 00000111011 15132754524 0016766 0 ustar 00 <?php /** * WC_Order_Data_Store_CPT class file. * * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Data_Store_Interface { /** * Data stored in meta keys, but not considered "meta" for an order. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_customer_user', '_order_key', '_order_currency', '_billing_first_name', '_billing_last_name', '_billing_company', '_billing_address_1', '_billing_address_2', '_billing_city', '_billing_state', '_billing_postcode', '_billing_country', '_billing_email', '_billing_phone', '_shipping_first_name', '_shipping_last_name', '_shipping_company', '_shipping_address_1', '_shipping_address_2', '_shipping_city', '_shipping_state', '_shipping_postcode', '_shipping_country', '_shipping_phone', '_completed_date', '_paid_date', '_edit_lock', '_edit_last', '_cart_discount', '_cart_discount_tax', '_order_shipping', '_order_shipping_tax', '_order_tax', '_order_total', '_payment_method', '_payment_method_title', '_transaction_id', '_customer_ip_address', '_customer_user_agent', '_created_via', '_order_version', '_prices_include_tax', '_date_completed', '_date_paid', '_payment_tokens', '_billing_address_index', '_shipping_address_index', '_recorded_sales', '_recorded_coupon_usage_counts', '_download_permissions_granted', '_order_stock_reduced', ); /** * Method to create a new order in the database. * * @param WC_Order $order Order object. */ public function create( &$order ) { if ( '' === $order->get_order_key() ) { $order->set_order_key( wc_generate_order_key() ); } parent::create( $order ); do_action( 'woocommerce_new_order', $order->get_id(), $order ); } /** * Read order data. Can be overridden by child classes to load other props. * * @param WC_Order $order Order object. * @param object $post_object Post object. * @since 3.0.0 */ protected function read_order_data( &$order, $post_object ) { parent::read_order_data( $order, $post_object ); $id = $order->get_id(); $date_completed = get_post_meta( $id, '_date_completed', true ); $date_paid = get_post_meta( $id, '_date_paid', true ); if ( ! $date_completed ) { $date_completed = get_post_meta( $id, '_completed_date', true ); } if ( ! $date_paid ) { $date_paid = get_post_meta( $id, '_paid_date', true ); } $order->set_props( array( 'order_key' => get_post_meta( $id, '_order_key', true ), 'customer_id' => get_post_meta( $id, '_customer_user', true ), 'billing_first_name' => get_post_meta( $id, '_billing_first_name', true ), 'billing_last_name' => get_post_meta( $id, '_billing_last_name', true ), 'billing_company' => get_post_meta( $id, '_billing_company', true ), 'billing_address_1' => get_post_meta( $id, '_billing_address_1', true ), 'billing_address_2' => get_post_meta( $id, '_billing_address_2', true ), 'billing_city' => get_post_meta( $id, '_billing_city', true ), 'billing_state' => get_post_meta( $id, '_billing_state', true ), 'billing_postcode' => get_post_meta( $id, '_billing_postcode', true ), 'billing_country' => get_post_meta( $id, '_billing_country', true ), 'billing_email' => get_post_meta( $id, '_billing_email', true ), 'billing_phone' => get_post_meta( $id, '_billing_phone', true ), 'shipping_first_name' => get_post_meta( $id, '_shipping_first_name', true ), 'shipping_last_name' => get_post_meta( $id, '_shipping_last_name', true ), 'shipping_company' => get_post_meta( $id, '_shipping_company', true ), 'shipping_address_1' => get_post_meta( $id, '_shipping_address_1', true ), 'shipping_address_2' => get_post_meta( $id, '_shipping_address_2', true ), 'shipping_city' => get_post_meta( $id, '_shipping_city', true ), 'shipping_state' => get_post_meta( $id, '_shipping_state', true ), 'shipping_postcode' => get_post_meta( $id, '_shipping_postcode', true ), 'shipping_country' => get_post_meta( $id, '_shipping_country', true ), 'shipping_phone' => get_post_meta( $id, '_shipping_phone', true ), 'payment_method' => get_post_meta( $id, '_payment_method', true ), 'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ), 'transaction_id' => get_post_meta( $id, '_transaction_id', true ), 'customer_ip_address' => get_post_meta( $id, '_customer_ip_address', true ), 'customer_user_agent' => get_post_meta( $id, '_customer_user_agent', true ), 'created_via' => get_post_meta( $id, '_created_via', true ), 'date_completed' => $date_completed, 'date_paid' => $date_paid, 'cart_hash' => get_post_meta( $id, '_cart_hash', true ), 'customer_note' => $post_object->post_excerpt, ) ); } /** * Method to update an order in the database. * * @param WC_Order $order Order object. */ public function update( &$order ) { // Before updating, ensure date paid is set if missing. if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) { $order->set_date_paid( $order->get_date_created( 'edit' ) ); } // Also grab the current status so we can compare. $previous_status = get_post_status( $order->get_id() ); // Update the order. parent::update( $order ); // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. $new_status = $order->get_status( 'edit' ); if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) { do_action( 'woocommerce_new_order', $order->get_id(), $order ); } else { do_action( 'woocommerce_update_order', $order->get_id(), $order ); } } /** * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. * * @param WC_Order $order Order object. * @since 3.0.0 */ protected function update_post_meta( &$order ) { $updated_props = array(); $id = $order->get_id(); $meta_key_to_props = array( '_order_key' => 'order_key', '_customer_user' => 'customer_id', '_payment_method' => 'payment_method', '_payment_method_title' => 'payment_method_title', '_transaction_id' => 'transaction_id', '_customer_ip_address' => 'customer_ip_address', '_customer_user_agent' => 'customer_user_agent', '_created_via' => 'created_via', '_date_completed' => 'date_completed', '_date_paid' => 'date_paid', '_cart_hash' => 'cart_hash', ); $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $order->{"get_$prop"}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; switch ( $prop ) { case 'date_paid': case 'date_completed': $value = ! is_null( $value ) ? $value->getTimestamp() : ''; break; } $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); if ( $updated ) { $updated_props[] = $prop; } } $address_props = array( 'billing' => array( '_billing_first_name' => 'billing_first_name', '_billing_last_name' => 'billing_last_name', '_billing_company' => 'billing_company', '_billing_address_1' => 'billing_address_1', '_billing_address_2' => 'billing_address_2', '_billing_city' => 'billing_city', '_billing_state' => 'billing_state', '_billing_postcode' => 'billing_postcode', '_billing_country' => 'billing_country', '_billing_email' => 'billing_email', '_billing_phone' => 'billing_phone', ), 'shipping' => array( '_shipping_first_name' => 'shipping_first_name', '_shipping_last_name' => 'shipping_last_name', '_shipping_company' => 'shipping_company', '_shipping_address_1' => 'shipping_address_1', '_shipping_address_2' => 'shipping_address_2', '_shipping_city' => 'shipping_city', '_shipping_state' => 'shipping_state', '_shipping_postcode' => 'shipping_postcode', '_shipping_country' => 'shipping_country', '_shipping_phone' => 'shipping_phone', ), ); foreach ( $address_props as $props_key => $props ) { $props_to_update = $this->get_props_to_update( $order, $props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $order->{"get_$prop"}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); if ( $updated ) { $updated_props[] = $prop; $updated_props[] = $props_key; } } } parent::update_post_meta( $order ); // If address changed, store concatenated version to make searches faster. if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) { update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) ); } if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) { update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) ); } // Legacy date handling. @todo remove in 4.0. if ( in_array( 'date_paid', $updated_props, true ) ) { $value = $order->get_date_paid( 'edit' ); // In 2.6.x date_paid was stored as _paid_date in local mysql format. update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); } if ( in_array( 'date_completed', $updated_props, true ) ) { $value = $order->get_date_completed( 'edit' ); // In 2.6.x date_completed was stored as _completed_date in local mysql format. update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); } // If customer changed, update any downloadable permissions. if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); } // Mark user account as active. if ( in_array( 'customer_id', $updated_props, true ) ) { wc_update_user_last_active( $order->get_customer_id() ); } do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); } /** * Excerpt for post. * * @param WC_Order $order Order object. * @return string */ protected function get_post_excerpt( $order ) { return $order->get_customer_note(); } /** * Get order key. * * @since 4.3.0 * @param WC_order $order Order object. * @return string */ protected function get_order_key( $order ) { if ( '' !== $order->get_order_key() ) { return $order->get_order_key(); } return parent::get_order_key( $order ); } /** * Get amount already refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_refunded( $order ) { global $wpdb; $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM( postmeta.meta_value ) FROM $wpdb->postmeta AS postmeta INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) WHERE postmeta.meta_key = '_refund_amount' AND postmeta.post_id = posts.ID", $order->get_id() ) ); return floatval( $total ); } /** * Get the total tax refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_tax_refunded( $order ) { global $wpdb; $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM( order_itemmeta.meta_value ) FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) WHERE order_itemmeta.order_item_id = order_items.order_item_id AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')", $order->get_id() ) ); return abs( $total ); } /** * Get the total shipping refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_shipping_refunded( $order ) { global $wpdb; $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM( order_itemmeta.meta_value ) FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) WHERE order_itemmeta.order_item_id = order_items.order_item_id AND order_itemmeta.meta_key IN ('cost')", $order->get_id() ) ); return abs( $total ); } /** * Finds an Order ID based on an order key. * * @param string $order_key An order key has generated by. * @return int The ID of an order, or 0 if the order could not be found */ public function get_order_id_by_order_key( $order_key ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) ); } /** * Return count of orders with a specific status. * * @param string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses. * @return int */ public function get_order_count( $status ) { global $wpdb; return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) ); } /** * Get all orders matching the passed in args. * * @deprecated 3.1.0 - Use wc_get_orders instead. * @see wc_get_orders() * * @param array $args List of args passed to wc_get_orders(). * * @return array|object */ public function get_orders( $args = array() ) { wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' ); return wc_get_orders( $args ); } /** * Generate meta query for wc_get_orders. * * @param array $values List of customers ids or emails. * @param string $relation 'or' or 'and' relation used to build the WP meta_query. * @return array */ private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { $meta_query = array( 'relation' => strtoupper( $relation ), 'customer_emails' => array( 'key' => '_billing_email', 'value' => array(), 'compare' => 'IN', ), 'customer_ids' => array( 'key' => '_customer_user', 'value' => array(), 'compare' => 'IN', ), ); foreach ( $values as $value ) { if ( is_array( $value ) ) { $query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' ); if ( is_wp_error( $query_part ) ) { return $query_part; } $meta_query[] = $query_part; } elseif ( is_email( $value ) ) { $meta_query['customer_emails']['value'][] = sanitize_email( $value ); } elseif ( is_numeric( $value ) ) { $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); } else { return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values ); } } if ( empty( $meta_query['customer_emails']['value'] ) ) { unset( $meta_query['customer_emails'] ); unset( $meta_query['relation'] ); } if ( empty( $meta_query['customer_ids']['value'] ) ) { unset( $meta_query['customer_ids'] ); unset( $meta_query['relation'] ); } return $meta_query; } /** * Get unpaid orders after a certain date, * * @param int $date Timestamp. * @return array */ public function get_unpaid_orders( $date ) { global $wpdb; $unpaid_orders = $wpdb->get_col( $wpdb->prepare( // @codingStandardsIgnoreStart "SELECT posts.ID FROM {$wpdb->posts} AS posts WHERE posts.post_type IN ('" . implode( "','", wc_get_order_types() ) . "') AND posts.post_status = 'wc-pending' AND posts.post_modified < %s", // @codingStandardsIgnoreEnd gmdate( 'Y-m-d H:i:s', absint( $date ) ) ) ); return $unpaid_orders; } /** * Search order data for a term and return ids. * * @param string $term Searched term. * @return array of ids */ public function search_orders( $term ) { global $wpdb; /** * Searches on meta data can be slow - this lets you choose what fields to search. * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). * * @var array */ $search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_shop_order_search_fields', array( '_billing_address_index', '_shipping_address_index', '_billing_last_name', '_billing_email', ) ) ); $order_ids = array(); if ( is_numeric( $term ) ) { $order_ids[] = absint( $term ); } if ( ! empty( $search_fields ) ) { $order_ids = array_unique( array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' ) ), $wpdb->get_col( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items as order_items WHERE order_item_name LIKE %s", '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' ) ) ) ); } return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); } /** * Gets information about whether permissions were generated yet. * * @param WC_Order|int $order Order ID or order object. * @return bool */ public function get_download_permissions_granted( $order ) { $order_id = WC_Order_Factory::get_order_id( $order ); return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); } /** * Stores information about whether permissions were generated yet. * * @param WC_Order|int $order Order ID or order object. * @param bool $set True or false. */ public function set_download_permissions_granted( $order, $set ) { $order_id = WC_Order_Factory::get_order_id( $order ); update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); } /** * Gets information about whether sales were recorded. * * @param WC_Order|int $order Order ID or order object. * @return bool */ public function get_recorded_sales( $order ) { $order_id = WC_Order_Factory::get_order_id( $order ); return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); } /** * Stores information about whether sales were recorded. * * @param WC_Order|int $order Order ID or order object. * @param bool $set True or false. */ public function set_recorded_sales( $order, $set ) { $order_id = WC_Order_Factory::get_order_id( $order ); update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); } /** * Gets information about whether coupon counts were updated. * * @param WC_Order|int $order Order ID or order object. * @return bool */ public function get_recorded_coupon_usage_counts( $order ) { $order_id = WC_Order_Factory::get_order_id( $order ); return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); } /** * Stores information about whether coupon counts were updated. * * @param WC_Order|int $order Order ID or order object. * @param bool $set True or false. */ public function set_recorded_coupon_usage_counts( $order, $set ) { $order_id = WC_Order_Factory::get_order_id( $order ); update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); } /** * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys. * Pass $coupon_id if key for only one of the coupon is needed. * * @param WC_Order $order Order object. * @param int $coupon_id If passed, will return held key for that coupon. * * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon. */ public function get_coupon_held_keys( $order, $coupon_id = null ) { $held_keys = $order->get_meta( '_coupon_held_keys' ); if ( $coupon_id ) { return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null; } return $held_keys; } /** * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys. * * @param WC_Order $order Order object. * @param int $coupon_id If passed, will return held key for that coupon. * * @return mixed */ public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { $held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' ); if ( $coupon_id ) { return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null; } return $held_keys_for_user; } /** * Add/Update list of meta keys that are currently being used by this order to hold a coupon. * This is used to figure out what all meta entries we should delete when order is cancelled/completed. * * @param WC_Order $order Order object. * @param array $held_keys Array of coupon_code => meta_key. * @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user. * * @return mixed */ public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) { $order->update_meta_data( '_coupon_held_keys', $held_keys ); } if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) { $order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user ); } } /** * Release all coupons held by this order. * * @param WC_Order $order Current order object. * @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request. */ public function release_held_coupons( $order, $save = true ) { $coupon_held_keys = $this->get_coupon_held_keys( $order ); if ( is_array( $coupon_held_keys ) ) { foreach ( $coupon_held_keys as $coupon_id => $meta_key ) { delete_post_meta( $coupon_id, $meta_key ); } } $order->delete_meta_data( '_coupon_held_keys' ); $coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order ); if ( is_array( $coupon_held_keys_for_users ) ) { foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) { delete_post_meta( $coupon_id, $meta_key ); } } $order->delete_meta_data( '_coupon_held_keys_for_users' ); if ( $save ) { $order->save_meta_data(); } } /** * Gets information about whether stock was reduced. * * @param WC_Order|int $order Order ID or order object. * @return bool */ public function get_stock_reduced( $order ) { $order_id = WC_Order_Factory::get_order_id( $order ); return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); } /** * Stores information about whether stock was reduced. * * @param WC_Order|int $order Order ID or order object. * @param bool $set True or false. */ public function set_stock_reduced( $order, $set ) { $order_id = WC_Order_Factory::get_order_id( $order ); update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); } /** * Get the order type based on Order ID. * * @since 3.0.0 * @param int|WP_Post $order Order | Order id. * * @return string */ public function get_order_type( $order ) { return get_post_type( $order ); } /** * Get valid WP_Query args from a WC_Order_Query's query variables. * * @since 3.1.0 * @param array $query_vars query vars from a WC_Order_Query. * @return array */ protected function get_wp_query_args( $query_vars ) { // Map query vars to ones that get_wp_query_args or WP_Query recognize. $key_mapping = array( 'customer_id' => 'customer_user', 'status' => 'post_status', 'currency' => 'order_currency', 'version' => 'order_version', 'discount_total' => 'cart_discount', 'discount_tax' => 'cart_discount_tax', 'shipping_total' => 'order_shipping', 'shipping_tax' => 'order_shipping_tax', 'cart_tax' => 'order_tax', 'total' => 'order_total', 'page' => 'paged', ); foreach ( $key_mapping as $query_key => $db_key ) { if ( isset( $query_vars[ $query_key ] ) ) { $query_vars[ $db_key ] = $query_vars[ $query_key ]; unset( $query_vars[ $query_key ] ); } } // Add the 'wc-' prefix to status if needed. if ( ! empty( $query_vars['post_status'] ) ) { if ( is_array( $query_vars['post_status'] ) ) { foreach ( $query_vars['post_status'] as &$status ) { $status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status; } } else { $query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status']; } } $wp_query_args = parent::get_wp_query_args( $query_vars ); if ( ! isset( $wp_query_args['date_query'] ) ) { $wp_query_args['date_query'] = array(); } if ( ! isset( $wp_query_args['meta_query'] ) ) { $wp_query_args['meta_query'] = array(); } $date_queries = array( 'date_created' => 'post_date', 'date_modified' => 'post_modified', 'date_completed' => '_date_completed', 'date_paid' => '_date_paid', ); foreach ( $date_queries as $query_var_key => $db_key ) { if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { // Remove any existing meta queries for the same keys to prevent conflicts. $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); $meta_query_index = array_search( $db_key, $existing_queries, true ); if ( false !== $meta_query_index ) { unset( $wp_query_args['meta_query'][ $meta_query_index ] ); } $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); } } if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) { $values = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] ); $customer_query = $this->get_orders_generate_customer_meta_query( $values ); if ( is_wp_error( $customer_query ) ) { $wp_query_args['errors'][] = $customer_query; } else { $wp_query_args['meta_query'][] = $customer_query; } } if ( isset( $query_vars['anonymized'] ) ) { if ( $query_vars['anonymized'] ) { $wp_query_args['meta_query'][] = array( 'key' => '_anonymized', 'value' => 'yes', ); } else { $wp_query_args['meta_query'][] = array( 'key' => '_anonymized', 'compare' => 'NOT EXISTS', ); } } if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { $wp_query_args['no_found_rows'] = true; } return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this ); } /** * Query for Orders matching specific criteria. * * @since 3.1.0 * * @param array $query_vars query vars from a WC_Order_Query. * * @return array|object */ public function query( $query_vars ) { $args = $this->get_wp_query_args( $query_vars ); if ( ! empty( $args['errors'] ) ) { $query = (object) array( 'posts' => array(), 'found_posts' => 0, 'max_num_pages' => 0, ); } else { $query = new WP_Query( $args ); } if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) { $orders = $query->posts; } else { update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches. $order_ids = wp_list_pluck( $query->posts, 'ID' ); $orders = $this->compile_orders( $order_ids, $query_vars, $query ); } if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { return (object) array( 'orders' => $orders, 'total' => $query->found_posts, 'max_num_pages' => $query->max_num_pages, ); } return $orders; } /** * Compile order response and set caches as needed for order ids. * * @param array $order_ids List of order IDS to compile. * @param array $query_vars Original query arguments. * @param WP_Query $query Query object. * * @return array Orders. */ private function compile_orders( $order_ids, $query_vars, $query ) { if ( empty( $order_ids ) ) { return array(); } $orders = array(); // Lets do some cache hydrations so that we don't have to fetch data from DB for every order. $this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars ); $this->prime_refund_caches_for_order( $order_ids, $query_vars ); $this->prime_order_item_caches_for_orders( $order_ids, $query_vars ); foreach ( $query->posts as $post ) { $order = wc_get_order( $post ); // If the order returns false, don't add it to the list. if ( false === $order ) { continue; } $orders[] = $order; } return $orders; } /** * Prime refund cache for orders. * * @param array $order_ids Order Ids to prime cache for. * @param array $query_vars Query vars for the query. */ private function prime_refund_caches_for_order( $order_ids, $query_vars ) { if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) { return; } if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) { return; } } $cache_keys_mapping = array(); foreach ( $order_ids as $order_id ) { $cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id; } $non_cached_ids = array(); $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); foreach ( $order_ids as $order_id ) { if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { $non_cached_ids[] = $order_id; } } if ( empty( $non_cached_ids ) ) { return; } $refunds = wc_get_orders( array( 'type' => 'shop_order_refund', 'post_parent__in' => $non_cached_ids, 'limit' => - 1, ) ); $order_refunds = array_reduce( $refunds, function ( $order_refunds_array, WC_Order_Refund $refund ) { if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) { $order_refunds_array[ $refund->get_parent_id() ] = array(); } $order_refunds_array[ $refund->get_parent_id() ][] = $refund; return $order_refunds_array; }, array() ); foreach ( $non_cached_ids as $order_id ) { $refunds = array(); if ( isset( $order_refunds[ $order_id ] ) ) { $refunds = $order_refunds[ $order_id ]; } wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' ); } } /** * Prime following caches: * 1. item-$order_item_id For individual items. * 2. order-items-$order-id For fetching items associated with an order. * 3. order-item meta. * * @param array $order_ids Order Ids to prime cache for. * @param array $query_vars Query vars for the query. */ private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) { global $wpdb; if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { $line_items = array( 'line_items', 'shipping_lines', 'fee_lines', 'coupon_lines', ); if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) { return; } } $cache_keys = array_map( function ( $order_id ) { return 'order-items-' . $order_id; }, $order_ids ); $cache_values = wc_cache_get_multiple( $cache_keys, 'orders' ); $non_cached_ids = array(); foreach ( $order_ids as $order_id ) { if ( false === $cache_values[ 'order-items-' . $order_id ] ) { $non_cached_ids[] = $order_id; } } if ( empty( $non_cached_ids ) ) { return; } $non_cached_ids = esc_sql( $non_cached_ids ); $non_cached_ids_string = implode( ',', $non_cached_ids ); $order_items = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;" ); if ( empty( $order_items ) ) { return; } $order_items_for_all_orders = array_reduce( $order_items, function ( $order_items_collection, $order_item ) { if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) { $order_items_collection[ $order_item->order_id ] = array(); } $order_items_collection[ $order_item->order_id ][] = $order_item; return $order_items_collection; } ); foreach ( $order_items_for_all_orders as $order_id => $items ) { wp_cache_set( 'order-items-' . $order_id, $items, 'orders' ); } foreach ( $order_items as $item ) { wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); } $order_item_ids = wp_list_pluck( $order_items, 'order_item_id' ); update_meta_cache( 'order_item', $order_item_ids ); } /** * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it. * * @param array $order_ids Order Ids to prime cache for. * @param array $query_vars Query vars for the query. */ private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) { global $wpdb; if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) { return; } } $cache_keys_mapping = array(); foreach ( $order_ids as $order_id ) { $cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' ); } $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); $non_cached_ids = array(); foreach ( $order_ids as $order_id ) { if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { $non_cached_ids[] = $order_id; } } if ( empty( $non_cached_ids ) ) { return; } $order_ids = esc_sql( $non_cached_ids ); $order_ids_in = "'" . implode( "', '", $order_ids ) . "'"; $raw_meta_data_array = $wpdb->get_results( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT post_id as object_id, meta_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ( $order_ids_in ) ORDER BY post_id" // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); $raw_meta_data_collection = array_reduce( $raw_meta_data_array, function ( $collection, $raw_meta_data ) { if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) { $collection[ $raw_meta_data->object_id ] = array(); } $collection[ $raw_meta_data->object_id ][] = $raw_meta_data; return $collection; }, array() ); WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' ); } /** * Return the order type of a given item which belongs to WC_Order. * * @since 3.2.0 * @param WC_Order $order Order Object. * @param int $order_item_id Order item id. * @return string Order Item type */ public function get_order_item_type( $order, $order_item_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) ); } } includes/data-stores/class-wc-order-item-data-store.php 0000644 00000011673 15132754524 0017153 0 ustar 00 <?php /** * Class WC_Order_Item_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Data Store: Misc Order Item Data functions. * * @version 3.0.0 */ class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface { /** * Add an order item to an order. * * @since 3.0.0 * @param int $order_id Order ID. * @param array $item order_item_name and order_item_type. * @return int Order Item ID */ public function add_order_item( $order_id, $item ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_name' => $item['order_item_name'], 'order_item_type' => $item['order_item_type'], 'order_id' => $order_id, ), array( '%s', '%s', '%d', ) ); $item_id = absint( $wpdb->insert_id ); $this->clear_caches( $item_id, $order_id ); return $item_id; } /** * Update an order item. * * @since 3.0.0 * @param int $item_id Item ID. * @param array $item order_item_name or order_item_type. * @return boolean */ public function update_order_item( $item_id, $item ) { global $wpdb; $updated = $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) ); $this->clear_caches( $item_id, null ); return $updated; } /** * Delete an order item. * * @since 3.0.0 * @param int $item_id Item ID. */ public function delete_order_item( $item_id ) { // Load the order ID before the deletion, since after, it won't exist in the database. $order_id = $this->get_order_id_by_order_item_id( $item_id ); global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) ); $this->clear_caches( $item_id, $order_id ); } /** * Update term meta. * * @since 3.0.0 * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param string $prev_value (default: ''). * @return bool */ public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ) { return update_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value ); } /** * Add term meta. * * @since 3.0.0 * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param bool $unique (default: false). * @return int New row ID or 0 */ public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ) { return add_metadata( 'order_item', $item_id, wp_slash( $meta_key ), is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $unique ); } /** * Delete term meta. * * @since 3.0.0 * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param string $meta_value (default: ''). * @param bool $delete_all (default: false). * @return bool */ public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { return delete_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $delete_all ); } /** * Get term meta. * * @since 3.0.0 * @param int $item_id Item ID. * @param string $key Meta key. * @param bool $single (default: true). * @return mixed */ public function get_metadata( $item_id, $key, $single = true ) { return get_metadata( 'order_item', $item_id, $key, $single ); } /** * Get order ID by order item ID. * * @since 3.0.0 * @param int $item_id Item ID. * @return int */ public function get_order_id_by_order_item_id( $item_id ) { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) ); } /** * Get the order item type based on Item ID. * * @since 3.0.0 * @param int $item_id Item ID. * @return string|null Order item type or null if no order item entry found. */ public function get_order_item_type( $item_id ) { global $wpdb; $order_item_type = $wpdb->get_var( $wpdb->prepare( "SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item_id ) ); return $order_item_type; } /** * Clear meta cache. * * @param int $item_id Item ID. * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID. */ protected function clear_caches( $item_id, $order_id ) { wp_cache_delete( 'item-' . $item_id, 'order-items' ); if ( ! $order_id ) { $order_id = $this->get_order_id_by_order_item_id( $item_id ); } if ( $order_id ) { wp_cache_delete( 'order-items-' . $order_id, 'orders' ); } } } includes/data-stores/class-wc-order-item-product-data-store.php 0000644 00000006210 15132754524 0020620 0 ustar 00 <?php /** * Class WC_Order_Item_Product_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Product Data Store * * @version 3.0.0 */ class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface { /** * Data stored in meta keys. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' ); /** * Read/populate data properties specific to this order item. * * @since 3.0.0 * @param WC_Order_Item_Product $item Product order item object. */ public function read( &$item ) { parent::read( $item ); $id = $item->get_id(); $item->set_props( array( 'product_id' => get_metadata( 'order_item', $id, '_product_id', true ), 'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ), 'quantity' => get_metadata( 'order_item', $id, '_qty', true ), 'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ), 'subtotal' => get_metadata( 'order_item', $id, '_line_subtotal', true ), 'total' => get_metadata( 'order_item', $id, '_line_total', true ), 'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ), ) ); $item->set_object_read( true ); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $id will be set. * * @since 3.0.0 * @param WC_Order_Item_Product $item Product order item object. */ public function save_item_data( &$item ) { $id = $item->get_id(); $changes = $item->get_changes(); $meta_key_to_props = array( '_product_id' => 'product_id', '_variation_id' => 'variation_id', '_qty' => 'quantity', '_tax_class' => 'tax_class', '_line_subtotal' => 'subtotal', '_line_subtotal_tax' => 'subtotal_tax', '_line_total' => 'total', '_line_tax' => 'total_tax', '_line_tax_data' => 'taxes', ); $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); foreach ( $props_to_update as $meta_key => $prop ) { update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); } } /** * Get a list of download IDs for a specific item from an order. * * @since 3.0.0 * @param WC_Order_Item_Product $item Product order item object. * @param WC_Order $order Order object. * @return array */ public function get_download_ids( $item, $order ) { global $wpdb; return $wpdb->get_col( $wpdb->prepare( "SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id", $order->get_billing_email(), $order->get_order_key(), $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id() ) ); } } includes/data-stores/class-wc-shipping-zone-data-store.php 0000644 00000026274 15132754524 0017701 0 ustar 00 <?php /** * Class WC_Shipping_Zone_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Shipping Zone Data Store. * * @version 3.0.0 */ class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Shipping_Zone_Data_Store_Interface, WC_Object_Data_Store_Interface { /** * Method to create a new shipping zone. * * @since 3.0.0 * @param WC_Shipping_Zone $zone Shipping zone object. */ public function create( &$zone ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_name' => $zone->get_zone_name(), 'zone_order' => $zone->get_zone_order(), ) ); $zone->set_id( $wpdb->insert_id ); $zone->save_meta_data(); $this->save_locations( $zone ); $zone->apply_changes(); WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); WC_Cache_Helper::get_transient_version( 'shipping', true ); } /** * Update zone in the database. * * @since 3.0.0 * @param WC_Shipping_Zone $zone Shipping zone object. */ public function update( &$zone ) { global $wpdb; if ( $zone->get_id() ) { $wpdb->update( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_name' => $zone->get_zone_name(), 'zone_order' => $zone->get_zone_order(), ), array( 'zone_id' => $zone->get_id() ) ); } $zone->save_meta_data(); $this->save_locations( $zone ); $zone->apply_changes(); WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); WC_Cache_Helper::get_transient_version( 'shipping', true ); } /** * Method to read a shipping zone from the database. * * @since 3.0.0 * @param WC_Shipping_Zone $zone Shipping zone object. * @throws Exception If invalid data store. */ public function read( &$zone ) { global $wpdb; // Zone 0 is used as a default if no other zones fit. if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) { $this->read_zone_locations( $zone ); $zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) ); $zone->read_meta_data(); $zone->set_object_read( true ); /** * Indicate that the WooCommerce shipping zone has been loaded. * * @param WC_Shipping_Zone $zone The shipping zone that has been loaded. */ do_action( 'woocommerce_shipping_zone_loaded', $zone ); return; } $zone_data = $wpdb->get_row( $wpdb->prepare( "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1", $zone->get_id() ) ); if ( ! $zone_data ) { throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); } $zone->set_zone_name( $zone_data->zone_name ); $zone->set_zone_order( $zone_data->zone_order ); $this->read_zone_locations( $zone ); $zone->read_meta_data(); $zone->set_object_read( true ); /** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */ do_action( 'woocommerce_shipping_zone_loaded', $zone ); } /** * Deletes a shipping zone from the database. * * @since 3.0.0 * @param WC_Shipping_Zone $zone Shipping zone object. * @param array $args Array of args to pass to the delete method. * @return void */ public function delete( &$zone, $args = array() ) { $zone_id = $zone->get_id(); if ( $zone_id ) { global $wpdb; // Delete methods and their settings. $methods = $this->get_methods( $zone_id, false ); if ( $methods ) { foreach ( $methods as $method ) { $this->delete_method( $method->instance_id ); } } // Delete zone. $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) ); $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) ); $zone->set_id( null ); WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); WC_Cache_Helper::get_transient_version( 'shipping', true ); do_action( 'woocommerce_delete_shipping_zone', $zone_id ); } } /** * Get a list of shipping methods for a specific zone. * * @since 3.0.0 * @param int $zone_id Zone ID. * @param bool $enabled_only True to request enabled methods only. * @return array Array of objects containing method_id, method_order, instance_id, is_enabled */ public function get_methods( $zone_id, $enabled_only ) { global $wpdb; if ( $enabled_only ) { $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1"; } else { $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d"; } return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Get count of methods for a zone. * * @since 3.0.0 * @param int $zone_id Zone ID. * @return int Method Count */ public function get_method_count( $zone_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) ); } /** * Add a shipping method to a zone. * * @since 3.0.0 * @param int $zone_id Zone ID. * @param string $type Method Type/ID. * @param int $order Method Order. * @return int Instance ID */ public function add_method( $zone_id, $type, $order ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'method_id' => $type, 'zone_id' => $zone_id, 'method_order' => $order, ), array( '%s', '%d', '%d', ) ); return $wpdb->insert_id; } /** * Delete a method instance. * * @since 3.0.0 * @param int $instance_id Instance ID. */ public function delete_method( $instance_id ) { global $wpdb; $method = $this->get_method( $instance_id ); if ( ! $method ) { return; } delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' ); $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) ); do_action( 'woocommerce_delete_shipping_zone_method', $instance_id ); } /** * Get a shipping zone method instance. * * @since 3.0.0 * @param int $instance_id Instance ID. * @return object */ public function get_method( $instance_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) ); } /** * Find a matching zone ID for a given package. * * @since 3.0.0 * @param object $package Package information. * @return int */ public function get_zone_id_from_package( $package ) { global $wpdb; $country = strtoupper( wc_clean( $package['destination']['country'] ) ); $state = strtoupper( wc_clean( $package['destination']['state'] ) ); $continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) ); $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); // Work out criteria for our zone search. $criteria = array(); $criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country ); $criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state ); $criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent ); $criteria[] = 'OR ( location_type IS NULL ) )'; // Postcode range and wildcard matching. $postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" ); if ( $postcode_locations ) { $zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) ); $matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country ); $do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) ); if ( ! empty( $do_not_match ) ) { $criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')'; } } /** * Get shipping zone criteria * * @since 3.6.6 * @param array $criteria Get zone criteria. * @param array $package Package information. * @param array $postcode_locations Postcode range and wildcard matching. */ $criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations ); // Get matching zones. return $wpdb->get_var( "SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode' WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared . ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1' ); } /** * Return an ordered list of zones. * * @since 3.0.0 * @return array An array of objects containing a zone_id, zone_name, and zone_order. */ public function get_zones() { global $wpdb; return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" ); } /** * Return a zone ID from an instance ID. * * @since 3.0.0 * @param int $id Instnace ID. * @return int */ public function get_zone_id_by_instance_id( $id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) ); } /** * Read location data from the database. * * @param WC_Shipping_Zone $zone Shipping zone object. */ private function read_zone_locations( &$zone ) { global $wpdb; $locations = $wpdb->get_results( $wpdb->prepare( "SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d", $zone->get_id() ) ); if ( $locations ) { foreach ( $locations as $location ) { $zone->add_location( $location->location_code, $location->location_type ); } } } /** * Save locations to the DB. * This function clears old locations, then re-inserts new if any changes are found. * * @since 3.0.0 * * @param WC_Shipping_Zone $zone Shipping zone object. * * @return bool|void */ private function save_locations( &$zone ) { $changed_props = array_keys( $zone->get_changes() ); if ( ! in_array( 'zone_locations', $changed_props, true ) ) { return false; } global $wpdb; $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); foreach ( $zone->get_zone_locations( 'edit' ) as $location ) { $wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id(), 'location_code' => $location->code, 'location_type' => $location->type, ) ); } } } includes/data-stores/class-wc-payment-token-data-store.php 0000644 00000024657 15132754524 0017705 0 ustar 00 <?php /** * Class WC_Payment_Token_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Payment Token Data Store: Custom Table. * * @version 3.0.0 */ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment_Token_Data_Store_Interface, WC_Object_Data_Store_Interface { /** * Meta type. Payment tokens are a new object type. * * @var string */ protected $meta_type = 'payment_token'; /** * If we have already saved our extra data, don't do automatic / default handling. * * @var bool */ protected $extra_data_saved = false; /** * Create a new payment token in the database. * * @since 3.0.0 * * @param WC_Payment_Token $token Payment token object. * * @throws Exception Throw exception if invalid or missing payment token fields. */ public function create( &$token ) { if ( false === $token->validate() ) { throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); } global $wpdb; if ( ! $token->is_default() && $token->get_user_id() > 0 ) { $default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() ); if ( is_null( $default_token ) ) { $token->set_default( true ); } } $payment_token_data = array( 'gateway_id' => $token->get_gateway_id( 'edit' ), 'token' => $token->get_token( 'edit' ), 'user_id' => $token->get_user_id( 'edit' ), 'type' => $token->get_type( 'edit' ), ); $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data ); $token_id = $wpdb->insert_id; $token->set_id( $token_id ); $this->save_extra_data( $token, true ); $token->save_meta_data(); $token->apply_changes(); // Make sure all other tokens are not set to default. if ( $token->is_default() && $token->get_user_id() > 0 ) { WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id ); } do_action( 'woocommerce_new_payment_token', $token_id, $token ); } /** * Update a payment token. * * @since 3.0.0 * * @param WC_Payment_Token $token Payment token object. * * @throws Exception Throw exception if invalid or missing payment token fields. */ public function update( &$token ) { if ( false === $token->validate() ) { throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); } global $wpdb; $updated_props = array(); $core_props = array( 'gateway_id', 'token', 'user_id', 'type' ); $changed_props = array_keys( $token->get_changes() ); foreach ( $changed_props as $prop ) { if ( ! in_array( $prop, $core_props, true ) ) { continue; } $updated_props[] = $prop; $payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' ); } if ( ! empty( $payment_token_data ) ) { $wpdb->update( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data, array( 'token_id' => $token->get_id() ) ); } $updated_extra_props = $this->save_extra_data( $token ); $updated_props = array_merge( $updated_props, $updated_extra_props ); $token->save_meta_data(); $token->apply_changes(); // Make sure all other tokens are not set to default. if ( $token->is_default() && $token->get_user_id() > 0 ) { WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() ); } do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props ); do_action( 'woocommerce_payment_token_updated', $token->get_id() ); } /** * Remove a payment token from the database. * * @since 3.0.0 * @param WC_Payment_Token $token Payment token object. * @param bool $force_delete Unused param. */ public function delete( &$token, $force_delete = false ) { global $wpdb; $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) ); $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) ); do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token ); } /** * Read a token from the database. * * @since 3.0.0 * * @param WC_Payment_Token $token Payment token object. * * @throws Exception Throw exception if invalid payment token. */ public function read( &$token ) { global $wpdb; $data = $wpdb->get_row( $wpdb->prepare( "SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1", $token->get_id() ) ); if ( $data ) { $token->set_props( array( 'token' => $data->token, 'user_id' => $data->user_id, 'gateway_id' => $data->gateway_id, 'default' => $data->is_default, ) ); $this->read_extra_data( $token ); $token->read_meta_data(); $token->set_object_read( true ); do_action( 'woocommerce_payment_token_loaded', $token ); } else { throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) ); } } /** * Read extra data associated with the token (like last4 digits of a card for expiry dates). * * @param WC_Payment_Token $token Payment token object. * @since 3.0.0 */ protected function read_extra_data( &$token ) { foreach ( $token->get_extra_data_keys() as $key ) { $function = 'set_' . $key; if ( is_callable( array( $token, $function ) ) ) { $token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) ); } } } /** * Saves extra token data as meta. * * @since 3.0.0 * @param WC_Payment_Token $token Payment token object. * @param bool $force By default, only changed props are updated. When this param is true all props are updated. * @return array List of updated props. */ protected function save_extra_data( &$token, $force = false ) { if ( $this->extra_data_saved ) { return array(); } $updated_props = array(); $extra_data_keys = $token->get_extra_data_keys(); $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array(); $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props ); foreach ( $extra_data_keys as $key ) { if ( ! array_key_exists( $key, $props_to_update ) ) { continue; } $function = 'get_' . $key; if ( is_callable( array( $token, $function ) ) ) { if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) { $updated_props[] = $key; } } } return $updated_props; } /** * Returns an array of objects (stdObject) matching specific token criteria. * Accepts token_id, user_id, gateway_id, and type. * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @since 3.0.0 * @param array $args List of accepted args: token_id, gateway_id, user_id, type. * @return array */ public function get_tokens( $args ) { global $wpdb; $args = wp_parse_args( $args, array( 'token_id' => '', 'user_id' => '', 'gateway_id' => '', 'type' => '', ) ); $sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens"; $where = array( '1=1' ); if ( $args['token_id'] ) { $token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) ); $where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')"; } if ( $args['user_id'] ) { $where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) ); } if ( $args['gateway_id'] ) { $gateway_ids = array( $args['gateway_id'] ); } else { $gateways = WC_Payment_Gateways::instance(); $gateway_ids = $gateways->get_payment_gateway_ids(); } $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1; $posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' ); $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', '; $limits = 'LIMIT ' . $pgstrt . $posts_per_page; $gateway_ids[] = ''; $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; if ( $args['type'] ) { $where[] = $wpdb->prepare( 'type = %s', $args['type'] ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits ); return $token_results; } /** * Returns an stdObject of a token for a user's default token. * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @since 3.0.0 * @param int $user_id User ID. * @return object */ public function get_users_default_token( $user_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", $user_id ) ); } /** * Returns an stdObject of a token. * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @since 3.0.0 * @param int $token_id Token ID. * @return object */ public function get_token_by_id( $token_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", $token_id ) ); } /** * Returns metadata for a specific payment token. * * @since 3.0.0 * @param int $token_id Token ID. * @return array */ public function get_metadata( $token_id ) { return get_metadata( 'payment_token', $token_id ); } /** * Get a token's type by ID. * * @since 3.0.0 * @param int $token_id Token ID. * @return string */ public function get_token_type_by_id( $token_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", $token_id ) ); } /** * Update's a tokens default status in the database. Used for quickly * looping through tokens and setting their statuses instead of creating a bunch * of objects. * * @since 3.0.0 * * @param int $token_id Token ID. * @param bool $status Whether given payment token is the default payment token or not. * * @return void */ public function set_default_status( $token_id, $status = true ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'is_default' => (int) $status ), array( 'token_id' => $token_id, ) ); } } includes/data-stores/class-wc-customer-data-store.php 0000644 00000034643 15132754524 0016747 0 ustar 00 <?php /** * Class WC_Customer_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Customer Data Store. * * @version 3.0.0 */ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface { /** * Data stored in meta keys, but not considered "meta". * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( 'locale', 'billing_postcode', 'billing_city', 'billing_address_1', 'billing_address_2', 'billing_state', 'billing_country', 'shipping_postcode', 'shipping_city', 'shipping_address_1', 'shipping_address_2', 'shipping_state', 'shipping_country', 'paying_customer', 'last_update', 'first_name', 'last_name', 'display_name', 'show_admin_bar_front', 'use_ssl', 'admin_color', 'rich_editing', 'comment_shortcuts', 'dismissed_wp_pointers', 'show_welcome_panel', 'session_tokens', 'nickname', 'description', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_phone', 'wptests_capabilities', 'wptests_user_level', 'syntax_highlighting', '_order_count', '_money_spent', '_last_order', '_woocommerce_tracks_anon_id', ); /** * Internal meta type used to store user data. * * @var string */ protected $meta_type = 'user'; /** * Callback to remove unwanted meta data. * * @param object $meta Meta object. * @return bool */ protected function exclude_internal_meta_keys( $meta ) { global $wpdb; $table_prefix = $wpdb->prefix ? $wpdb->prefix : 'wp_'; return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' ) && 0 !== strpos( $meta->meta_key, 'closedpostboxes_' ) && 0 !== strpos( $meta->meta_key, 'metaboxhidden_' ) && 0 !== strpos( $meta->meta_key, 'manageedit-' ) && ! strstr( $meta->meta_key, $table_prefix ) && 0 !== stripos( $meta->meta_key, 'wp_' ); } /** * Method to create a new customer in the database. * * @since 3.0.0 * * @param WC_Customer $customer Customer object. * * @throws WC_Data_Exception If unable to create new customer. */ public function create( &$customer ) { $id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() ); if ( is_wp_error( $id ) ) { throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() ); } $customer->set_id( $id ); $this->update_user_meta( $customer ); // Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email. $customer->set_password( '' ); wp_update_user( apply_filters( 'woocommerce_update_customer_args', array( 'ID' => $customer->get_id(), 'role' => $customer->get_role(), 'display_name' => $customer->get_display_name(), ), $customer ) ); $wp_user = new WP_User( $customer->get_id() ); $customer->set_date_created( $wp_user->user_registered ); $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); $customer->save_meta_data(); $customer->apply_changes(); do_action( 'woocommerce_new_customer', $customer->get_id(), $customer ); } /** * Method to read a customer object. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @throws Exception If invalid customer. */ public function read( &$customer ) { $user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false; // User object is required. if ( ! $user_object || empty( $user_object->ID ) ) { throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); } $customer_id = $customer->get_id(); // Load meta but exclude deprecated props and parent keys. $user_meta = array_diff_key( array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ), array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ), array_change_key_case( (array) $user_object->data ) ); $customer->set_props( $user_meta ); $customer->set_props( array( 'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ), 'email' => $user_object->user_email, 'username' => $user_object->user_login, 'display_name' => $user_object->display_name, 'date_created' => $user_object->user_registered, // Mysql string in local format. 'date_modified' => get_user_meta( $customer_id, 'last_update', true ), 'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer', ) ); $customer->read_meta_data(); $customer->set_object_read( true ); do_action( 'woocommerce_customer_loaded', $customer ); } /** * Updates a customer in the database. * * @since 3.0.0 * @param WC_Customer $customer Customer object. */ public function update( &$customer ) { wp_update_user( apply_filters( 'woocommerce_update_customer_args', array( 'ID' => $customer->get_id(), 'user_email' => $customer->get_email(), 'display_name' => $customer->get_display_name(), ), $customer ) ); // Only update password if a new one was set with set_password. if ( $customer->get_password() ) { wp_update_user( array( 'ID' => $customer->get_id(), 'user_pass' => $customer->get_password(), ) ); $customer->set_password( '' ); } $this->update_user_meta( $customer ); $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); $customer->save_meta_data(); $customer->apply_changes(); do_action( 'woocommerce_update_customer', $customer->get_id(), $customer ); } /** * Deletes a customer from the database. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$customer, $args = array() ) { if ( ! $customer->get_id() ) { return; } $args = wp_parse_args( $args, array( 'reassign' => 0, ) ); $id = $customer->get_id(); wp_delete_user( $id, $args['reassign'] ); do_action( 'woocommerce_delete_customer', $id ); } /** * Helper method that updates all the meta for a customer. Used for update & create. * * @since 3.0.0 * @param WC_Customer $customer Customer object. */ private function update_user_meta( $customer ) { $updated_props = array(); $changed_props = $customer->get_changes(); $meta_key_to_props = array( 'paying_customer' => 'is_paying_customer', 'first_name' => 'first_name', 'last_name' => 'last_name', ); foreach ( $meta_key_to_props as $meta_key => $prop ) { if ( ! array_key_exists( $prop, $changed_props ) ) { continue; } if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { $updated_props[] = $prop; } } $billing_address_props = array( 'billing_first_name' => 'billing_first_name', 'billing_last_name' => 'billing_last_name', 'billing_company' => 'billing_company', 'billing_address_1' => 'billing_address_1', 'billing_address_2' => 'billing_address_2', 'billing_city' => 'billing_city', 'billing_state' => 'billing_state', 'billing_postcode' => 'billing_postcode', 'billing_country' => 'billing_country', 'billing_email' => 'billing_email', 'billing_phone' => 'billing_phone', ); foreach ( $billing_address_props as $meta_key => $prop ) { $prop_key = substr( $prop, 8 ); if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) { continue; } if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { $updated_props[] = $prop; } } $shipping_address_props = array( 'shipping_first_name' => 'shipping_first_name', 'shipping_last_name' => 'shipping_last_name', 'shipping_company' => 'shipping_company', 'shipping_address_1' => 'shipping_address_1', 'shipping_address_2' => 'shipping_address_2', 'shipping_city' => 'shipping_city', 'shipping_state' => 'shipping_state', 'shipping_postcode' => 'shipping_postcode', 'shipping_country' => 'shipping_country', 'shipping_phone' => 'shipping_phone', ); foreach ( $shipping_address_props as $meta_key => $prop ) { $prop_key = substr( $prop, 9 ); if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) { continue; } if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { $updated_props[] = $prop; } } do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); } /** * Gets the customers last order. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return WC_Order|false */ public function get_last_order( &$customer ) { $last_order = apply_filters( 'woocommerce_customer_get_last_order', get_user_meta( $customer->get_id(), '_last_order', true ), $customer ); if ( '' === $last_order ) { global $wpdb; $last_order = $wpdb->get_var( // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared "SELECT posts.ID FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) ORDER BY posts.ID DESC" // phpcs:enable ); update_user_meta( $customer->get_id(), '_last_order', $last_order ); } if ( ! $last_order ) { return false; } return wc_get_order( absint( $last_order ) ); } /** * Return the number of orders this customer has. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return integer */ public function get_order_count( &$customer ) { $count = apply_filters( 'woocommerce_customer_get_order_count', get_user_meta( $customer->get_id(), '_order_count', true ), $customer ); if ( '' === $count ) { global $wpdb; $count = $wpdb->get_var( // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared "SELECT COUNT(*) FROM $wpdb->posts as posts LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) AND meta_value = '" . esc_sql( $customer->get_id() ) . "'" // phpcs:enable ); update_user_meta( $customer->get_id(), '_order_count', $count ); } return absint( $count ); } /** * Return how much money this customer has spent. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return float */ public function get_total_spent( &$customer ) { $spent = apply_filters( 'woocommerce_customer_get_total_spent', get_user_meta( $customer->get_id(), '_money_spent', true ), $customer ); if ( '' === $spent ) { global $wpdb; $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); $spent = $wpdb->get_var( // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared apply_filters( 'woocommerce_customer_get_total_spent_query', "SELECT SUM(meta2.meta_value) FROM $wpdb->posts as posts LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' AND posts.post_type = 'shop_order' AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) AND meta2.meta_key = '_order_total'", $customer ) // phpcs:enable ); if ( ! $spent ) { $spent = 0; } update_user_meta( $customer->get_id(), '_money_spent', $spent ); } return wc_format_decimal( $spent, 2 ); } /** * Search customers and return customer IDs. * * @param string $term Search term. * @param int|string $limit Limit search results. * @since 3.0.7 * * @return array */ public function search_customers( $term, $limit = '' ) { $results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit ); if ( is_array( $results ) ) { return $results; } $query = new WP_User_Query( apply_filters( 'woocommerce_customer_search_customers', array( 'search' => '*' . esc_attr( $term ) . '*', 'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ), 'fields' => 'ID', 'number' => $limit, ), $term, $limit, 'main_query' ) ); $query2 = new WP_User_Query( apply_filters( 'woocommerce_customer_search_customers', array( 'fields' => 'ID', 'number' => $limit, 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'first_name', 'value' => $term, 'compare' => 'LIKE', ), array( 'key' => 'last_name', 'value' => $term, 'compare' => 'LIKE', ), ), ), $term, $limit, 'meta_query' ) ); $results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) ); if ( $limit && count( $results ) > $limit ) { $results = array_slice( $results, 0, $limit ); } return $results; } /** * Get all user ids who have `billing_email` set to any of the email passed in array. * * @param array $emails List of emails to check against. * * @return array */ public function get_user_ids_for_billing_email( $emails ) { $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); $users_query = new WP_User_Query( array( 'fields' => 'ID', 'meta_query' => array( array( 'key' => 'billing_email', 'value' => $emails, 'compare' => 'IN', ), ), ) ); return array_unique( $users_query->get_results() ); } } includes/data-stores/abstract-wc-order-data-store-cpt.php 0000644 00000034234 15132754524 0017477 0 ustar 00 <?php /** * Abstract_WC_Order_Data_Store_CPT class file. * * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Abstract Order Data Store: Stored in CPT. * * @version 3.0.0 */ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Abstract_Order_Data_Store_Interface { /** * Internal meta type used to store order data. * * @var string */ protected $meta_type = 'post'; /** * Data stored in meta keys, but not considered "meta" for an order. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_order_currency', '_cart_discount', '_cart_discount_tax', '_order_shipping', '_order_shipping_tax', '_order_tax', '_order_total', '_order_version', '_prices_include_tax', '_payment_tokens', ); /* |-------------------------------------------------------------------------- | CRUD Methods |-------------------------------------------------------------------------- */ /** * Method to create a new order in the database. * * @param WC_Order $order Order object. */ public function create( &$order ) { $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() ); if ( ! $order->get_date_created( 'edit' ) ) { $order->set_date_created( time() ); } $id = wp_insert_post( apply_filters( 'woocommerce_new_order_data', array( 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), 'post_type' => $order->get_type( 'edit' ), 'post_status' => $this->get_post_status( $order ), 'ping_status' => 'closed', 'post_author' => 1, 'post_title' => $this->get_post_title(), 'post_password' => $this->get_order_key( $order ), 'post_parent' => $order->get_parent_id( 'edit' ), 'post_excerpt' => $this->get_post_excerpt( $order ), ) ), true ); if ( $id && ! is_wp_error( $id ) ) { $order->set_id( $id ); $this->update_post_meta( $order ); $order->save_meta_data(); $order->apply_changes(); $this->clear_caches( $order ); } } /** * Method to read an order from the database. * * @param WC_Order $order Order object. * * @throws Exception If passed order is invalid. */ public function read( &$order ) { $order->set_defaults(); $post_object = get_post( $order->get_id() ); if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) { throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); } $order->set_props( array( 'parent_id' => $post_object->post_parent, 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), 'status' => $post_object->post_status, ) ); $this->read_order_data( $order, $post_object ); $order->read_meta_data(); $order->set_object_read( true ); /** * In older versions, discounts may have been stored differently. * Update them now so if the object is saved, the correct values are * stored. @todo When meta is flattened, handle this during migration. */ if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) { $order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) ); } } /** * Method to update an order in the database. * * @param WC_Order $order Order object. */ public function update( &$order ) { $order->save_meta_data(); $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); if ( null === $order->get_date_created( 'edit' ) ) { $order->set_date_created( time() ); } $changes = $order->get_changes(); // Only update the post when the post data changes. if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) { $post_data = array( 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), 'post_status' => $this->get_post_status( $order ), 'post_parent' => $order->get_parent_id(), 'post_excerpt' => $this->get_post_excerpt( $order ), 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), ); /** * When updating this object, to prevent infinite loops, use $wpdb * to update data, since wp_update_post spawns more calls to the * save_post action. * * This ensures hooks are fired by either WP itself (admin screen save), * or an update purely from CRUD. */ if ( doing_action( 'save_post' ) ) { $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) ); clean_post_cache( $order->get_id() ); } else { wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) ); } $order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. } $this->update_post_meta( $order ); $order->apply_changes(); $this->clear_caches( $order ); } /** * Method to delete an order from the database. * * @param WC_Order $order Order object. * @param array $args Array of args to pass to the delete method. * * @return void */ public function delete( &$order, $args = array() ) { $id = $order->get_id(); $args = wp_parse_args( $args, array( 'force_delete' => false, ) ); if ( ! $id ) { return; } if ( $args['force_delete'] ) { wp_delete_post( $id ); $order->set_id( 0 ); do_action( 'woocommerce_delete_order', $id ); } else { wp_trash_post( $id ); $order->set_status( 'trash' ); do_action( 'woocommerce_trash_order', $id ); } } /* |-------------------------------------------------------------------------- | Additional Methods |-------------------------------------------------------------------------- */ /** * Get the status to save to the post object. * * Plugins extending the order classes can override this to change the stored status/add prefixes etc. * * @since 3.6.0 * @param WC_order $order Order object. * @return string */ protected function get_post_status( $order ) { $order_status = $order->get_status( 'edit' ); if ( ! $order_status ) { $order_status = apply_filters( 'woocommerce_default_order_status', 'pending' ); } $post_status = $order_status; $valid_statuses = get_post_stati(); // Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed. // @todo In the future this should only happen based on `wc_is_order_status`, but in order to // preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong // Notice will be needed here, followed by future removal. if ( ! in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) { $post_status = 'wc-' . $post_status; } return $post_status; } /** * Excerpt for post. * * @param WC_order $order Order object. * @return string */ protected function get_post_excerpt( $order ) { return ''; } /** * Get a title for the new post type. * * @return string */ protected function get_post_title() { // @codingStandardsIgnoreStart /* translators: %s: Order date */ return sprintf( __( 'Order – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreEnd } /** * Get order key. * * @since 4.3.0 * @param WC_order $order Order object. * @return string */ protected function get_order_key( $order ) { return wc_generate_order_key(); } /** * Read order data. Can be overridden by child classes to load other props. * * @param WC_Order $order Order object. * @param object $post_object Post object. * @since 3.0.0 */ protected function read_order_data( &$order, $post_object ) { $id = $order->get_id(); $order->set_props( array( 'currency' => get_post_meta( $id, '_order_currency', true ), 'discount_total' => get_post_meta( $id, '_cart_discount', true ), 'discount_tax' => get_post_meta( $id, '_cart_discount_tax', true ), 'shipping_total' => get_post_meta( $id, '_order_shipping', true ), 'shipping_tax' => get_post_meta( $id, '_order_shipping_tax', true ), 'cart_tax' => get_post_meta( $id, '_order_tax', true ), 'total' => get_post_meta( $id, '_order_total', true ), 'version' => get_post_meta( $id, '_order_version', true ), 'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === get_post_meta( $id, '_prices_include_tax', true ) : 'yes' === get_option( 'woocommerce_prices_include_tax' ), ) ); // Gets extra data associated with the order if needed. foreach ( $order->get_extra_data_keys() as $key ) { $function = 'set_' . $key; if ( is_callable( array( $order, $function ) ) ) { $order->{$function}( get_post_meta( $order->get_id(), '_' . $key, true ) ); } } } /** * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. * * @param WC_Order $order Order object. * @since 3.0.0 */ protected function update_post_meta( &$order ) { $updated_props = array(); $meta_key_to_props = array( '_order_currency' => 'currency', '_cart_discount' => 'discount_total', '_cart_discount_tax' => 'discount_tax', '_order_shipping' => 'shipping_total', '_order_shipping_tax' => 'shipping_tax', '_order_tax' => 'cart_tax', '_order_total' => 'total', '_order_version' => 'version', '_prices_include_tax' => 'prices_include_tax', ); $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $order->{"get_$prop"}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; if ( 'prices_include_tax' === $prop ) { $value = $value ? 'yes' : 'no'; } $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); if ( $updated ) { $updated_props[] = $prop; } } do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); } /** * Clear any caches. * * @param WC_Order $order Order object. * @since 3.0.0 */ protected function clear_caches( &$order ) { clean_post_cache( $order->get_id() ); wc_delete_shop_order_transients( $order ); wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' ); } /** * Read order items of a specific type from the database for this order. * * @param WC_Order $order Order object. * @param string $type Order item type. * @return array */ public function read_items( $order, $type ) { global $wpdb; // Get from cache if available. $items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false; if ( false === $items ) { $items = $wpdb->get_results( $wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() ) ); foreach ( $items as $item ) { wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); } if ( 0 < $order->get_id() ) { wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' ); } } $items = wp_list_filter( $items, array( 'order_item_type' => $type ) ); if ( ! empty( $items ) ) { $items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) ); } else { $items = array(); } return $items; } /** * Remove all line items (products, coupons, shipping, taxes) from the order. * * @param WC_Order $order Order object. * @param string $type Order item type. Default null. */ public function delete_items( $order, $type = null ) { global $wpdb; if ( ! empty( $type ) ) { $wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order->get_id(), $type ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order->get_id(), $type ) ); } else { $wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order->get_id() ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order->get_id() ) ); } $this->clear_caches( $order ); } /** * Get token ids for an order. * * @param WC_Order $order Order object. * @return array */ public function get_payment_token_ids( $order ) { $token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) ); return $token_ids; } /** * Update token ids for an order. * * @param WC_Order $order Order object. * @param array $token_ids Payment token ids. */ public function update_payment_token_ids( $order, $token_ids ) { update_post_meta( $order->get_id(), '_payment_tokens', $token_ids ); } } includes/data-stores/class-wc-data-store-wp.php 0000644 00000047344 15132754524 0015536 0 ustar 00 <?php /** * Shared logic for WP based data. * Contains functions like meta handling for all default data stores. * Your own data store doesn't need to use WC_Data_Store_WP -- you can write * your own meta handling functions. * * @version 3.0.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * WC_Data_Store_WP class. */ class WC_Data_Store_WP { /** * Meta type. This should match up with * the types available at https://developer.wordpress.org/reference/functions/add_metadata/. * WP defines 'post', 'user', 'comment', and 'term'. * * @var string */ protected $meta_type = 'post'; /** * This only needs set if you are using a custom metadata type (for example payment tokens. * This should be the name of the field your table uses for associating meta with objects. * For example, in payment_tokenmeta, this would be payment_token_id. * * @var string */ protected $object_id_field_for_meta = ''; /** * Data stored in meta keys, but not considered "meta" for an object. * * @since 3.0.0 * * @var array */ protected $internal_meta_keys = array(); /** * Meta data which should exist in the DB, even if empty. * * @since 3.6.0 * * @var array */ protected $must_exist_meta_keys = array(); /** * Get and store terms from a taxonomy. * * @since 3.0.0 * @param WC_Data|integer $object WC_Data object or object ID. * @param string $taxonomy Taxonomy name e.g. product_cat. * @return array of terms */ protected function get_term_ids( $object, $taxonomy ) { if ( is_numeric( $object ) ) { $object_id = $object; } else { $object_id = $object->get_id(); } $terms = get_the_terms( $object_id, $taxonomy ); if ( false === $terms || is_wp_error( $terms ) ) { return array(); } return wp_list_pluck( $terms, 'term_id' ); } /** * Returns an array of meta for an object. * * @since 3.0.0 * @param WC_Data $object WC_Data object. * @return array */ public function read_meta( &$object ) { global $wpdb; $db_info = $this->get_db_info(); $raw_meta_data = $wpdb->get_results( $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value FROM {$db_info['table']} WHERE {$db_info['object_id_field']} = %d ORDER BY {$db_info['meta_id_field']}", // phpcs:enable $object->get_id() ) ); return $this->filter_raw_meta_data( $object, $raw_meta_data ); } /** * Helper method to filter internal meta keys from all meta data rows for the object. * * @since 4.7.0 * * @param WC_Data $object WC_Data object. * @param array $raw_meta_data Array of std object of meta data to be filtered. * * @return mixed|void */ public function filter_raw_meta_data( &$object, $raw_meta_data ) { $this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys ); $meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) ); return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this ); } /** * Deletes meta based on meta ID. * * @since 3.0.0 * @param WC_Data $object WC_Data object. * @param stdClass $meta (containing at least ->id). */ public function delete_meta( &$object, $meta ) { delete_metadata_by_mid( $this->meta_type, $meta->id ); } /** * Add new piece of meta. * * @since 3.0.0 * @param WC_Data $object WC_Data object. * @param stdClass $meta (containing ->key and ->value). * @return int meta ID */ public function add_meta( &$object, $meta ) { return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false ); } /** * Update meta. * * @since 3.0.0 * @param WC_Data $object WC_Data object. * @param stdClass $meta (containing ->id, ->key and ->value). */ public function update_meta( &$object, $meta ) { update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key ); } /** * Table structure is slightly different between meta types, this function will return what we need to know. * * @since 3.0.0 * @return array Array elements: table, object_id_field, meta_id_field */ protected function get_db_info() { global $wpdb; $meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well. $table = $wpdb->prefix; // If we are dealing with a type of metadata that is not a core type, the table should be prefixed. if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ), true ) ) { $table .= 'woocommerce_'; } $table .= $this->meta_type . 'meta'; $object_id_field = $this->meta_type . '_id'; // Figure out our field names. if ( 'user' === $this->meta_type ) { $meta_id_field = 'umeta_id'; $table = $wpdb->usermeta; } if ( ! empty( $this->object_id_field_for_meta ) ) { $object_id_field = $this->object_id_field_for_meta; } return array( 'table' => $table, 'object_id_field' => $object_id_field, 'meta_id_field' => $meta_id_field, ); } /** * Internal meta keys we don't want exposed as part of meta_data. This is in * addition to all data props with _ prefix. * * @since 2.6.0 * * @param string $key Prefix to be added to meta keys. * @return string */ protected function prefix_key( $key ) { return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key; } /** * Callback to remove unwanted meta data. * * @param object $meta Meta object to check if it should be excluded or not. * @return bool */ protected function exclude_internal_meta_keys( $meta ) { return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' ); } /** * Gets a list of props and meta keys that need updated based on change state * or if they are present in the database or not. * * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc). * @param array $meta_key_to_props A mapping of meta keys => prop names. * @param string $meta_type The internal WP meta type (post, user, etc). * @return array A mapping of meta keys => prop names, filtered by ones that should be updated. */ protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) { $props_to_update = array(); $changed_props = $object->get_changes(); // Props should be updated if they are a part of the $changed array or don't exist yet. foreach ( $meta_key_to_props as $meta_key => $prop ) { if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) { $props_to_update[ $meta_key ] = $prop; } } return $props_to_update; } /** * Update meta data in, or delete it from, the database. * * Avoids storing meta when it's either an empty string or empty array. * Other empty values such as numeric 0 and null should still be stored. * Data-stores can force meta to exist using `must_exist_meta_keys`. * * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist. * * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc). * @param string $meta_key Meta key to update. * @param mixed $meta_value Value to save. * * @since 3.6.0 Added to prevent empty meta being stored unless required. * * @return bool True if updated/deleted. */ protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) { if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) { $updated = delete_post_meta( $object->get_id(), $meta_key ); } else { $updated = update_post_meta( $object->get_id(), $meta_key, $meta_value ); } return (bool) $updated; } /** * Get valid WP_Query args from a WC_Object_Query's query variables. * * @since 3.1.0 * @param array $query_vars query vars from a WC_Object_Query. * @return array */ protected function get_wp_query_args( $query_vars ) { $skipped_values = array( '', array(), null ); $wp_query_args = array( 'errors' => array(), 'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query ); foreach ( $query_vars as $key => $value ) { if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) { continue; } // Build meta queries out of vars that are stored in internal meta keys. if ( in_array( '_' . $key, $this->internal_meta_keys, true ) ) { // Check for existing values if wildcard is used. if ( '*' === $value ) { $wp_query_args['meta_query'][] = array( array( 'key' => '_' . $key, 'compare' => 'EXISTS', ), array( 'key' => '_' . $key, 'value' => '', 'compare' => '!=', ), ); } else { $wp_query_args['meta_query'][] = array( 'key' => '_' . $key, 'value' => $value, 'compare' => is_array( $value ) ? 'IN' : '=', ); } } else { // Other vars get mapped to wp_query args or just left alone. $key_mapping = array( 'parent' => 'post_parent', 'parent_exclude' => 'post_parent__not_in', 'exclude' => 'post__not_in', 'limit' => 'posts_per_page', 'type' => 'post_type', 'return' => 'fields', ); if ( isset( $key_mapping[ $key ] ) ) { $wp_query_args[ $key_mapping[ $key ] ] = $value; } else { $wp_query_args[ $key ] = $value; } } } return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars ); } /** * Map a valid date query var to WP_Query arguments. * Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators. * Also accepts a WC_DateTime object. * * @since 3.2.0 * @param mixed $query_var A valid date format. * @param string $key meta or db column key. * @param array $wp_query_args WP_Query args. * @return array Modified $wp_query_args */ public function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) { $query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/'; $valid_operators = array( '>', '>=', '=', '<=', '<', '...' ); // YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision. $precision = 'second'; $dates = array(); $operator = '='; try { // Specific time query with a WC_DateTime. if ( is_a( $query_var, 'WC_DateTime' ) ) { $dates[] = $query_var; } elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp. $dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) ); } elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates. if ( ! empty( $sections[1] ) ) { $dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] ); } $operator = in_array( $sections[2], $valid_operators, true ) ? $sections[2] : ''; $dates[] = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] ); if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) { $precision = 'day'; } } else { // Specific time query with a string. $dates[] = wc_string_to_datetime( $query_var ); $precision = 'day'; } } catch ( Exception $e ) { return $wp_query_args; } // Check for valid inputs. if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) { return $wp_query_args; } // Build date query for 'post_date' or 'post_modified' keys. if ( 'post_date' === $key || 'post_modified' === $key ) { if ( ! isset( $wp_query_args['date_query'] ) ) { $wp_query_args['date_query'] = array(); } $query_arg = array( 'column' => 'day' === $precision ? $key : $key . '_gmt', 'inclusive' => '>' !== $operator && '<' !== $operator, ); // Add 'before'/'after' query args. $comparisons = array(); if ( '>' === $operator || '>=' === $operator || '...' === $operator ) { $comparisons[] = 'after'; } if ( '<' === $operator || '<=' === $operator || '...' === $operator ) { $comparisons[] = 'before'; } foreach ( $comparisons as $index => $comparison ) { if ( 'day' === $precision ) { /** * WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and * 'after' string query, so we have to use the array format in 'day' precision. * * @see https://core.trac.wordpress.org/ticket/29908 */ $query_arg[ $comparison ]['year'] = $dates[ $index ]->date( 'Y' ); $query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' ); $query_arg[ $comparison ]['day'] = $dates[ $index ]->date( 'j' ); } else { /** * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries, * so we have to use a string query. */ $query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() ); } } if ( empty( $comparisons ) ) { $query_arg['year'] = $dates[0]->date( 'Y' ); $query_arg['month'] = $dates[0]->date( 'n' ); $query_arg['day'] = $dates[0]->date( 'j' ); if ( 'second' === $precision ) { $query_arg['hour'] = $dates[0]->date( 'H' ); $query_arg['minute'] = $dates[0]->date( 'i' ); $query_arg['second'] = $dates[0]->date( 's' ); } } $wp_query_args['date_query'][] = $query_arg; return $wp_query_args; } // Build meta query for unrecognized keys. if ( ! isset( $wp_query_args['meta_query'] ) ) { $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } // Meta dates are stored as timestamps in the db. // Check against beginning/end-of-day timestamps when using 'day' precision. if ( 'day' === $precision ) { $start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) ); $end_timestamp = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) ); switch ( $operator ) { case '>': case '<=': $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $end_timestamp, 'compare' => $operator, ); break; case '<': case '>=': $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $start_timestamp, 'compare' => $operator, ); break; default: $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $start_timestamp, 'compare' => '>=', ); $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $end_timestamp, 'compare' => '<=', ); } } else { if ( '...' !== $operator ) { $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $dates[0]->getTimestamp(), 'compare' => $operator, ); } else { $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $dates[0]->getTimestamp(), 'compare' => '>=', ); $wp_query_args['meta_query'][] = array( 'key' => $key, 'value' => $dates[1]->getTimestamp(), 'compare' => '<=', ); } } return $wp_query_args; } /** * Return list of internal meta keys. * * @since 3.2.0 * @return array */ public function get_internal_meta_keys() { return $this->internal_meta_keys; } /** * Check if the terms are suitable for searching. * * Uses an array of stopwords (terms) that are excluded from the separate * term matching when searching for posts. The list of English stopwords is * the approximate search engines list, and is translatable. * * @since 3.4.0 * @param array $terms Terms to check. * @return array Terms that are not stopwords. */ protected function get_valid_search_terms( $terms ) { $valid_terms = array(); $stopwords = $this->get_search_stopwords(); foreach ( $terms as $term ) { // keep before/after spaces when term is for exact match, otherwise trim quotes and spaces. if ( preg_match( '/^".+"$/', $term ) ) { $term = trim( $term, "\"'" ); } else { $term = trim( $term, "\"' " ); } // Avoid single A-Z and single dashes. if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) { continue; } if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) { continue; } $valid_terms[] = $term; } return $valid_terms; } /** * Retrieve stopwords used when parsing search terms. * * @since 3.4.0 * @return array Stopwords. */ protected function get_search_stopwords() { // Translators: This is a comma-separated list of very common words that should be excluded from a search, like a, an, and the. These are usually called "stopwords". You should not simply translate these individual words into your language. Instead, look for and provide commonly accepted stopwords in your language. $stopwords = array_map( 'wc_strtolower', array_map( 'trim', explode( ',', _x( 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www', 'Comma-separated list of search stopwords in your language', 'woocommerce' ) ) ) ); return apply_filters( 'wp_search_stopwords', $stopwords ); } /** * Get data to save to a lookup table. * * @since 3.6.0 * @param int $id ID of object to update. * @param string $table Lookup table name. * @return array */ protected function get_data_for_lookup_table( $id, $table ) { return array(); } /** * Get primary key name for lookup table. * * @since 3.6.0 * @param string $table Lookup table name. * @return string */ protected function get_primary_key_for_lookup_table( $table ) { return ''; } /** * Update a lookup table for an object. * * @since 3.6.0 * @param int $id ID of object to update. * @param string $table Lookup table name. * * @return NULL */ protected function update_lookup_table( $id, $table ) { global $wpdb; $id = absint( $id ); $table = sanitize_key( $table ); if ( empty( $id ) || empty( $table ) ) { return false; } $existing_data = wp_cache_get( 'lookup_table', 'object_' . $id ); $update_data = $this->get_data_for_lookup_table( $id, $table ); if ( ! empty( $update_data ) && $update_data !== $existing_data ) { $wpdb->replace( $wpdb->$table, $update_data ); wp_cache_set( 'lookup_table', $update_data, 'object_' . $id ); } } /** * Delete lookup table data for an ID. * * @since 3.6.0 * @param int $id ID of object to update. * @param string $table Lookup table name. */ public function delete_from_lookup_table( $id, $table ) { global $wpdb; $id = absint( $id ); $table = sanitize_key( $table ); if ( empty( $id ) || empty( $table ) ) { return false; } $pk = $this->get_primary_key_for_lookup_table( $table ); $wpdb->delete( $wpdb->$table, array( $pk => $id, ) ); wp_cache_delete( 'lookup_table', 'object_' . $id ); } /** * Converts a WP post date string into a timestamp. * * @since 4.8.0 * * @param string $time_string The WP post date string. * @return int|null The date string converted to a timestamp or null. */ protected function string_to_timestamp( $time_string ) { return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null; } } includes/data-stores/class-wc-customer-data-store-session.php 0000644 00000012336 15132754524 0020423 0 ustar 00 <?php /** * Class WC_Customer_Data_Store_Session file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Customer Data Store which stores the data in session. * * @version 3.0.0 */ class WC_Customer_Data_Store_Session extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface { /** * Keys which are also stored in a session (so we can make sure they get updated...) * * @var array */ protected $session_keys = array( 'id', 'date_modified', 'billing_postcode', 'billing_city', 'billing_address_1', 'billing_address', 'billing_address_2', 'billing_state', 'billing_country', 'shipping_postcode', 'shipping_city', 'shipping_address_1', 'shipping_address', 'shipping_address_2', 'shipping_state', 'shipping_country', 'is_vat_exempt', 'calculated_shipping', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_phone', ); /** * Simply update the session. * * @param WC_Customer $customer Customer object. */ public function create( &$customer ) { $this->save_to_session( $customer ); } /** * Simply update the session. * * @param WC_Customer $customer Customer object. */ public function update( &$customer ) { $this->save_to_session( $customer ); } /** * Saves all customer data to the session. * * @param WC_Customer $customer Customer object. */ public function save_to_session( $customer ) { $data = array(); foreach ( $this->session_keys as $session_key ) { $function_key = $session_key; if ( 'billing_' === substr( $session_key, 0, 8 ) ) { $session_key = str_replace( 'billing_', '', $session_key ); } $data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' ); } WC()->session->set( 'customer', $data ); } /** * Read customer data from the session unless the user has logged in, in * which case the stored ID will differ from the actual ID. * * @since 3.0.0 * @param WC_Customer $customer Customer object. */ public function read( &$customer ) { $data = (array) WC()->session->get( 'customer' ); /** * There is a valid session if $data is not empty, and the ID matches the logged in user ID. * * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded. */ if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) { foreach ( $this->session_keys as $session_key ) { if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) { continue; } $function_key = $session_key; if ( 'billing_' === substr( $session_key, 0, 8 ) ) { $session_key = str_replace( 'billing_', '', $session_key ); } if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) { $customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) ); } } } $this->set_defaults( $customer ); $customer->set_object_read( true ); } /** * Load default values if props are unset. * * @param WC_Customer $customer Customer object. */ protected function set_defaults( &$customer ) { try { $default = wc_get_customer_default_location(); $has_shipping_address = $customer->has_shipping_address(); if ( ! $customer->get_billing_country() ) { $customer->set_billing_country( $default['country'] ); } if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) { $customer->set_shipping_country( $customer->get_billing_country() ); } if ( ! $customer->get_billing_state() ) { $customer->set_billing_state( $default['state'] ); } if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) { $customer->set_shipping_state( $customer->get_billing_state() ); } if ( ! $customer->get_billing_email() && is_user_logged_in() ) { $current_user = wp_get_current_user(); $customer->set_billing_email( $current_user->user_email ); } } catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch } } /** * Deletes a customer from the database. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$customer, $args = array() ) { WC()->session->set( 'customer', null ); } /** * Gets the customers last order. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return WC_Order|false */ public function get_last_order( &$customer ) { return false; } /** * Return the number of orders this customer has. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return integer */ public function get_order_count( &$customer ) { return 0; } /** * Return how much money this customer has spent. * * @since 3.0.0 * @param WC_Customer $customer Customer object. * @return float */ public function get_total_spent( &$customer ) { return 0; } } includes/data-stores/class-wc-order-refund-data-store-cpt.php 0000644 00000007006 15132754524 0020257 0 ustar 00 <?php /** * Class WC_Order_Refund_Data_Store_CPT file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Refund Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Refund_Data_Store_Interface { /** * Data stored in meta keys, but not considered "meta" for an order. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_order_currency', '_cart_discount', '_refund_amount', '_refunded_by', '_refunded_payment', '_refund_reason', '_cart_discount_tax', '_order_shipping', '_order_shipping_tax', '_order_tax', '_order_total', '_order_version', '_prices_include_tax', '_payment_tokens', ); /** * Delete a refund - no trash is supported. * * @param WC_Order $order Order object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$order, $args = array() ) { $id = $order->get_id(); $parent_order_id = $order->get_parent_id(); $refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id; if ( ! $id ) { return; } wp_delete_post( $id ); wp_cache_delete( $refund_cache_key, 'orders' ); $order->set_id( 0 ); do_action( 'woocommerce_delete_order_refund', $id ); } /** * Read refund data. Can be overridden by child classes to load other props. * * @param WC_Order_Refund $refund Refund object. * @param object $post_object Post object. * @since 3.0.0 */ protected function read_order_data( &$refund, $post_object ) { parent::read_order_data( $refund, $post_object ); $id = $refund->get_id(); $refund->set_props( array( 'amount' => get_post_meta( $id, '_refund_amount', true ), 'refunded_by' => metadata_exists( 'post', $id, '_refunded_by' ) ? get_post_meta( $id, '_refunded_by', true ) : absint( $post_object->post_author ), 'refunded_payment' => wc_string_to_bool( get_post_meta( $id, '_refunded_payment', true ) ), 'reason' => metadata_exists( 'post', $id, '_refund_reason' ) ? get_post_meta( $id, '_refund_reason', true ) : $post_object->post_excerpt, ) ); } /** * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. * * @param WC_Order_Refund $refund Refund object. * @since 3.0.0 */ protected function update_post_meta( &$refund ) { parent::update_post_meta( $refund ); $updated_props = array(); $meta_key_to_props = array( '_refund_amount' => 'amount', '_refunded_by' => 'refunded_by', '_refunded_payment' => 'refunded_payment', '_refund_reason' => 'reason', ); $props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $refund->{"get_$prop"}( 'edit' ); update_post_meta( $refund->get_id(), $meta_key, $value ); $updated_props[] = $prop; } do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props ); } /** * Get a title for the new post type. * * @return string */ protected function get_post_title() { return sprintf( /* translators: %s: Order date */ __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText ); } } includes/data-stores/class-wc-product-grouped-data-store-cpt.php 0000644 00000005422 15132754524 0021006 0 ustar 00 <?php /** * Class WC_Product_Grouped_Data_Store_CPT file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Grouped Product Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface { /** * Helper method that updates all the post meta for a grouped product. * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. * @since 3.0.0 */ protected function update_post_meta( &$product, $force = false ) { $meta_key_to_props = array( '_children' => 'children', ); $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $product->{"get_$prop"}( 'edit' ); $updated = update_post_meta( $product->get_id(), $meta_key, $value ); if ( $updated ) { $this->updated_props[] = $prop; } } parent::update_post_meta( $product, $force ); } /** * Handle updated meta props after updating meta data. * * @since 3.0.0 * @param WC_Product $product Product object. */ protected function handle_updated_props( &$product ) { if ( in_array( 'children', $this->updated_props, true ) ) { $this->update_prices_from_children( $product ); } parent::handle_updated_props( $product ); } /** * Sync grouped product prices with children. * * @since 3.0.0 * @param WC_Product|int $product Product object or product ID. */ public function sync_price( &$product ) { $this->update_prices_from_children( $product ); } /** * Loop over child products and update the grouped product prices. * * @param WC_Product $product Product object. */ protected function update_prices_from_children( &$product ) { $child_prices = array(); foreach ( $product->get_children( 'edit' ) as $child_id ) { $child = wc_get_product( $child_id ); if ( $child ) { $child_prices[] = $child->get_price( 'edit' ); } } $child_prices = array_filter( $child_prices ); delete_post_meta( $product->get_id(), '_price' ); delete_post_meta( $product->get_id(), '_sale_price' ); delete_post_meta( $product->get_id(), '_regular_price' ); if ( ! empty( $child_prices ) ) { add_post_meta( $product->get_id(), '_price', min( $child_prices ) ); add_post_meta( $product->get_id(), '_price', max( $child_prices ) ); } $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); /** * Fire an action for this direct update so it can be detected by other code. * * @since 3.6 * @param int $product_id Product ID that was updated directly. */ do_action( 'woocommerce_updated_product_price', $product->get_id() ); } } includes/data-stores/class-wc-product-data-store-cpt.php 0000644 00000213400 15132754524 0017340 0 ustar 00 <?php /** * WC_Product_Data_Store_CPT class file. * * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Product Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface { /** * Data stored in meta keys, but not considered "meta". * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( '_visibility', '_sku', '_price', '_regular_price', '_sale_price', '_sale_price_dates_from', '_sale_price_dates_to', 'total_sales', '_tax_status', '_tax_class', '_manage_stock', '_stock', '_stock_status', '_backorders', '_low_stock_amount', '_sold_individually', '_weight', '_length', '_width', '_height', '_upsell_ids', '_crosssell_ids', '_purchase_note', '_default_attributes', '_product_attributes', '_virtual', '_downloadable', '_download_limit', '_download_expiry', '_featured', '_downloadable_files', '_wc_rating_count', '_wc_average_rating', '_wc_review_count', '_variation_description', '_thumbnail_id', '_file_paths', '_product_image_gallery', '_product_version', '_wp_old_slug', '_edit_last', '_edit_lock', ); /** * Meta data which should exist in the DB, even if empty. * * @since 3.6.0 * * @var array */ protected $must_exist_meta_keys = array( '_tax_class', ); /** * If we have already saved our extra data, don't do automatic / default handling. * * @var bool */ protected $extra_data_saved = false; /** * Stores updated props. * * @var array */ protected $updated_props = array(); /* |-------------------------------------------------------------------------- | CRUD Methods |-------------------------------------------------------------------------- */ /** * Method to create a new product in the database. * * @param WC_Product $product Product object. */ public function create( &$product ) { if ( ! $product->get_date_created( 'edit' ) ) { $product->set_date_created( time() ); } $id = wp_insert_post( apply_filters( 'woocommerce_new_product_data', array( 'post_type' => 'product', 'post_status' => $product->get_status() ? $product->get_status() : 'publish', 'post_author' => get_current_user_id(), 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), 'post_content' => $product->get_description(), 'post_excerpt' => $product->get_short_description(), 'post_parent' => $product->get_parent_id(), 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', 'ping_status' => 'closed', 'menu_order' => $product->get_menu_order(), 'post_password' => $product->get_post_password( 'edit' ), 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), 'post_name' => $product->get_slug( 'edit' ), ) ), true ); if ( $id && ! is_wp_error( $id ) ) { $product->set_id( $id ); $this->update_post_meta( $product, true ); $this->update_terms( $product, true ); $this->update_visibility( $product, true ); $this->update_attributes( $product, true ); $this->update_version_and_type( $product ); $this->handle_updated_props( $product ); $this->clear_caches( $product ); $product->save_meta_data(); $product->apply_changes(); do_action( 'woocommerce_new_product', $id, $product ); } } /** * Method to read a product from the database. * * @param WC_Product $product Product object. * @throws Exception If invalid product. */ public function read( &$product ) { $product->set_defaults(); $post_object = get_post( $product->get_id() ); if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) { throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); } $product->set_props( array( 'name' => $post_object->post_title, 'slug' => $post_object->post_name, 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), 'status' => $post_object->post_status, 'description' => $post_object->post_content, 'short_description' => $post_object->post_excerpt, 'parent_id' => $post_object->post_parent, 'menu_order' => $post_object->menu_order, 'post_password' => $post_object->post_password, 'reviews_allowed' => 'open' === $post_object->comment_status, ) ); $this->read_attributes( $product ); $this->read_downloads( $product ); $this->read_visibility( $product ); $this->read_product_data( $product ); $this->read_extra_data( $product ); $product->set_object_read( true ); do_action( 'woocommerce_product_read', $product->get_id() ); } /** * Method to update a product in the database. * * @param WC_Product $product Product object. */ public function update( &$product ) { $product->save_meta_data(); $changes = $product->get_changes(); // Only update the post when the post data changes. if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { $post_data = array( 'post_content' => $product->get_description( 'edit' ), 'post_excerpt' => $product->get_short_description( 'edit' ), 'post_title' => $product->get_name( 'edit' ), 'post_parent' => $product->get_parent_id( 'edit' ), 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', 'menu_order' => $product->get_menu_order( 'edit' ), 'post_password' => $product->get_post_password( 'edit' ), 'post_name' => $product->get_slug( 'edit' ), 'post_type' => 'product', ); if ( $product->get_date_created( 'edit' ) ) { $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); } if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); } else { $post_data['post_modified'] = current_time( 'mysql' ); $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); } /** * When updating this object, to prevent infinite loops, use $wpdb * to update data, since wp_update_post spawns more calls to the * save_post action. * * This ensures hooks are fired by either WP itself (admin screen save), * or an update purely from CRUD. */ if ( doing_action( 'save_post' ) ) { $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); clean_post_cache( $product->get_id() ); } else { wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); } $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. } else { // Only update post modified time to record this save event. $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, array( 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', 1 ), ), array( 'ID' => $product->get_id(), ) ); clean_post_cache( $product->get_id() ); } $this->update_post_meta( $product ); $this->update_terms( $product ); $this->update_visibility( $product ); $this->update_attributes( $product ); $this->update_version_and_type( $product ); $this->handle_updated_props( $product ); $this->clear_caches( $product ); wc_get_container() ->get( DownloadPermissionsAdjuster::class ) ->maybe_schedule_adjust_download_permissions( $product ); $product->apply_changes(); do_action( 'woocommerce_update_product', $product->get_id(), $product ); } /** * Method to delete a product from the database. * * @param WC_Product $product Product object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$product, $args = array() ) { $id = $product->get_id(); $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; $args = wp_parse_args( $args, array( 'force_delete' => false, ) ); if ( ! $id ) { return; } if ( $args['force_delete'] ) { do_action( 'woocommerce_before_delete_' . $post_type, $id ); wp_delete_post( $id ); $product->set_id( 0 ); do_action( 'woocommerce_delete_' . $post_type, $id ); } else { wp_trash_post( $id ); $product->set_status( 'trash' ); do_action( 'woocommerce_trash_' . $post_type, $id ); } } /* |-------------------------------------------------------------------------- | Additional Methods |-------------------------------------------------------------------------- */ /** * Read product data. Can be overridden by child classes to load other props. * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function read_product_data( &$product ) { $id = $product->get_id(); $post_meta_values = get_post_meta( $id ); $meta_key_to_props = array( '_sku' => 'sku', '_regular_price' => 'regular_price', '_sale_price' => 'sale_price', '_price' => 'price', '_sale_price_dates_from' => 'date_on_sale_from', '_sale_price_dates_to' => 'date_on_sale_to', 'total_sales' => 'total_sales', '_tax_status' => 'tax_status', '_tax_class' => 'tax_class', '_manage_stock' => 'manage_stock', '_backorders' => 'backorders', '_low_stock_amount' => 'low_stock_amount', '_sold_individually' => 'sold_individually', '_weight' => 'weight', '_length' => 'length', '_width' => 'width', '_height' => 'height', '_upsell_ids' => 'upsell_ids', '_crosssell_ids' => 'cross_sell_ids', '_purchase_note' => 'purchase_note', '_default_attributes' => 'default_attributes', '_virtual' => 'virtual', '_downloadable' => 'downloadable', '_download_limit' => 'download_limit', '_download_expiry' => 'download_expiry', '_thumbnail_id' => 'image_id', '_stock' => 'stock_quantity', '_stock_status' => 'stock_status', '_wc_average_rating' => 'average_rating', '_wc_rating_count' => 'rating_counts', '_wc_review_count' => 'review_count', '_product_image_gallery' => 'gallery_image_ids', ); $set_props = array(); foreach ( $meta_key_to_props as $meta_key => $prop ) { $meta_value = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null; $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values. } $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); $product->set_props( $set_props ); } /** * Re-reads stock from the DB ignoring changes. * * @param WC_Product $product Product object. * @param int|float $new_stock New stock level if already read. */ public function read_stock_quantity( &$product, $new_stock = null ) { $object_read = $product->get_object_read(); $product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db). $product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock ); $product->set_object_read( $object_read ); } /** * Read extra data associated with the product, like button text or product URL for external products. * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function read_extra_data( &$product ) { foreach ( $product->get_extra_data_keys() as $key ) { $function = 'set_' . $key; if ( is_callable( array( $product, $function ) ) ) { $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); } } } /** * Convert visibility terms to props. * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function read_visibility( &$product ) { $terms = get_the_terms( $product->get_id(), 'product_visibility' ); $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); $featured = in_array( 'featured', $term_names, true ); $exclude_search = in_array( 'exclude-from-search', $term_names, true ); $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); if ( $exclude_search && $exclude_catalog ) { $catalog_visibility = 'hidden'; } elseif ( $exclude_search ) { $catalog_visibility = 'catalog'; } elseif ( $exclude_catalog ) { $catalog_visibility = 'search'; } else { $catalog_visibility = 'visible'; } $product->set_props( array( 'featured' => $featured, 'catalog_visibility' => $catalog_visibility, ) ); } /** * Read attributes from post meta. * * @param WC_Product $product Product object. */ protected function read_attributes( &$product ) { $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { $attributes = array(); foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { $meta_value = array_merge( array( 'name' => '', 'value' => '', 'position' => 0, 'is_visible' => 0, 'is_variation' => 0, 'is_taxonomy' => 0, ), (array) $meta_attribute_value ); // Check if is a taxonomy attribute. if ( ! empty( $meta_value['is_taxonomy'] ) ) { if ( ! taxonomy_exists( $meta_value['name'] ) ) { continue; } $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); } else { $id = 0; $options = wc_get_text_attributes( $meta_value['value'] ); } $attribute = new WC_Product_Attribute(); $attribute->set_id( $id ); $attribute->set_name( $meta_value['name'] ); $attribute->set_options( $options ); $attribute->set_position( $meta_value['position'] ); $attribute->set_visible( $meta_value['is_visible'] ); $attribute->set_variation( $meta_value['is_variation'] ); $attributes[] = $attribute; } $product->set_attributes( $attributes ); } } /** * Read downloads from post meta. * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function read_downloads( &$product ) { $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); if ( $meta_values ) { $downloads = array(); foreach ( $meta_values as $key => $value ) { if ( ! isset( $value['name'], $value['file'] ) ) { continue; } $download = new WC_Product_Download(); $download->set_id( $key ); $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); $downloads[] = $download; } $product->set_downloads( $downloads ); } } /** * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. * @since 3.0.0 */ protected function update_post_meta( &$product, $force = false ) { $meta_key_to_props = array( '_sku' => 'sku', '_regular_price' => 'regular_price', '_sale_price' => 'sale_price', '_sale_price_dates_from' => 'date_on_sale_from', '_sale_price_dates_to' => 'date_on_sale_to', 'total_sales' => 'total_sales', '_tax_status' => 'tax_status', '_tax_class' => 'tax_class', '_manage_stock' => 'manage_stock', '_backorders' => 'backorders', '_low_stock_amount' => 'low_stock_amount', '_sold_individually' => 'sold_individually', '_weight' => 'weight', '_length' => 'length', '_width' => 'width', '_height' => 'height', '_upsell_ids' => 'upsell_ids', '_crosssell_ids' => 'cross_sell_ids', '_purchase_note' => 'purchase_note', '_default_attributes' => 'default_attributes', '_virtual' => 'virtual', '_downloadable' => 'downloadable', '_product_image_gallery' => 'gallery_image_ids', '_download_limit' => 'download_limit', '_download_expiry' => 'download_expiry', '_thumbnail_id' => 'image_id', '_stock' => 'stock_quantity', '_stock_status' => 'stock_status', '_wc_average_rating' => 'average_rating', '_wc_rating_count' => 'rating_counts', '_wc_review_count' => 'review_count', ); // Make sure to take extra data (like product url or text for external products) into account. $extra_data_keys = $product->get_extra_data_keys(); foreach ( $extra_data_keys as $key ) { $meta_key_to_props[ '_' . $key ] = $key; } $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $product->{"get_$prop"}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; switch ( $prop ) { case 'virtual': case 'downloadable': case 'manage_stock': case 'sold_individually': $value = wc_bool_to_string( $value ); break; case 'gallery_image_ids': $value = implode( ',', $value ); break; case 'date_on_sale_from': case 'date_on_sale_to': $value = $value ? $value->getTimestamp() : ''; break; case 'stock_quantity': // Fire actions to let 3rd parties know the stock is about to be changed. if ( $product->is_type( 'variation' ) ) { /** * Action to signal that the value of 'stock_quantity' for a variation is about to change. * * @since 4.9 * * @param int $product The variation whose stock is about to change. */ do_action( 'woocommerce_variation_before_set_stock', $product ); } else { /** * Action to signal that the value of 'stock_quantity' for a product is about to change. * * @since 4.9 * * @param int $product The product whose stock is about to change. */ do_action( 'woocommerce_product_before_set_stock', $product ); } break; } $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); if ( $updated ) { $this->updated_props[] = $prop; } } // Update extra data associated with the product like button text or product URL for external products. if ( ! $this->extra_data_saved ) { foreach ( $extra_data_keys as $key ) { $meta_key = '_' . $key; $function = 'get_' . $key; if ( ! array_key_exists( $meta_key, $props_to_update ) ) { continue; } if ( is_callable( array( $product, $function ) ) ) { $value = $product->{$function}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); if ( $updated ) { $this->updated_props[] = $key; } } } } if ( $this->update_downloads( $product, $force ) ) { $this->updated_props[] = 'downloads'; } } /** * Handle updated meta props after updating meta data. * * @since 3.0.0 * @param WC_Product $product Product Object. */ protected function handle_updated_props( &$product ) { $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) ); if ( ! $price_is_synced ) { if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) { if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) { update_post_meta( $product->get_id(), '_sale_price', '' ); $product->set_sale_price( '' ); } } if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) { if ( $product->is_on_sale( 'edit' ) ) { update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); $product->set_price( $product->get_sale_price( 'edit' ) ); } else { update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); $product->set_price( $product->get_regular_price( 'edit' ) ); } } } if ( in_array( 'stock_quantity', $this->updated_props, true ) ) { if ( $product->is_type( 'variation' ) ) { do_action( 'woocommerce_variation_set_stock', $product ); } else { do_action( 'woocommerce_product_set_stock', $product ); } } if ( in_array( 'stock_status', $this->updated_props, true ) ) { if ( $product->is_type( 'variation' ) ) { do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); } else { do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); } } if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) { $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); } // Trigger action so 3rd parties can deal with updated props. do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); // After handling, we can reset the props array. $this->updated_props = array(); } /** * For all stored terms in all taxonomies, save them to the DB. * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. * @since 3.0.0 */ protected function update_terms( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_key_exists( 'category_ids', $changes ) ) { $categories = $product->get_category_ids( 'edit' ); if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) { $categories = array( get_option( 'default_product_cat', 0 ) ); } wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false ); } if ( $force || array_key_exists( 'tag_ids', $changes ) ) { wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); } if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); } _wc_recount_terms_by_product( $product->get_id() ); } /** * Update visibility terms based on props. * * @since 3.0.0 * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. */ protected function update_visibility( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { $terms = array(); if ( $product->get_featured() ) { $terms[] = 'featured'; } if ( 'outofstock' === $product->get_stock_status() ) { $terms[] = 'outofstock'; } $rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) ); if ( $rating > 0 ) { $terms[] = 'rated-' . $rating; } switch ( $product->get_catalog_visibility() ) { case 'hidden': $terms[] = 'exclude-from-search'; $terms[] = 'exclude-from-catalog'; break; case 'catalog': $terms[] = 'exclude-from-search'; break; case 'search': $terms[] = 'exclude-from-catalog'; break; } if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); } } } /** * Update attributes which are a mix of terms and meta data. * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. * @since 3.0.0 */ protected function update_attributes( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_key_exists( 'attributes', $changes ) ) { $attributes = $product->get_attributes(); $meta_values = array(); if ( $attributes ) { foreach ( $attributes as $attribute_key => $attribute ) { $value = ''; if ( is_null( $attribute ) ) { if ( taxonomy_exists( $attribute_key ) ) { // Handle attributes that have been unset. wp_set_object_terms( $product->get_id(), array(), $attribute_key ); } elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) { // Handle attributes that have been unset. wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) ); } continue; } elseif ( $attribute->is_taxonomy() ) { wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); } else { $value = wc_implode_text_attributes( $attribute->get_options() ); } // Store in format WC uses in meta. $meta_values[ $attribute_key ] = array( 'name' => $attribute->get_name(), 'value' => $value, 'position' => $attribute->get_position(), 'is_visible' => $attribute->get_visible() ? 1 : 0, 'is_variation' => $attribute->get_variation() ? 1 : 0, 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, ); } } // Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround. $this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) ); } } /** * Update downloads. * * @since 3.0.0 * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. * @return bool If updated or not. */ protected function update_downloads( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_key_exists( 'downloads', $changes ) ) { $downloads = $product->get_downloads(); $meta_values = array(); if ( $downloads ) { foreach ( $downloads as $key => $download ) { // Store in format WC uses in meta. $meta_values[ $key ] = $download->get_data(); } } if ( $product->is_type( 'variation' ) ) { do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); } else { do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); } return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) ); } return false; } /** * Make sure we store the product type and version (to track data changes). * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function update_version_and_type( &$product ) { $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); $new_type = $product->get_type(); wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); // Action for the transition. if ( $old_type !== $new_type ) { $this->updated_props[] = 'product_type'; do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); } } /** * Clear any caches. * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function clear_caches( &$product ) { wc_delete_product_transients( $product->get_id() ); if ( $product->get_parent_id( 'edit' ) ) { wc_delete_product_transients( $product->get_parent_id( 'edit' ) ); WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) ); } WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) ); WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() ); } /* |-------------------------------------------------------------------------- | wc-product-functions.php methods |-------------------------------------------------------------------------- */ /** * Returns an array of on sale products, as an array of objects with an * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. * * @return array * @since 3.0.0 */ public function get_on_sale_products() { global $wpdb; $exclude_term_ids = array(); $outofstock_join = ''; $outofstock_where = ''; $non_published_where = ''; $product_visibility_term_ids = wc_get_product_visibility_term_ids(); if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; } if ( count( $exclude_term_ids ) ) { $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id'; $outofstock_where = ' AND exclude_join.object_id IS NULL'; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_results( " SELECT posts.ID as id, posts.post_parent as parent_id FROM {$wpdb->posts} AS posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id $outofstock_join WHERE posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.onsale = 1 $outofstock_where AND posts.post_parent NOT IN ( SELECT ID FROM `$wpdb->posts` as posts WHERE posts.post_type = 'product' AND posts.post_parent = 0 AND posts.post_status != 'publish' ) GROUP BY posts.ID " ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Returns a list of product IDs ( id as key => parent as value) that are * featured. Uses get_posts instead of wc_get_products since we want * some extra meta queries and ALL products (posts_per_page = -1). * * @return array * @since 3.0.0 */ public function get_featured_product_ids() { $product_visibility_term_ids = wc_get_product_visibility_term_ids(); return get_posts( array( 'post_type' => array( 'product', 'product_variation' ), 'posts_per_page' => -1, 'post_status' => 'publish', 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'relation' => 'AND', array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_term_ids['featured'] ), ), array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), 'operator' => 'NOT IN', ), ), 'fields' => 'id=>parent', ) ); } /** * Check if product sku is found for any other product IDs. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421. * @return bool */ public function is_existing_sku( $product_id, $sku ) { global $wpdb; // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery return (bool) $wpdb->get_var( $wpdb->prepare( " SELECT posts.ID FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status != 'trash' AND lookup.sku = %s AND lookup.product_id <> %d LIMIT 1 ", wp_slash( $sku ), $product_id ) ); } /** * Return product ID based on SKU. * * @since 3.0.0 * @param string $sku Product SKU. * @return int */ public function get_product_id_by_sku( $sku ) { global $wpdb; // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $id = $wpdb->get_var( $wpdb->prepare( " SELECT posts.ID FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status != 'trash' AND lookup.sku = %s LIMIT 1 ", $sku ) ); return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku ); } /** * Returns an array of IDs of products that have sales starting soon. * * @since 3.0.0 * @return array */ public function get_starting_sales() { global $wpdb; // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery return $wpdb->get_col( $wpdb->prepare( "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id WHERE postmeta.meta_key = '_sale_price_dates_from' AND postmeta_2.meta_key = '_price' AND postmeta_3.meta_key = '_sale_price' AND postmeta.meta_value > 0 AND postmeta.meta_value < %s AND postmeta_2.meta_value != postmeta_3.meta_value", time() ) ); } /** * Returns an array of IDs of products that have sales which are due to end. * * @since 3.0.0 * @return array */ public function get_ending_sales() { global $wpdb; // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery return $wpdb->get_col( $wpdb->prepare( "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id WHERE postmeta.meta_key = '_sale_price_dates_to' AND postmeta_2.meta_key = '_price' AND postmeta_3.meta_key = '_regular_price' AND postmeta.meta_value > 0 AND postmeta.meta_value < %s AND postmeta_2.meta_value != postmeta_3.meta_value", time() ) ); } /** * Find a matching (enabled) variation within a variable product. * * @since 3.0.0 * @param WC_Product $product Variable product. * @param array $match_attributes Array of attributes we want to try to match. * @return int Matching variation ID or 0. */ public function find_matching_product_variation( $product, $match_attributes = array() ) { global $wpdb; $meta_attribute_names = array(); // Get attributes to match in meta. foreach ( $product->get_attributes() as $attribute ) { if ( ! $attribute->get_variation() ) { continue; } $meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() ); } // Get the attributes of the variations. $query = $wpdb->prepare( " SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID WHERE postmeta.post_id IN ( SELECT ID FROM {$wpdb->posts} WHERE {$wpdb->posts}.post_parent = %d AND {$wpdb->posts}.post_status = 'publish' AND {$wpdb->posts}.post_type = 'product_variation' ) ", $product->get_id() ); $query .= " AND postmeta.meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $meta_attribute_names ) ) . "' )"; $query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;'; $attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( ! $attributes ) { return 0; } $sorted_meta = array(); foreach ( $attributes as $m ) { $sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key } /** * Check each variation to find the one that matches the $match_attributes. * * Note: Not all meta fields will be set which is why we check existance. */ foreach ( $sorted_meta as $variation_id => $variation ) { $match = true; // Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use. foreach ( $variation as $attribute_key => $attribute_value ) { $match_any_value = '' === $attribute_value; if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) { $match = false; // Requires a selection but no value was provide. } if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided. if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) { $match = false; // Provided value does not match variation. } } } if ( true === $match ) { return $variation_id; } } if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { /** * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. */ return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); } return 0; } /** * Creates all possible combinations of variations from the attributes, without creating duplicates. * * @since 3.6.0 * @todo Add to interface in 4.0. * @param WC_Product $product Variable product. * @param int $limit Limit the number of created variations. * @return int Number of created variations. */ public function create_all_product_variations( $product, $limit = -1 ) { $count = 0; if ( ! $product ) { return $count; } $attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' ); if ( empty( $attributes ) ) { return $count; } // Get existing variations so we don't create duplicates. $existing_variations = array_map( 'wc_get_product', $product->get_children() ); $existing_attributes = array(); foreach ( $existing_variations as $existing_variation ) { $existing_attributes[] = $existing_variation->get_attributes(); } $possible_attributes = array_reverse( wc_array_cartesian( $attributes ) ); foreach ( $possible_attributes as $possible_attribute ) { // Allow any order if key/values -- do not use strict mode. if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict continue; } $variation = wc_get_product_object( 'variation' ); $variation->set_parent_id( $product->get_id() ); $variation->set_attributes( $possible_attribute ); $variation_id = $variation->save(); do_action( 'product_variation_linked', $variation_id ); $count ++; if ( $limit > 0 && $count >= $limit ) { break; } } return $count; } /** * Make sure all variations have a sort order set so they can be reordered correctly. * * @param int $parent_id Product ID. */ public function sort_all_product_variations( $parent_id ) { global $wpdb; // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", $parent_id ) ); $index = 1; foreach ( $ids as $id ) { // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); } } /** * Return a list of related products (using data like categories and IDs). * * @since 3.0.0 * @param array $cats_array List of categories IDs. * @param array $tags_array List of tags IDs. * @param array $exclude_ids Excluded IDs. * @param int $limit Limit of results. * @param int $product_id Product ID. * @return array */ public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { global $wpdb; $args = array( 'categories' => $cats_array, 'tags' => $tags_array, 'exclude_ids' => $exclude_ids, 'limit' => $limit + 10, ); $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args ); // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_col( implode( ' ', $related_product_query ) ); } /** * Builds the related posts query. * * @since 3.0.0 * * @param array $cats_array List of categories IDs. * @param array $tags_array List of tags IDs. * @param array $exclude_ids Excluded IDs. * @param int $limit Limit of results. * * @return array */ public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { global $wpdb; $include_term_ids = array_merge( $cats_array, $tags_array ); $exclude_term_ids = array(); $product_visibility_term_ids = wc_get_product_visibility_term_ids(); if ( $product_visibility_term_ids['exclude-from-catalog'] ) { $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; } $query = array( 'fields' => " SELECT DISTINCT ID FROM {$wpdb->posts} p ", 'join' => '', 'where' => " WHERE 1=1 AND p.post_status = 'publish' AND p.post_type = 'product' ", 'limits' => ' LIMIT ' . absint( $limit ) . ' ', ); if ( count( $exclude_term_ids ) ) { $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; $query['where'] .= ' AND exclude_join.object_id IS NULL'; } if ( count( $include_term_ids ) ) { $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; } if ( count( $exclude_ids ) ) { $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )'; } return $query; } /** * Update a product's stock amount directly in the database. * * Updates both post meta and lookup tables. Ignores manage stock setting on the product. * * @param int $product_id_with_stock Product ID. * @param int|float|null $stock_quantity Stock quantity. */ protected function set_product_stock( $product_id_with_stock, $stock_quantity ) { global $wpdb; // Generate SQL. $sql = $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock ); $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' ); $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). // Sometimes I wonder if it shouldn't be part of update_lookup_table. wp_cache_delete( $product_id_with_stock, 'post_meta' ); $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); } /** * Update a product's stock amount directly. * * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables. * Uses locking to update the quantity. If the lock is not acquired, change is lost. * * @since 3.0.0 this supports set, increase and decrease. * @param int $product_id_with_stock Product ID. * @param int|float|null $stock_quantity Stock quantity. * @param string $operation Set, increase and decrease. * @return int|float New stock level. */ public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { global $wpdb; // Ensures a row exists to update. add_post_meta( $product_id_with_stock, '_stock', 0, true ); if ( 'set' === $operation ) { $new_stock = wc_stock_amount( $stock_quantity ); // Generate SQL. $sql = $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $new_stock, $product_id_with_stock ); } else { $current_stock = wc_stock_amount( $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';", $product_id_with_stock ) ) ); // Calculate new value for filter below. Set multiplier to subtract or add the meta_value. switch ( $operation ) { case 'increase': $new_stock = $current_stock + wc_stock_amount( $stock_quantity ); $multiplier = 1; break; default: $new_stock = $current_stock - wc_stock_amount( $stock_quantity ); $multiplier = -1; break; } // Generate SQL. $sql = $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'", wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation. $product_id_with_stock ); } $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation ); $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). // Sometimes I wonder if it shouldn't be part of update_lookup_table. wp_cache_delete( $product_id_with_stock, 'post_meta' ); $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); /** * Fire an action for this direct update so it can be detected by other code. * * @since 3.6 * @param int $product_id_with_stock Product ID that was updated directly. */ do_action( 'woocommerce_updated_product_stock', $product_id_with_stock ); return $new_stock; } /** * Update a product's sale count directly. * * Uses queries rather than update_post_meta so we can do this in one query for performance. * * @since 3.0.0 this supports set, increase and decrease. * @param int $product_id Product ID. * @param int|null $quantity Quantity. * @param string $operation set, increase and decrease. */ public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { global $wpdb; add_post_meta( $product_id, 'total_sales', 0, true ); // Update stock in DB directly. switch ( $operation ) { case 'increase': // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); break; case 'decrease': // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); break; default: // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); break; } wp_cache_delete( $product_id, 'post_meta' ); $this->update_lookup_table( $product_id, 'wc_product_meta_lookup' ); /** * Fire an action for this direct update so it can be detected by other code. * * @since 3.6 * @param int $product_id Product ID that was updated directly. */ do_action( 'woocommerce_updated_product_sales', $product_id ); } /** * Update a products average rating meta. * * @since 3.0.0 * @todo Deprecate unused function? * @param WC_Product $product Product object. */ public function update_average_rating( $product ) { update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); self::update_visibility( $product, true ); } /** * Update a products review count meta. * * @since 3.0.0 * @todo Deprecate unused function? * @param WC_Product $product Product object. */ public function update_review_count( $product ) { update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); } /** * Update a products rating counts. * * @since 3.0.0 * @todo Deprecate unused function? * @param WC_Product $product Product object. */ public function update_rating_counts( $product ) { update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); } /** * Get shipping class ID by slug. * * @since 3.0.0 * @param string $slug Product shipping class slug. * @return int|false */ public function get_shipping_class_id_by_slug( $slug ) { $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); if ( $shipping_class_term ) { return $shipping_class_term->term_id; } else { return false; } } /** * Returns an array of products. * * @param array $args Args to pass to WC_Product_Query(). * @return array|object * @see wc_get_products */ public function get_products( $args = array() ) { $query = new WC_Product_Query( $args ); return $query->get_products(); } /** * Search product data for a term and return ids. * * @param string $term Search term. * @param string $type Type of product. * @param bool $include_variations Include variations in search or not. * @param bool $all_statuses Should we search all statuses or limit to published. * @param null|int $limit Limit returned results. @since 3.5.0. * @param null|array $include Keep specific results. @since 3.6.0. * @param null|array $exclude Discard specific results. @since 3.6.0. * @return array of ids */ public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) { global $wpdb; $custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit ); if ( is_array( $custom_results ) ) { return $custom_results; } $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); $join_query = ''; $type_where = ''; $status_where = ''; $limit_query = ''; // When searching variations we should include the parent's meta table for use in searches. if ( $include_variations ) { $join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent "; } /** * Hook woocommerce_search_products_post_statuses. * * @since 3.7.0 * @param array $post_statuses List of post statuses. */ $post_statuses = apply_filters( 'woocommerce_search_products_post_statuses', current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ) ); // See if search term contains OR keywords. if ( stristr( $term, ' or ' ) ) { $term_groups = preg_split( '/\s+or\s+/i', $term ); } else { $term_groups = array( $term ); } $search_where = ''; $search_queries = array(); foreach ( $term_groups as $term_group ) { // Parse search terms. if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) { $search_terms = $this->get_valid_search_terms( $matches[0] ); $count = count( $search_terms ); // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. if ( 9 < $count || 0 === $count ) { $search_terms = array( $term_group ); } } else { $search_terms = array( $term_group ); } $term_group_query = ''; $searchand = ''; foreach ( $search_terms as $search_term ) { $like = '%' . $wpdb->esc_like( $search_term ) . '%'; // Variations should also search the parent's meta table for fallback fields. if ( $include_variations ) { $variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like ); } else { $variation_query = ''; } $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $searchand = ' AND '; } if ( $term_group_query ) { $search_queries[] = $term_group_query; } } if ( ! empty( $search_queries ) ) { $search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') '; } if ( ! empty( $include ) && is_array( $include ) ) { $search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') '; } if ( ! empty( $exclude ) && is_array( $exclude ) ) { $search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') '; } if ( 'virtual' === $type ) { $type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) '; } elseif ( 'downloadable' === $type ) { $type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) '; } if ( ! $all_statuses ) { $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') "; } if ( $limit ) { $limit_query = $wpdb->prepare( ' LIMIT %d ', $limit ); } // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery $search_results = $wpdb->get_results( // phpcs:disable "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id $join_query WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "') $search_where $status_where $type_where ORDER BY posts.post_parent ASC, posts.post_title ASC $limit_query " // phpcs:enable ); $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) ); if ( is_numeric( $term ) ) { $post_id = absint( $term ); $post_type = get_post_type( $post_id ); if ( 'product_variation' === $post_type && $include_variations ) { $product_ids[] = $post_id; } elseif ( 'product' === $post_type ) { $product_ids[] = $post_id; } $product_ids[] = wp_get_post_parent_id( $post_id ); } return wp_parse_id_list( $product_ids ); } /** * Get the product type based on product ID. * * @since 3.0.0 * @param int $product_id Product ID. * @return bool|string */ public function get_product_type( $product_id ) { $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id; $product_type = wp_cache_get( $cache_key, 'products' ); if ( $product_type ) { return $product_type; } $post_type = get_post_type( $product_id ); if ( 'product_variation' === $post_type ) { $product_type = 'variation'; } elseif ( 'product' === $post_type ) { $terms = get_the_terms( $product_id, 'product_type' ); $product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; } else { $product_type = false; } wp_cache_set( $cache_key, $product_type, 'products' ); return $product_type; } /** * Add ability to get products by 'reviews_allowed' in WC_Product_Query. * * @since 3.2.0 * @param string $where Where clause. * @param WP_Query $wp_query WP_Query instance. * @return string */ public function reviews_allowed_query_where( $where, $wp_query ) { global $wpdb; if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) { if ( $wp_query->query_vars['reviews_allowed'] ) { $where .= " AND $wpdb->posts.comment_status = 'open'"; } else { $where .= " AND $wpdb->posts.comment_status = 'closed'"; } } return $where; } /** * Get valid WP_Query args from a WC_Product_Query's query variables. * * @since 3.2.0 * @param array $query_vars Query vars from a WC_Product_Query. * @return array */ protected function get_wp_query_args( $query_vars ) { // Map query vars to ones that get_wp_query_args or WP_Query recognize. $key_mapping = array( 'status' => 'post_status', 'page' => 'paged', 'include' => 'post__in', 'stock_quantity' => 'stock', 'average_rating' => 'wc_average_rating', 'review_count' => 'wc_review_count', ); foreach ( $key_mapping as $query_key => $db_key ) { if ( isset( $query_vars[ $query_key ] ) ) { $query_vars[ $db_key ] = $query_vars[ $query_key ]; unset( $query_vars[ $query_key ] ); } } // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'. $boolean_queries = array( 'virtual', 'downloadable', 'sold_individually', 'manage_stock', ); foreach ( $boolean_queries as $boolean_query ) { if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) { $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no'; } } // These queries cannot be auto-generated so we have to remove them and build them manually. $manual_queries = array( 'sku' => '', 'featured' => '', 'visibility' => '', ); foreach ( $manual_queries as $key => $manual_query ) { if ( isset( $query_vars[ $key ] ) ) { $manual_queries[ $key ] = $query_vars[ $key ]; unset( $query_vars[ $key ] ); } } $wp_query_args = parent::get_wp_query_args( $query_vars ); if ( ! isset( $wp_query_args['date_query'] ) ) { $wp_query_args['date_query'] = array(); } if ( ! isset( $wp_query_args['meta_query'] ) ) { $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } // Handle product types. if ( 'variation' === $query_vars['type'] ) { $wp_query_args['post_type'] = 'product_variation'; } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) { $wp_query_args['post_type'] = array( 'product_variation', 'product' ); $wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'relation' => 'OR', array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $query_vars['type'], ), array( 'taxonomy' => 'product_type', 'field' => 'id', 'operator' => 'NOT EXISTS', ), ); } else { $wp_query_args['post_type'] = 'product'; $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $query_vars['type'], ); } // Handle product categories. if ( ! empty( $query_vars['category'] ) ) { $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_cat', 'field' => 'slug', 'terms' => $query_vars['category'], ); } // Handle product tags. if ( ! empty( $query_vars['tag'] ) ) { unset( $wp_query_args['tag'] ); $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_tag', 'field' => 'slug', 'terms' => $query_vars['tag'], ); } // Handle shipping classes. if ( ! empty( $query_vars['shipping_class'] ) ) { $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_shipping_class', 'field' => 'slug', 'terms' => $query_vars['shipping_class'], ); } // Handle total_sales. // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix. if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) { $wp_query_args['meta_query'][] = array( 'key' => 'total_sales', 'value' => absint( $query_vars['total_sales'] ), 'compare' => '=', ); } // Handle SKU. if ( $manual_queries['sku'] ) { // Check for existing values if wildcard is used. if ( '*' === $manual_queries['sku'] ) { $wp_query_args['meta_query'][] = array( array( 'key' => '_sku', 'compare' => 'EXISTS', ), array( 'key' => '_sku', 'value' => '', 'compare' => '!=', ), ); } else { $wp_query_args['meta_query'][] = array( 'key' => '_sku', 'value' => $manual_queries['sku'], 'compare' => 'LIKE', ); } } // Handle featured. if ( '' !== $manual_queries['featured'] ) { $product_visibility_term_ids = wc_get_product_visibility_term_ids(); if ( $manual_queries['featured'] ) { $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_term_ids['featured'] ), ); $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), 'operator' => 'NOT IN', ); } else { $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_term_ids['featured'] ), 'operator' => 'NOT IN', ); } } // Handle visibility. if ( $manual_queries['visibility'] ) { switch ( $manual_queries['visibility'] ) { case 'search': $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'slug', 'terms' => array( 'exclude-from-search' ), 'operator' => 'NOT IN', ); break; case 'catalog': $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'slug', 'terms' => array( 'exclude-from-catalog' ), 'operator' => 'NOT IN', ); break; case 'visible': $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'slug', 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), 'operator' => 'NOT IN', ); break; case 'hidden': $wp_query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'slug', 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), 'operator' => 'AND', ); break; } } // Handle date queries. $date_queries = array( 'date_created' => 'post_date', 'date_modified' => 'post_modified', 'date_on_sale_from' => '_sale_price_dates_from', 'date_on_sale_to' => '_sale_price_dates_to', ); foreach ( $date_queries as $query_var_key => $db_key ) { if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { // Remove any existing meta queries for the same keys to prevent conflicts. $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); foreach ( $existing_queries as $query_index => $query_contents ) { unset( $wp_query_args['meta_query'][ $query_index ] ); } $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); } } // Handle paginate. if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { $wp_query_args['no_found_rows'] = true; } // Handle reviews_allowed. if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) { add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 ); } // Handle orderby. if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) { $wp_query_args['orderby'] = 'post__in'; } return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this ); } /** * Query for Products matching specific criteria. * * @since 3.2.0 * * @param array $query_vars Query vars from a WC_Product_Query. * * @return array|object */ public function query( $query_vars ) { $args = $this->get_wp_query_args( $query_vars ); if ( ! empty( $args['errors'] ) ) { $query = (object) array( 'posts' => array(), 'found_posts' => 0, 'max_num_pages' => 0, ); } else { $query = new WP_Query( $args ); } if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) { // Prime caches before grabbing objects. update_post_caches( $query->posts, array( 'product', 'product_variation' ) ); } $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) ); if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { return (object) array( 'products' => $products, 'total' => $query->found_posts, 'max_num_pages' => $query->max_num_pages, ); } return $products; } /** * Get data to save to a lookup table. * * @since 3.6.0 * @param int $id ID of object to update. * @param string $table Lookup table name. * @return array */ protected function get_data_for_lookup_table( $id, $table ) { if ( 'wc_product_meta_lookup' === $table ) { $price_meta = (array) get_post_meta( $id, '_price', false ); $manage_stock = get_post_meta( $id, '_manage_stock', true ); $stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null; $price = wc_format_decimal( get_post_meta( $id, '_price', true ) ); $sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) ); return array( 'product_id' => absint( $id ), 'sku' => get_post_meta( $id, '_sku', true ), 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, 'min_price' => reset( $price_meta ), 'max_price' => end( $price_meta ), 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, 'stock_quantity' => $stock, 'stock_status' => get_post_meta( $id, '_stock_status', true ), 'rating_count' => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ), 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), 'total_sales' => get_post_meta( $id, 'total_sales', true ), 'tax_status' => get_post_meta( $id, '_tax_status', true ), 'tax_class' => get_post_meta( $id, '_tax_class', true ), ); } return array(); } /** * Get primary key name for lookup table. * * @since 3.6.0 * @param string $table Lookup table name. * @return string */ protected function get_primary_key_for_lookup_table( $table ) { if ( 'wc_product_meta_lookup' === $table ) { return 'product_id'; } return ''; } /** * Returns query statement for getting current `_stock` of a product. * * @internal MAX function below is used to make sure result is a scalar. * @param int $product_id Product ID. * @return string|void Query statement. */ public function get_query_for_stock( $product_id ) { global $wpdb; return $wpdb->prepare( " SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table WHERE meta_table.meta_key = '_stock' AND meta_table.post_id = %d ", $product_id ); } } includes/data-stores/class-wc-order-item-shipping-data-store.php 0000644 00000004304 15132754524 0020763 0 ustar 00 <?php /** * WC Order Item Shipping Data Store * * @version 3.0.0 * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Order_Item_Shipping_Data_Store class. */ class WC_Order_Item_Shipping_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface { /** * Data stored in meta keys. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( 'method_id', 'instance_id', 'cost', 'total_tax', 'taxes' ); /** * Read/populate data properties specific to this order item. * * @since 3.0.0 * @param WC_Order_Item_Shipping $item Item to read to. * @throws Exception If invalid shipping order item. */ public function read( &$item ) { parent::read( $item ); $id = $item->get_id(); $item->set_props( array( 'method_id' => get_metadata( 'order_item', $id, 'method_id', true ), 'instance_id' => get_metadata( 'order_item', $id, 'instance_id', true ), 'total' => get_metadata( 'order_item', $id, 'cost', true ), 'taxes' => get_metadata( 'order_item', $id, 'taxes', true ), ) ); // BW compat. if ( '' === $item->get_instance_id() && strstr( $item->get_method_id(), ':' ) ) { $legacy_method_id = explode( ':', $item->get_method_id() ); $item->set_method_id( $legacy_method_id[0] ); $item->set_instance_id( $legacy_method_id[1] ); } $item->set_object_read( true ); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $id will be set. * * @since 3.0.0 * @param WC_Order_Item_Shipping $item Item to save. */ public function save_item_data( &$item ) { $id = $item->get_id(); $changes = $item->get_changes(); $meta_key_to_props = array( 'method_id' => 'method_id', 'instance_id' => 'instance_id', 'cost' => 'total', 'total_tax' => 'total_tax', 'taxes' => 'taxes', ); $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); foreach ( $props_to_update as $meta_key => $prop ) { update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); } } } includes/data-stores/class-wc-order-item-tax-data-store.php 0000644 00000004362 15132754524 0017742 0 ustar 00 <?php /** * Class WC_Order_Item_Tax_Data_Store file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Order Item Tax Data Store * * @version 3.0.0 */ class WC_Order_Item_Tax_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface { /** * Data stored in meta keys. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( 'rate_id', 'label', 'compound', 'tax_amount', 'shipping_tax_amount', 'rate_percent' ); /** * Read/populate data properties specific to this order item. * * @since 3.0.0 * @param WC_Order_Item_Tax $item Tax order item object. * @throws Exception If invalid order item. */ public function read( &$item ) { parent::read( $item ); $id = $item->get_id(); $item->set_props( array( 'rate_id' => get_metadata( 'order_item', $id, 'rate_id', true ), 'label' => get_metadata( 'order_item', $id, 'label', true ), 'compound' => get_metadata( 'order_item', $id, 'compound', true ), 'tax_total' => get_metadata( 'order_item', $id, 'tax_amount', true ), 'shipping_tax_total' => get_metadata( 'order_item', $id, 'shipping_tax_amount', true ), 'rate_percent' => get_metadata( 'order_item', $id, 'rate_percent', true ), ) ); $item->set_object_read( true ); } /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $id will be set. * * @since 3.0.0 * @param WC_Order_Item_Tax $item Tax order item object. */ public function save_item_data( &$item ) { $id = $item->get_id(); $changes = $item->get_changes(); $meta_key_to_props = array( 'rate_id' => 'rate_id', 'label' => 'label', 'compound' => 'compound', 'tax_amount' => 'tax_total', 'shipping_tax_amount' => 'shipping_tax_total', 'rate_percent' => 'rate_percent', ); $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); foreach ( $props_to_update as $meta_key => $prop ) { update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); } } } includes/data-stores/class-wc-product-variation-data-store-cpt.php 0000644 00000050403 15132754524 0021334 0 ustar 00 <?php /** * Class WC_Product_Variation_Data_Store_CPT file. * * @package WooCommerce\DataStores */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Variation Product Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface { /** * Callback to remove unwanted meta data. * * @param object $meta Meta object. * @return bool false if excluded. */ protected function exclude_internal_meta_keys( $meta ) { return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' ); } /* |-------------------------------------------------------------------------- | CRUD Methods |-------------------------------------------------------------------------- */ /** * Reads a product from the database and sets its data to the class. * * @since 3.0.0 * @param WC_Product_Variation $product Product object. * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID. */ public function read( &$product ) { $product->set_defaults(); if ( ! $product->get_id() ) { return; } $post_object = get_post( $product->get_id() ); if ( ! $post_object ) { return; } if ( 'product_variation' !== $post_object->post_type ) { throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) ); } $product->set_props( array( 'name' => $post_object->post_title, 'slug' => $post_object->post_name, 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), 'status' => $post_object->post_status, 'menu_order' => $post_object->menu_order, 'reviews_allowed' => 'open' === $post_object->comment_status, 'parent_id' => $post_object->post_parent, 'attribute_summary' => $post_object->post_excerpt, ) ); // The post parent is not a valid variable product so we should prevent this. if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { $product->set_parent_id( 0 ); } $this->read_downloads( $product ); $this->read_product_data( $product ); $this->read_extra_data( $product ); $product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) ); $updates = array(); /** * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update. */ $new_title = $this->generate_product_title( $product ); if ( $post_object->post_title !== $new_title ) { $product->set_name( $new_title ); $updates = array_merge( $updates, array( 'post_title' => $new_title ) ); } /** * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values. * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated * for respective products when they're read. */ $new_attribute_summary = $this->generate_attribute_summary( $product ); if ( $new_attribute_summary !== $post_object->post_excerpt ) { $product->set_attribute_summary( $new_attribute_summary ); $updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) ); } if ( ! empty( $updates ) ) { $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) ); clean_post_cache( $product->get_id() ); } // Set object_read true once all data is read. $product->set_object_read( true ); } /** * Create a new product. * * @since 3.0.0 * @param WC_Product_Variation $product Product object. */ public function create( &$product ) { if ( ! $product->get_date_created() ) { $product->set_date_created( time() ); } $new_title = $this->generate_product_title( $product ); if ( $product->get_name( 'edit' ) !== $new_title ) { $product->set_name( $new_title ); } $attribute_summary = $this->generate_attribute_summary( $product ); $product->set_attribute_summary( $attribute_summary ); // The post parent is not a valid variable product so we should prevent this. if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { $product->set_parent_id( 0 ); } $id = wp_insert_post( apply_filters( 'woocommerce_new_product_variation_data', array( 'post_type' => 'product_variation', 'post_status' => $product->get_status() ? $product->get_status() : 'publish', 'post_author' => get_current_user_id(), 'post_title' => $product->get_name( 'edit' ), 'post_excerpt' => $product->get_attribute_summary( 'edit' ), 'post_content' => '', 'post_parent' => $product->get_parent_id(), 'comment_status' => 'closed', 'ping_status' => 'closed', 'menu_order' => $product->get_menu_order(), 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), 'post_name' => $product->get_slug( 'edit' ), ) ), true ); if ( $id && ! is_wp_error( $id ) ) { $product->set_id( $id ); $this->update_post_meta( $product, true ); $this->update_terms( $product, true ); $this->update_visibility( $product, true ); $this->update_attributes( $product, true ); $this->handle_updated_props( $product ); $product->save_meta_data(); $product->apply_changes(); $this->update_version_and_type( $product ); $this->update_guid( $product ); $this->clear_caches( $product ); do_action( 'woocommerce_new_product_variation', $id, $product ); } } /** * Updates an existing product. * * @since 3.0.0 * @param WC_Product_Variation $product Product object. */ public function update( &$product ) { $product->save_meta_data(); if ( ! $product->get_date_created() ) { $product->set_date_created( time() ); } $new_title = $this->generate_product_title( $product ); if ( $product->get_name( 'edit' ) !== $new_title ) { $product->set_name( $new_title ); } // The post parent is not a valid variable product so we should prevent this. if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { $product->set_parent_id( 0 ); } $changes = $product->get_changes(); if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) { $product->set_attribute_summary( $this->generate_attribute_summary( $product ) ); } // Only update the post when the post data changes. if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) { $post_data = array( 'post_title' => $product->get_name( 'edit' ), 'post_excerpt' => $product->get_attribute_summary( 'edit' ), 'post_parent' => $product->get_parent_id( 'edit' ), 'comment_status' => 'closed', 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', 'menu_order' => $product->get_menu_order( 'edit' ), 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), 'post_type' => 'product_variation', 'post_name' => $product->get_slug( 'edit' ), ); /** * When updating this object, to prevent infinite loops, use $wpdb * to update data, since wp_update_post spawns more calls to the * save_post action. * * This ensures hooks are fired by either WP itself (admin screen save), * or an update purely from CRUD. */ if ( doing_action( 'save_post' ) ) { $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); clean_post_cache( $product->get_id() ); } else { wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); } $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. } else { // Only update post modified time to record this save event. $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, array( 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', 1 ), ), array( 'ID' => $product->get_id(), ) ); clean_post_cache( $product->get_id() ); } $this->update_post_meta( $product ); $this->update_terms( $product ); $this->update_visibility( $product, true ); $this->update_attributes( $product ); $this->handle_updated_props( $product ); $product->apply_changes(); $this->update_version_and_type( $product ); $this->clear_caches( $product ); do_action( 'woocommerce_update_product_variation', $product->get_id(), $product ); } /* |-------------------------------------------------------------------------- | Additional Methods |-------------------------------------------------------------------------- */ /** * Generates a title with attribute information for a variation. * Products will get a title of the form "Name - Value, Value" or just "Name". * * @since 3.0.0 * @param WC_Product $product Product object. * @return string */ protected function generate_product_title( $product ) { $attributes = (array) $product->get_attributes(); // Do not include attributes if the product has 3+ attributes. $should_include_attributes = count( $attributes ) < 3; // Do not include attributes if an attribute name has 2+ words and the // product has multiple attributes. if ( $should_include_attributes && 1 < count( $attributes ) ) { foreach ( $attributes as $name => $value ) { if ( false !== strpos( $name, '-' ) ) { $should_include_attributes = false; break; } } } $should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product ); $separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product ); $title_base = get_post_field( 'post_title', $product->get_parent_id() ); $title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : ''; return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix ); } /** * Generates attribute summary for the variation. * * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. * * @since 3.6.0 * @param WC_Product_Variation $product Product variation to generate the attribute summary for. * * @return string */ protected function generate_attribute_summary( $product ) { return wc_get_formatted_variation( $product, true, true ); } /** * Make sure we store the product version (to track data changes). * * @param WC_Product $product Product object. * @since 3.0.0 */ protected function update_version_and_type( &$product ) { wp_set_object_terms( $product->get_id(), '', 'product_type' ); update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); } /** * Read post data. * * @since 3.0.0 * @param WC_Product_Variation $product Product object. * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status. */ protected function read_product_data( &$product ) { $id = $product->get_id(); $product->set_props( array( 'description' => get_post_meta( $id, '_variation_description', true ), 'regular_price' => get_post_meta( $id, '_regular_price', true ), 'sale_price' => get_post_meta( $id, '_sale_price', true ), 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), 'stock_status' => get_post_meta( $id, '_stock_status', true ), 'low_stock_amount' => get_post_meta( $id, '_low_stock_amount', true ), 'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ), 'virtual' => get_post_meta( $id, '_virtual', true ), 'downloadable' => get_post_meta( $id, '_downloadable', true ), 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), 'download_limit' => get_post_meta( $id, '_download_limit', true ), 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), 'image_id' => get_post_thumbnail_id( $id ), 'backorders' => get_post_meta( $id, '_backorders', true ), 'sku' => get_post_meta( $id, '_sku', true ), 'stock_quantity' => get_post_meta( $id, '_stock', true ), 'weight' => get_post_meta( $id, '_weight', true ), 'length' => get_post_meta( $id, '_length', true ), 'width' => get_post_meta( $id, '_width', true ), 'height' => get_post_meta( $id, '_height', true ), 'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ), ) ); if ( $product->is_on_sale( 'edit' ) ) { $product->set_price( $product->get_sale_price( 'edit' ) ); } else { $product->set_price( $product->get_regular_price( 'edit' ) ); } $parent_object = get_post( $product->get_parent_id() ); $terms = get_the_terms( $product->get_parent_id(), 'product_visibility' ); $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); $exclude_search = in_array( 'exclude-from-search', $term_names, true ); $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); if ( $exclude_search && $exclude_catalog ) { $catalog_visibility = 'hidden'; } elseif ( $exclude_search ) { $catalog_visibility = 'catalog'; } elseif ( $exclude_catalog ) { $catalog_visibility = 'search'; } else { $catalog_visibility = 'visible'; } $product->set_parent_data( array( 'title' => $parent_object ? $parent_object->post_title : '', 'status' => $parent_object ? $parent_object->post_status : '', 'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ), 'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ), 'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ), 'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ), 'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ), 'length' => get_post_meta( $product->get_parent_id(), '_length', true ), 'width' => get_post_meta( $product->get_parent_id(), '_width', true ), 'height' => get_post_meta( $product->get_parent_id(), '_height', true ), 'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ), 'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ), 'image_id' => get_post_thumbnail_id( $product->get_parent_id() ), 'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ), 'catalog_visibility' => $catalog_visibility, ) ); // Pull data from the parent when there is no user-facing way to set props. $product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) ); $product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) ); $product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) ); } /** * For all stored terms in all taxonomies, save them to the DB. * * @since 3.0.0 * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. */ protected function update_terms( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); } } /** * Update visibility terms based on props. * * @since 3.0.0 * * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. */ protected function update_visibility( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) { $terms = array(); if ( 'outofstock' === $product->get_stock_status() ) { $terms[] = 'outofstock'; } wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ); } } /** * Update attribute meta values. * * @since 3.0.0 * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. */ protected function update_attributes( &$product, $force = false ) { $changes = $product->get_changes(); if ( $force || array_key_exists( 'attributes', $changes ) ) { global $wpdb; $product_id = $product->get_id(); $attributes = $product->get_attributes(); $updated_attribute_keys = array(); foreach ( $attributes as $key => $value ) { update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) ); $updated_attribute_keys[] = 'attribute_' . $key; } // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d", $wpdb->esc_like( 'attribute_' ) . '%', $product_id ) ); foreach ( $delete_attribute_keys as $key ) { delete_post_meta( $product_id, $key ); } } } /** * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. * * @since 3.0.0 * @param WC_Product $product Product object. * @param bool $force Force update. Used during create. */ public function update_post_meta( &$product, $force = false ) { $meta_key_to_props = array( '_variation_description' => 'description', ); $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $product->{"get_$prop"}( 'edit' ); $updated = update_post_meta( $product->get_id(), $meta_key, $value ); if ( $updated ) { $this->updated_props[] = $prop; } } parent::update_post_meta( $product, $force ); } /** * Update product variation guid. * * @param WC_Product_Variation $product Product variation object. * * @since 3.6.0 */ protected function update_guid( $product ) { global $wpdb; $guid = home_url( add_query_arg( array( 'post_type' => 'product_variation', 'p' => $product->get_id(), ), '' ) ); $wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) ); } } includes/data-stores/class-wc-coupon-data-store-cpt.php 0000644 00000060133 15132754524 0017166 0 ustar 00 <?php /** * Class WC_Coupon_Data_Store_CPT file. * * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Coupon Data Store: Custom Post Type. * * @version 3.0.0 */ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface { /** * Internal meta type used to store coupon data. * * @since 3.0.0 * @var string */ protected $meta_type = 'post'; /** * Data stored in meta keys, but not considered "meta" for a coupon. * * @since 3.0.0 * @var array */ protected $internal_meta_keys = array( 'discount_type', 'coupon_amount', 'expiry_date', 'date_expires', 'usage_count', 'individual_use', 'product_ids', 'exclude_product_ids', 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'free_shipping', 'product_categories', 'exclude_product_categories', 'exclude_sale_items', 'minimum_amount', 'maximum_amount', 'customer_email', '_used_by', '_edit_lock', '_edit_last', ); /** * The updated coupon properties * * @since 4.1.0 * @var array */ protected $updated_props = array(); /** * Method to create a new coupon in the database. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. */ public function create( &$coupon ) { if ( ! $coupon->get_date_created( 'edit' ) ) { $coupon->set_date_created( time() ); } $coupon_id = wp_insert_post( apply_filters( 'woocommerce_new_coupon_data', array( 'post_type' => 'shop_coupon', 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_title' => $coupon->get_code( 'edit' ), 'post_content' => '', 'post_excerpt' => $coupon->get_description( 'edit' ), 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ), ) ), true ); if ( $coupon_id ) { $coupon->set_id( $coupon_id ); $this->update_post_meta( $coupon ); $coupon->save_meta_data(); $coupon->apply_changes(); delete_transient( 'rest_api_coupons_type_count' ); do_action( 'woocommerce_new_coupon', $coupon_id, $coupon ); } } /** * Method to read a coupon. * * @since 3.0.0 * * @param WC_Coupon $coupon Coupon object. * * @throws Exception If invalid coupon. */ public function read( &$coupon ) { $coupon->set_defaults(); $post_object = get_post( $coupon->get_id() ); if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) { throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) ); } $coupon_id = $coupon->get_id(); $coupon->set_props( array( 'code' => $post_object->post_title, 'description' => $post_object->post_excerpt, 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine. 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ), 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ), 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ), 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ), 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ), 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ), 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ), 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ), 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null, 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ), 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ), 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ), 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ), 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ), 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ), 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ), 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ), ) ); $coupon->read_meta_data(); $coupon->set_object_read( true ); do_action( 'woocommerce_coupon_loaded', $coupon ); } /** * Updates a coupon in the database. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. */ public function update( &$coupon ) { $coupon->save_meta_data(); $changes = $coupon->get_changes(); if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { $post_data = array( 'post_title' => $coupon->get_code( 'edit' ), 'post_excerpt' => $coupon->get_description( 'edit' ), 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ), 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), ); /** * When updating this object, to prevent infinite loops, use $wpdb * to update data, since wp_update_post spawns more calls to the * save_post action. * * This ensures hooks are fired by either WP itself (admin screen save), * or an update purely from CRUD. */ if ( doing_action( 'save_post' ) ) { $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) ); clean_post_cache( $coupon->get_id() ); } else { wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ); } $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. } $this->update_post_meta( $coupon ); $coupon->apply_changes(); delete_transient( 'rest_api_coupons_type_count' ); do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon ); } /** * Deletes a coupon from the database. * * @since 3.0.0 * * @param WC_Coupon $coupon Coupon object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$coupon, $args = array() ) { $args = wp_parse_args( $args, array( 'force_delete' => false, ) ); $id = $coupon->get_id(); if ( ! $id ) { return; } if ( $args['force_delete'] ) { wp_delete_post( $id ); wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' ); $coupon->set_id( 0 ); do_action( 'woocommerce_delete_coupon', $id ); } else { wp_trash_post( $id ); do_action( 'woocommerce_trash_coupon', $id ); } } /** * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class. * * @param WC_Coupon $coupon Coupon object. * @since 3.0.0 */ private function update_post_meta( &$coupon ) { $meta_key_to_props = array( 'discount_type' => 'discount_type', 'coupon_amount' => 'amount', 'individual_use' => 'individual_use', 'product_ids' => 'product_ids', 'exclude_product_ids' => 'excluded_product_ids', 'usage_limit' => 'usage_limit', 'usage_limit_per_user' => 'usage_limit_per_user', 'limit_usage_to_x_items' => 'limit_usage_to_x_items', 'usage_count' => 'usage_count', 'date_expires' => 'date_expires', 'free_shipping' => 'free_shipping', 'product_categories' => 'product_categories', 'exclude_product_categories' => 'excluded_product_categories', 'exclude_sale_items' => 'exclude_sale_items', 'minimum_amount' => 'minimum_amount', 'maximum_amount' => 'maximum_amount', 'customer_email' => 'email_restrictions', ); $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $coupon->{"get_$prop"}( 'edit' ); $value = is_string( $value ) ? wp_slash( $value ) : $value; switch ( $prop ) { case 'individual_use': case 'free_shipping': case 'exclude_sale_items': $value = wc_bool_to_string( $value ); break; case 'product_ids': case 'excluded_product_ids': $value = implode( ',', array_filter( array_map( 'intval', $value ) ) ); break; case 'product_categories': case 'excluded_product_categories': $value = array_filter( array_map( 'intval', $value ) ); break; case 'email_restrictions': $value = array_filter( array_map( 'sanitize_email', $value ) ); break; case 'date_expires': $value = $value ? $value->getTimestamp() : null; break; } $updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value ); if ( $updated ) { $this->updated_props[] = $prop; } } do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props ); } /** * Increase usage count for current coupon. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. * @param string $used_by Either user ID or billing email. * @param WC_Order $order (Optional) If passed, clears the hold record associated with order. * @return int New usage count. */ public function increase_usage_count( &$coupon, $used_by = '', $order = null ) { $coupon_held_key_for_user = ''; if ( $order instanceof WC_Order ) { $coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() ); } $new_count = $this->update_usage_count_meta( $coupon, 'increase' ); if ( $used_by ) { $this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user ); $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); } do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by ); return $new_count; } /** * Helper function to add a `_used_by` record to track coupons used by the user. * * @param WC_Coupon $coupon Coupon object. * @param string $used_by Either user ID or billing email. * @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record. */ private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) { global $wpdb; if ( $coupon_held_key && '' !== $coupon_held_key ) { // Looks like we added a tentative record for this coupon getting used. // Lets change the tentative record to a permanent one. $result = $wpdb->query( $wpdb->prepare( " UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1", '_used_by', $used_by, $coupon_held_key ) ); if ( ! $result ) { // If no rows were updated, then insert a `_used_by` row manually to maintain consistency. add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); } } else { add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); } } /** * Decrease usage count for current coupon. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. * @param string $used_by Either user ID or billing email. * @return int New usage count. */ public function decrease_usage_count( &$coupon, $used_by = '' ) { global $wpdb; $new_count = $this->update_usage_count_meta( $coupon, 'decrease' ); if ( $used_by ) { /** * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes. * all instances where the key and value match, and we only want to delete one. */ $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $coupon->get_id() ) ); if ( $meta_id ) { delete_metadata_by_mid( 'post', $meta_id ); $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); } } do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by ); return $new_count; } /** * Increase or decrease the usage count for a coupon by 1. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. * @param string $operation 'increase' or 'decrease'. * @return int New usage count */ private function update_usage_count_meta( &$coupon, $operation = 'increase' ) { global $wpdb; $id = $coupon->get_id(); $operator = ( 'increase' === $operation ) ? '+' : '-'; add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true ); $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); // Get the latest value direct from the DB, instead of possibly the WP meta cache. return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); } /** * Returns tentative usage count for coupon. * * @param int $coupon_id Coupon ID. * * @return int Tentative usage count. */ public function get_tentative_usage_count( $coupon_id ) { global $wpdb; return $wpdb->get_var( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->get_tentative_usage_query( $coupon_id ) ); } /** * Get the number of uses for a coupon by user ID. * * @since 3.0.0 * @param WC_Coupon $coupon Coupon object. * @param int $user_id User ID. * @return int */ public function get_usage_by_user_id( &$coupon, $user_id ) { global $wpdb; $usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $coupon->get_id(), $user_id ) ); $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ); return $tentative_usage_count + $usage_count; } /** * Get the number of uses for a coupon by email address * * @since 3.6.4 * @param WC_Coupon $coupon Coupon object. * @param string $email Email address. * @return int */ public function get_usage_by_email( &$coupon, $email ) { global $wpdb; $usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", $coupon->get_id(), $email ) ); $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) ); return $tentative_usage_count + $usage_count; } /** * Get tentative coupon usages for user. * * @param int $coupon_id Coupon ID. * @param array $user_aliases Array of user aliases to check tentative usages for. * * @return string|null */ public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) { global $wpdb; return $wpdb->get_var( $this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) ); // WPCS: unprepared SQL ok. } /** * Get held time for resources before cancelling the order. Use 60 minutes as sane default. * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds. * * @return int */ private function get_tentative_held_time() { return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60; } /** * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon. * * @param WC_Coupon $coupon Coupon object. * * @return bool|int|string|null Returns meta key if coupon was held, null if returned early. */ public function check_and_hold_coupon( $coupon ) { global $wpdb; $usage_limit = $coupon->get_usage_limit(); $held_time = $this->get_tentative_held_time(); if ( 0 >= $usage_limit || 0 >= $held_time ) { return null; } if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { return null; } // Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`. // We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple. if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) { $coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter. $coupon->save(); } $query_for_usages = $wpdb->prepare( " SELECT meta_value from $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = 'usage_count' AND {$wpdb->postmeta}.post_id = %d LIMIT 1 FOR UPDATE ", $coupon->get_id() ); $query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() ); $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); $coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); $insert_statement = $wpdb->prepare( " INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) SELECT %d, %s, %s FROM DUAL WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d ", $coupon->get_id(), $coupon_usage_key, '', $usage_limit ); // WPCS: unprepared SQL ok. /** * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock. * We will try to insert 3 times before giving up to recover from deadlock. */ for ( $count = 0; $count < 3; $count++ ) { $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. if ( false !== $result ) { break; } } return $result > 0 ? $coupon_usage_key : $result; } /** * Generate query to calculate tentative usages for the coupon. * * @param int $coupon_id Coupon ID to get tentative usage query for. * * @return string Query for tentative usages. */ private function get_tentative_usage_query( $coupon_id ) { global $wpdb; return $wpdb->prepare( " SELECT COUNT(meta_id) FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key like %s AND {$wpdb->postmeta}.meta_key > %s AND {$wpdb->postmeta}.post_id = %d FOR UPDATE ", array( '_coupon_held_%', '_coupon_held_' . time(), $coupon_id, ) ); // WPCS: unprepared SQL ok. } /** * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon. * * @param WC_Coupon $coupon Coupon object. * @param array $user_aliases Emails or Ids to check for user. * @param string $user_alias Email/ID to use as `used_by` value. * * @return null|false|int */ public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) { global $wpdb; $limit_per_user = $coupon->get_usage_limit_per_user(); $held_time = $this->get_tentative_held_time(); if ( 0 >= $limit_per_user || 0 >= $held_time ) { // This coupon do not have any restriction for usage per customer. No need to check further, lets bail. return null; } if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { return null; } $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); $query_for_usages = $wpdb->prepare( " SELECT COUNT(*) FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = '_used_by' AND {$wpdb->postmeta}.meta_value IN ('$format') AND {$wpdb->postmeta}.post_id = %d FOR UPDATE ", array_merge( $user_aliases, array( $coupon->get_id() ) ) ); // WPCS: unprepared SQL ok. $query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases ); $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); $coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); $insert_statement = $wpdb->prepare( " INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) SELECT %d, %s, %s FROM DUAL WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d ", $coupon->get_id(), $coupon_used_by_meta_key, $user_alias, $limit_per_user ); // WPCS: unprepared SQL ok. // This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is // high concurrency, in which case DB will abort the query which has done less work to resolve deadlock. // We will try up to 3 times before giving up. for ( $count = 0; $count < 3; $count++ ) { $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. if ( false !== $result ) { break; } } return $result > 0 ? $coupon_used_by_meta_key : $result; } /** * Generate query to calculate tentative usages for the coupon by the user. * * @param int $coupon_id Coupon ID. * @param array $user_aliases List of user aliases to check for usages. * * @return string Tentative usages query. */ private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) { global $wpdb; $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); // Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query. return $wpdb->prepare( " SELECT COUNT( meta_id ) FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key like %s AND {$wpdb->postmeta}.meta_key > %s AND {$wpdb->postmeta}.post_id = %d AND {$wpdb->postmeta}.meta_value IN ('$format') FOR UPDATE ", array_merge( array( '_maybe_used_by_%', '_maybe_used_by_' . time(), $coupon_id, ), $user_aliases ) ); // WPCS: unprepared SQL ok. } /** * Return a coupon code for a specific ID. * * @since 3.0.0 * @param int $id Coupon ID. * @return string Coupon Code */ public function get_code_by_id( $id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE ID = %d AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) ); } /** * Return an array of IDs for for a specific coupon code. * Can return multiple to check for existence. * * @since 3.0.0 * @param string $code Coupon code. * @return array Array of IDs. */ public function get_ids_by_code( $code ) { global $wpdb; return $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC", wc_sanitize_coupon_code( $code ) ) ); } } includes/data-stores/class-wc-customer-download-data-store.php 0000644 00000034560 15132754524 0020552 0 ustar 00 <?php /** * WC_Customer_Download_Data_Store class file. * * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Customer Download Data Store. * * @version 3.0.0 */ class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store_Interface { /** * Names of the database fields for the download permissions table. */ const DOWNLOAD_PERMISSION_DB_FIELDS = array( 'download_id', 'product_id', 'user_id', 'user_email', 'order_id', 'order_key', 'downloads_remaining', 'access_granted', 'download_count', 'access_expires', ); /** * Create download permission for a user, from an array of data. * * @param array $data Data to create the permission for. * @returns int The database id of the created permission, or false if the permission creation failed. */ public function create_from_data( $data ) { $data = array_intersect_key( $data, array_flip( self::DOWNLOAD_PERMISSION_DB_FIELDS ) ); $id = $this->insert_new_download_permission( $data ); do_action( 'woocommerce_grant_product_download_access', $data ); return $id; } /** * Create download permission for a user. * * @param WC_Customer_Download $download WC_Customer_Download object. */ public function create( &$download ) { global $wpdb; // Always set a access granted date. if ( is_null( $download->get_access_granted( 'edit' ) ) ) { $download->set_access_granted( time() ); } $data = array(); foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) { $value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' ); $data[ $db_field_name ] = $value; } $inserted_id = $this->insert_new_download_permission( $data ); if ( $inserted_id ) { $download->set_id( $inserted_id ); $download->apply_changes(); } do_action( 'woocommerce_grant_product_download_access', $data ); } /** * Create download permission for a user, from an array of data. * Assumes that all the keys in the passed data are valid. * * @param array $data Data to create the permission for. * @return int The database id of the created permission, or false if the permission creation failed. */ private function insert_new_download_permission( $data ) { global $wpdb; // Always set a access granted date. if ( ! isset( $data['access_granted'] ) ) { $data['access_granted'] = time(); } $data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] ); if ( isset( $data['access_expires'] ) ) { $data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] ); } $format = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', ); $result = $wpdb->insert( $wpdb->prefix . 'woocommerce_downloadable_product_permissions', apply_filters( 'woocommerce_downloadable_file_permission_data', $data ), apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data ) ); return $result ? $wpdb->insert_id : false; } /** * Adjust a date value to be inserted in the database. * * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes. * @return string The date converted to 'Y-m-d' format. * @throws Exception The passed value can't be converted to a date. */ private function adjust_date_for_db( $date ) { if ( 'WC_DateTime' === get_class( $date ) ) { $date = $date->getTimestamp(); } $adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date if ( $adjusted_date ) { return $adjusted_date; } $msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) ); throw new Exception( $msg ); } /** * Method to read a download permission from the database. * * @param WC_Customer_Download $download WC_Customer_Download object. * * @throws Exception Throw exception if invalid download is passed. */ public function read( &$download ) { global $wpdb; if ( ! $download->get_id() ) { throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); } $download->set_defaults(); $raw_download = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $download->get_id() ) ); if ( ! $raw_download ) { throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); } $download->set_props( array( 'download_id' => $raw_download->download_id, 'product_id' => $raw_download->product_id, 'user_id' => $raw_download->user_id, 'user_email' => $raw_download->user_email, 'order_id' => $raw_download->order_id, 'order_key' => $raw_download->order_key, 'downloads_remaining' => $raw_download->downloads_remaining, 'access_granted' => strtotime( $raw_download->access_granted ), 'download_count' => $raw_download->download_count, 'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ), ) ); $download->set_object_read( true ); } /** * Method to update a download in the database. * * @param WC_Customer_Download $download WC_Customer_Download object. */ public function update( &$download ) { global $wpdb; $data = array( 'download_id' => $download->get_download_id( 'edit' ), 'product_id' => $download->get_product_id( 'edit' ), 'user_id' => $download->get_user_id( 'edit' ), 'user_email' => $download->get_user_email( 'edit' ), 'order_id' => $download->get_order_id( 'edit' ), 'order_key' => $download->get_order_key( 'edit' ), 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), 'download_count' => $download->get_download_count( 'edit' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, ); $format = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', ); $wpdb->update( $wpdb->prefix . 'woocommerce_downloadable_product_permissions', $data, array( 'permission_id' => $download->get_id(), ), $format ); $download->apply_changes(); } /** * Method to delete a download permission from the database. * * @param WC_Customer_Download $download WC_Customer_Download object. * @param array $args Array of args to pass to the delete method. */ public function delete( &$download, $args = array() ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $download->get_id() ) ); $download->set_id( 0 ); } /** * Method to delete a download permission from the database by ID. * * @param int $id permission_id of the download to be deleted. */ public function delete_by_id( $id ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) ); } /** * Method to delete a download permission from the database by order ID. * * @param int $id Order ID of the downloads that will be deleted. */ public function delete_by_order_id( $id ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d", $id ) ); } /** * Method to delete a download permission from the database by download ID. * * @param int $id download_id of the downloads that will be deleted. */ public function delete_by_download_id( $id ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE download_id = %s", $id ) ); } /** * Method to delete a download permission from the database by user ID. * * @since 3.4.0 * @param int $id user ID of the downloads that will be deleted. * @return bool True if deleted rows. */ public function delete_by_user_id( $id ) { global $wpdb; return (bool) $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_id = %d", $id ) ); } /** * Method to delete a download permission from the database by user email. * * @since 3.4.0 * @param string $email email of the downloads that will be deleted. * @return bool True if deleted rows. */ public function delete_by_user_email( $email ) { global $wpdb; return (bool) $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s", $email ) ); } /** * Get a download object. * * @param array $data From the DB. * @return WC_Customer_Download */ private function get_download( $data ) { return new WC_Customer_Download( $data ); } /** * Get array of download ids by specified args. * * @param array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separeted list of fields (for example: 'order_id,user_id,user_email'). * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return']. */ public function get_downloads( $args = array() ) { global $wpdb; $args = wp_parse_args( $args, array( 'user_email' => '', 'user_id' => '', 'order_id' => '', 'order_key' => '', 'product_id' => '', 'download_id' => '', 'orderby' => 'permission_id', 'order' => 'ASC', 'limit' => -1, 'page' => 1, 'return' => 'objects', ) ); $valid_fields = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' ); $get_results_output = ARRAY_A; if ( 'ids' === $args['return'] ) { $fields = 'permission_id'; } elseif ( 'objects' === $args['return'] ) { $fields = '*'; $get_results_output = OBJECT; } else { $fields = explode( ',', (string) $args['return'] ); $fields = implode( ', ', array_intersect( $fields, $valid_fields ) ); } $query = array(); $query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1"; if ( $args['user_email'] ) { $query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) ); } if ( $args['user_id'] ) { $query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) ); } if ( $args['order_id'] ) { $query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] ); } if ( $args['order_key'] ) { $query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] ); } if ( $args['product_id'] ) { $query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] ); } if ( $args['download_id'] ) { $query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] ); } $orderby = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id'; $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); $query[] = "ORDER BY {$orderby_sql}"; if ( 0 < $args['limit'] ) { $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( implode( ' ', $query ), $get_results_output ); switch ( $args['return'] ) { case 'ids': return wp_list_pluck( $results, 'permission_id' ); case 'objects': return array_map( array( $this, 'get_download' ), $results ); default: return $results; } } /** * Update download ids if the hash changes. * * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash. * * @param int $product_id Product ID. * @param string $old_id Old download_id. * @param string $new_id New download_id. */ public function update_download_id( $product_id, $old_id, $new_id ) { global $wpdb; wc_deprecated_function( __METHOD__, '3.3' ); $wpdb->update( $wpdb->prefix . 'woocommerce_downloadable_product_permissions', array( 'download_id' => $new_id, ), array( 'download_id' => $old_id, 'product_id' => $product_id, ) ); } /** * Get a customers downloads. * * @param int $customer_id Customer ID. * @return array */ public function get_downloads_for_customer( $customer_id ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions WHERE user_id = %d AND permissions.order_id > 0 AND ( permissions.downloads_remaining > 0 OR permissions.downloads_remaining = '' ) AND ( permissions.access_expires IS NULL OR permissions.access_expires >= %s OR permissions.access_expires = '0000-00-00 00:00:00' ) ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;", $customer_id, date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date ) ); } /** * Update user prop for downloads based on order id. * * @param int $order_id Order ID. * @param int $customer_id Customer ID. * @param string $email Customer email address. */ public function update_user_by_order_id( $order_id, $customer_id, $email ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_downloadable_product_permissions', array( 'user_id' => $customer_id, 'user_email' => $email, ), array( 'order_id' => $order_id, ), array( '%d', '%s', ), array( '%d', ) ); } } includes/data-stores/class-wc-product-variable-data-store-cpt.php 0000644 00000057124 15132754524 0021134 0 ustar 00 <?php /** * File for WC Variable Product Data Store class. * * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC Variable Product Data Store: Stored in CPT. * * @version 3.0.0 */ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface { /** * Cached & hashed prices array for child variations. * * @var array */ protected $prices_array = array(); /** * Read attributes from post meta. * * @param WC_Product $product Product object. */ protected function read_attributes( &$product ) { $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { $attributes = array(); $force_update = false; foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { $meta_value = array_merge( array( 'name' => '', 'value' => '', 'position' => 0, 'is_visible' => 0, 'is_variation' => 0, 'is_taxonomy' => 0, ), (array) $meta_attribute_value ); // Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly. if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) { global $wpdb; $old_slug = 'attribute_' . $meta_attribute_key; $new_slug = 'attribute_' . sanitize_title( $meta_value['name'] ); $old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok. if ( $old_meta_rows ) { foreach ( $old_meta_rows as $old_meta_row ) { update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value ); } } $force_update = true; } // Check if is a taxonomy attribute. if ( ! empty( $meta_value['is_taxonomy'] ) ) { if ( ! taxonomy_exists( $meta_value['name'] ) ) { continue; } $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); } else { $id = 0; $options = wc_get_text_attributes( $meta_value['value'] ); } $attribute = new WC_Product_Attribute(); $attribute->set_id( $id ); $attribute->set_name( $meta_value['name'] ); $attribute->set_options( $options ); $attribute->set_position( $meta_value['position'] ); $attribute->set_visible( $meta_value['is_visible'] ); $attribute->set_variation( $meta_value['is_variation'] ); $attributes[] = $attribute; } $product->set_attributes( $attributes ); if ( $force_update ) { $this->update_attributes( $product, true ); } } } /** * Read product data. * * @param WC_Product $product Product object. * * @since 3.0.0 */ protected function read_product_data( &$product ) { parent::read_product_data( $product ); // Make sure data which does not apply to variables is unset. $product->set_regular_price( '' ); $product->set_sale_price( '' ); } /** * Loads variation child IDs. * * @param WC_Product $product Product object. * @param bool $force_read True to bypass the transient. * * @return array */ public function read_children( &$product, $force_read = false ) { $children_transient_name = 'wc_product_children_' . $product->get_id(); $children = get_transient( $children_transient_name ); if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { $all_args = array( 'post_parent' => $product->get_id(), 'post_type' => 'product_variation', 'orderby' => array( 'menu_order' => 'ASC', 'ID' => 'ASC', ), 'fields' => 'ids', 'post_status' => array( 'publish', 'private' ), 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts ); $visible_only_args = $all_args; $visible_only_args['post_status'] = 'publish'; if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $visible_only_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'outofstock', 'operator' => 'NOT IN', ); } $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) ); $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) ); set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 ); } $children['all'] = wp_parse_id_list( (array) $children['all'] ); $children['visible'] = wp_parse_id_list( (array) $children['visible'] ); return $children; } /** * Loads an array of attributes used for variations, as well as their possible values. * * @param WC_Product $product Product object. * * @return array */ public function read_variation_attributes( &$product ) { global $wpdb; $variation_attributes = array(); $attributes = $product->get_attributes(); $child_ids = $product->get_children(); $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id(); $cache_group = 'products'; $cached_data = wp_cache_get( $cache_key, $cache_group ); if ( false !== $cached_data ) { return $cached_data; } if ( ! empty( $attributes ) ) { foreach ( $attributes as $attribute ) { if ( empty( $attribute['is_variation'] ) ) { continue; } // Get possible values for this attribute, for only visible variations. if ( ! empty( $child_ids ) ) { $format = array_fill( 0, count( $child_ids ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; $query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids; $values = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine. $query_args ) ) ); } else { $values = array(); } // Empty value indicates that all options for given attribute are available. if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) { $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] ); // Get custom attributes (non taxonomy) as defined. } elseif ( ! $attribute['is_taxonomy'] ) { $text_attributes = wc_get_text_attributes( $attribute['value'] ); $assigned_text_attributes = $values; $values = array(); // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes ); foreach ( $text_attributes as $text_attribute ) { if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) { $values[] = $text_attribute; } } } else { foreach ( $text_attributes as $text_attribute ) { if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) { $values[] = $text_attribute; } } } } $variation_attributes[ $attribute['name'] ] = array_unique( $values ); } } wp_cache_set( $cache_key, $variation_attributes, $cache_group ); return $variation_attributes; } /** * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. * * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter. * This is to ensure modified prices are not cached, unless intended. * * @param WC_Product $product Product object. * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). * * @return array of prices * @since 3.0.0 */ public function read_price_data( &$product, $for_display = false ) { /** * Transient name for storing prices for this product (note: Max transient length is 45) * * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product. */ $transient_name = 'wc_var_prices_' . $product->get_id(); $transient_version = WC_Cache_Helper::get_transient_version( 'product' ); $price_hash = $this->get_price_hash( $product, $for_display ); // Check if prices array is stale. if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) { $this->prices_array = array( 'version' => $transient_version, ); } /** * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array. * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered. */ if ( empty( $this->prices_array[ $price_hash ] ) ) { $transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) ); // If the product version has changed since the transient was last saved, reset the transient cache. if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) { $transient_cached_prices_array = array( 'version' => $transient_version, ); } // If the prices are not stored for this hash, generate them and add to the transient. if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) { $prices_array = array( 'price' => array(), 'regular_price' => array(), 'sale_price' => array(), ); $variation_ids = $product->get_visible_children(); if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $variation_ids ); } foreach ( $variation_ids as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation ) { $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); // Skip empty prices. if ( '' === $price ) { continue; } // If sale price does not equal price, the product is not yet on sale. if ( $sale_price === $regular_price || $sale_price !== $price ) { $sale_price = $regular_price; } // If we are getting prices for display, we need to account for taxes. if ( $for_display ) { if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { $price = '' === $price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $price, ) ); $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $regular_price, ) ); $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $sale_price, ) ); } else { $price = '' === $price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $price, ) ); $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $regular_price, ) ); $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $sale_price, ) ); } } $prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() ); $prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() ); $prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() ); $prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display ); } } // Add all pricing data to the transient array. foreach ( $prices_array as $key => $values ) { $transient_cached_prices_array[ $price_hash ][ $key ] = $values; } set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 ); } /** * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class. * This value may differ from the transient cache. It is filtered once before storing locally. */ $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display ); } return $this->prices_array[ $price_hash ]; } /** * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters. * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique. * * @param WC_Product $product Product object. * @param bool $for_display If taxes should be calculated or not. * * @since 3.0.0 * @return string */ protected function get_price_hash( &$product, $for_display = false ) { global $wp_filter; $price_hash = array( false ); if ( $for_display && wc_tax_enabled() ) { $price_hash = array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates(), empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(), ); } $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' ); foreach ( $filter_names as $filter_name ) { if ( ! empty( $wp_filter[ $filter_name ] ) ) { $price_hash[ $filter_name ] = array(); foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) { $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) ); } } } return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) ); } /** * Does a child have a weight set? * * @param WC_Product $product Product object. * * @since 3.0.0 * @return boolean */ public function child_has_weight( $product ) { global $wpdb; $children = $product->get_visible_children(); if ( ! $children ) { return false; } $format = array_fill( 0, count( $children ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. } /** * Does a child have dimensions set? * * @param WC_Product $product Product object. * * @since 3.0.0 * @return boolean */ public function child_has_dimensions( $product ) { global $wpdb; $children = $product->get_visible_children(); if ( ! $children ) { return false; } $format = array_fill( 0, count( $children ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. } /** * Is a child in stock? * * @param WC_Product $product Product object. * * @since 3.0.0 * @return boolean */ public function child_is_in_stock( $product ) { return $this->child_has_stock_status( $product, 'instock' ); } /** * Does a child have a stock status? * * @param WC_Product $product Product object. * @param string $status 'instock', 'outofstock', or 'onbackorder'. * * @since 3.3.0 * @return boolean */ public function child_has_stock_status( $product, $status ) { global $wpdb; $children = $product->get_children(); if ( $children ) { $format = array_fill( 0, count( $children ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; $query_args = array( 'stock_status' => $status ) + $children; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) { $query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}"; } else { $query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}"; } $children_with_status = $wpdb->get_var( $wpdb->prepare( $query, $query_args ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } else { $children_with_status = 0; } return (bool) $children_with_status; } /** * Syncs all variation names if the parent name is changed. * * @param WC_Product $product Product object. * @param string $previous_name Variation previous name. * @param string $new_name Variation new name. * * @since 3.0.0 */ public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) { if ( $new_name !== $previous_name ) { global $wpdb; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_title = REPLACE( post_title, %s, %s ) WHERE post_type = 'product_variation' AND post_parent = %d", $previous_name ? $previous_name : 'AUTO-DRAFT', $new_name, $product->get_id() ) ); } } /** * Stock managed at the parent level - update children being managed by this product. * This sync function syncs downwards (from parent to child) when the variable product is saved. * * @param WC_Product $product Product object. * * @since 3.0.0 */ public function sync_managed_variation_stock_status( &$product ) { global $wpdb; if ( $product->get_manage_stock() ) { $children = $product->get_children(); $changed = false; if ( $children ) { $status = $product->get_stock_status(); $format = array_fill( 0, count( $children ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; $managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. foreach ( $managed_children as $managed_child ) { if ( update_post_meta( $managed_child, '_stock_status', $status ) ) { $this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' ); $changed = true; } } } if ( $changed ) { $children = $this->read_children( $product, true ); $product->set_children( $children['all'] ); $product->set_visible_children( $children['visible'] ); } } } /** * Sync variable product prices with children. * * @param WC_Product $product Product object. * * @since 3.0.0 */ public function sync_price( &$product ) { global $wpdb; $children = $product->get_visible_children(); if ( $children ) { $format = array_fill( 0, count( $children ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; $prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. } else { $prices = array(); } delete_post_meta( $product->get_id(), '_price' ); delete_post_meta( $product->get_id(), '_sale_price' ); delete_post_meta( $product->get_id(), '_regular_price' ); if ( $prices ) { sort( $prices, SORT_NUMERIC ); // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. foreach ( $prices as $price ) { if ( is_null( $price ) || '' === $price ) { continue; } add_post_meta( $product->get_id(), '_price', $price, false ); } } $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); /** * Fire an action for this direct update so it can be detected by other code. * * @since 3.6 * @param int $product_id Product ID that was updated directly. */ do_action( 'woocommerce_updated_product_price', $product->get_id() ); } /** * Sync variable product stock status with children. * Change does not persist unless saved by caller. * * @param WC_Product $product Product object. * * @since 3.0.0 */ public function sync_stock_status( &$product ) { if ( $product->child_is_in_stock() ) { $product->set_stock_status( 'instock' ); } elseif ( $product->child_is_on_backorder() ) { $product->set_stock_status( 'onbackorder' ); } else { $product->set_stock_status( 'outofstock' ); } } /** * Delete variations of a product. * * @param int $product_id Product ID. * @param bool $force_delete False to trash. * * @since 3.0.0 */ public function delete_variations( $product_id, $force_delete = false ) { if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { return; } $variation_ids = wp_parse_id_list( get_posts( array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'fields' => 'ids', 'post_status' => array( 'any', 'trash', 'auto-draft' ), 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts ) ) ); if ( ! empty( $variation_ids ) ) { foreach ( $variation_ids as $variation_id ) { if ( $force_delete ) { do_action( 'woocommerce_before_delete_product_variation', $variation_id ); wp_delete_post( $variation_id, true ); do_action( 'woocommerce_delete_product_variation', $variation_id ); } else { wp_trash_post( $variation_id ); do_action( 'woocommerce_trash_product_variation', $variation_id ); } } } delete_transient( 'wc_product_children_' . $product_id ); } /** * Untrash variations. * * @param int $product_id Product ID. */ public function untrash_variations( $product_id ) { $variation_ids = wp_parse_id_list( get_posts( array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'fields' => 'ids', 'post_status' => 'trash', 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts ) ) ); if ( ! empty( $variation_ids ) ) { foreach ( $variation_ids as $variation_id ) { wp_untrash_post( $variation_id ); } } delete_transient( 'wc_product_children_' . $product_id ); } } includes/legacy/abstract-wc-legacy-payment-token.php 0000644 00000003566 15132754524 0016616 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Payment Tokens. * Payment Tokens were introduced in 2.6.0 with create and update as methods. * Major CRUD changes occurred in 3.0, so these were deprecated (save and delete still work). * This legacy class is for backwards compatibility in case any code called ->read, ->update or ->create * directly on the object. * * @version 3.0.0 * @package WooCommerce\Classes * @category Class * @author WooCommerce */ abstract class WC_Legacy_Payment_Token extends WC_Data { /** * Sets the type of this payment token (CC, eCheck, or something else). * * @param string Payment Token Type (CC, eCheck) */ public function set_type( $type ) { wc_deprecated_function( 'WC_Payment_Token::set_type', '3.0.0', 'Type cannot be overwritten.' ); } /** * Read a token by ID. * @deprecated 3.0.0 - Init a token class with an ID. * * @param int $token_id */ public function read( $token_id ) { wc_deprecated_function( 'WC_Payment_Token::read', '3.0.0', 'a new token class initialized with an ID.' ); $this->set_id( $token_id ); $data_store = WC_Data_Store::load( 'payment-token' ); $data_store->read( $this ); } /** * Update a token. * @deprecated 3.0.0 - Use ::save instead. */ public function update() { wc_deprecated_function( 'WC_Payment_Token::update', '3.0.0', 'WC_Payment_Token::save instead.' ); $data_store = WC_Data_Store::load( 'payment-token' ); try { $data_store->update( $this ); } catch ( Exception $e ) { return false; } } /** * Create a token. * @deprecated 3.0.0 - Use ::save instead. */ public function create() { wc_deprecated_function( 'WC_Payment_Token::create', '3.0.0', 'WC_Payment_Token::save instead.' ); $data_store = WC_Data_Store::load( 'payment-token' ); try { $data_store->create( $this ); } catch ( Exception $e ) { return false; } } } includes/legacy/class-wc-legacy-customer.php 0000644 00000015663 15132754524 0015167 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Customer. * * @version 3.0.0 * @package WooCommerce\Classes * @category Class * @author WooThemes */ abstract class WC_Legacy_Customer extends WC_Data { /** * __isset legacy. * @param mixed $key * @return bool */ public function __isset( $key ) { $legacy_keys = array( 'id', 'country', 'state', 'postcode', 'city', 'address_1', 'address', 'address_2', 'shipping_country', 'shipping_state', 'shipping_postcode', 'shipping_city', 'shipping_address_1', 'shipping_address', 'shipping_address_2', 'is_vat_exempt', 'calculated_shipping', ); $key = $this->filter_legacy_key( $key ); return in_array( $key, $legacy_keys ); } /** * __get function. * @param string $key * @return string */ public function __get( $key ) { wc_doing_it_wrong( $key, 'Customer properties should not be accessed directly.', '3.0' ); $key = $this->filter_legacy_key( $key ); if ( in_array( $key, array( 'country', 'state', 'postcode', 'city', 'address_1', 'address', 'address_2' ) ) ) { $key = 'billing_' . $key; } return is_callable( array( $this, "get_{$key}" ) ) ? $this->{"get_{$key}"}() : ''; } /** * __set function. * * @param string $key * @param mixed $value */ public function __set( $key, $value ) { wc_doing_it_wrong( $key, 'Customer properties should not be set directly.', '3.0' ); $key = $this->filter_legacy_key( $key ); if ( is_callable( array( $this, "set_{$key}" ) ) ) { $this->{"set_{$key}"}( $value ); } } /** * Address and shipping_address are aliased, so we want to get the 'real' key name. * For all other keys, we can just return it. * @since 3.0.0 * @param string $key * @return string */ private function filter_legacy_key( $key ) { if ( 'address' === $key ) { $key = 'address_1'; } if ( 'shipping_address' === $key ) { $key = 'shipping_address_1'; } return $key; } /** * Sets session data for the location. * * @param string $country * @param string $state * @param string $postcode (default: '') * @param string $city (default: '') */ public function set_location( $country, $state, $postcode = '', $city = '' ) { $this->set_billing_location( $country, $state, $postcode, $city ); $this->set_shipping_location( $country, $state, $postcode, $city ); } /** * Get default country for a customer. * @return string */ public function get_default_country() { wc_deprecated_function( 'WC_Customer::get_default_country', '3.0', 'wc_get_customer_default_location' ); $default = wc_get_customer_default_location(); return $default['country']; } /** * Get default state for a customer. * @return string */ public function get_default_state() { wc_deprecated_function( 'WC_Customer::get_default_state', '3.0', 'wc_get_customer_default_location' ); $default = wc_get_customer_default_location(); return $default['state']; } /** * Set customer address to match shop base address. */ public function set_to_base() { wc_deprecated_function( 'WC_Customer::set_to_base', '3.0', 'WC_Customer::set_billing_address_to_base' ); $this->set_billing_address_to_base(); } /** * Set customer shipping address to base address. */ public function set_shipping_to_base() { wc_deprecated_function( 'WC_Customer::set_shipping_to_base', '3.0', 'WC_Customer::set_shipping_address_to_base' ); $this->set_shipping_address_to_base(); } /** * Calculated shipping. * @param boolean $calculated */ public function calculated_shipping( $calculated = true ) { wc_deprecated_function( 'WC_Customer::calculated_shipping', '3.0', 'WC_Customer::set_calculated_shipping' ); $this->set_calculated_shipping( $calculated ); } /** * Set default data for a customer. */ public function set_default_data() { wc_deprecated_function( 'WC_Customer::set_default_data', '3.0' ); } /** * Save data function. */ public function save_data() { $this->save(); } /** * Is the user a paying customer? * * @param int $user_id * * @return bool */ function is_paying_customer( $user_id = '' ) { wc_deprecated_function( 'WC_Customer::is_paying_customer', '3.0', 'WC_Customer::get_is_paying_customer' ); if ( ! empty( $user_id ) ) { $user_id = get_current_user_id(); } return '1' === get_user_meta( $user_id, 'paying_customer', true ); } /** * Legacy get address. */ function get_address() { wc_deprecated_function( 'WC_Customer::get_address', '3.0', 'WC_Customer::get_billing_address_1' ); return $this->get_billing_address_1(); } /** * Legacy get address 2. */ function get_address_2() { wc_deprecated_function( 'WC_Customer::get_address_2', '3.0', 'WC_Customer::get_billing_address_2' ); return $this->get_billing_address_2(); } /** * Legacy get country. */ function get_country() { wc_deprecated_function( 'WC_Customer::get_country', '3.0', 'WC_Customer::get_billing_country' ); return $this->get_billing_country(); } /** * Legacy get state. */ function get_state() { wc_deprecated_function( 'WC_Customer::get_state', '3.0', 'WC_Customer::get_billing_state' ); return $this->get_billing_state(); } /** * Legacy get postcode. */ function get_postcode() { wc_deprecated_function( 'WC_Customer::get_postcode', '3.0', 'WC_Customer::get_billing_postcode' ); return $this->get_billing_postcode(); } /** * Legacy get city. */ function get_city() { wc_deprecated_function( 'WC_Customer::get_city', '3.0', 'WC_Customer::get_billing_city' ); return $this->get_billing_city(); } /** * Legacy set country. * * @param string $country */ function set_country( $country ) { wc_deprecated_function( 'WC_Customer::set_country', '3.0', 'WC_Customer::set_billing_country' ); $this->set_billing_country( $country ); } /** * Legacy set state. * * @param string $state */ function set_state( $state ) { wc_deprecated_function( 'WC_Customer::set_state', '3.0', 'WC_Customer::set_billing_state' ); $this->set_billing_state( $state ); } /** * Legacy set postcode. * * @param string $postcode */ function set_postcode( $postcode ) { wc_deprecated_function( 'WC_Customer::set_postcode', '3.0', 'WC_Customer::set_billing_postcode' ); $this->set_billing_postcode( $postcode ); } /** * Legacy set city. * * @param string $city */ function set_city( $city ) { wc_deprecated_function( 'WC_Customer::set_city', '3.0', 'WC_Customer::set_billing_city' ); $this->set_billing_city( $city ); } /** * Legacy set address. * * @param string $address */ function set_address( $address ) { wc_deprecated_function( 'WC_Customer::set_address', '3.0', 'WC_Customer::set_billing_address' ); $this->set_billing_address( $address ); } /** * Legacy set address. * * @param string $address */ function set_address_2( $address ) { wc_deprecated_function( 'WC_Customer::set_address_2', '3.0', 'WC_Customer::set_billing_address_2' ); $this->set_billing_address_2( $address ); } } includes/legacy/api/v3/class-wc-api-authentication.php 0000644 00000031433 15132754524 0016744 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1.0 * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Authentication { /** * Setup class * * @since 2.1 */ public function __construct() { // To disable authentication, hook into this filter at a later priority and return a valid WP_User add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 ); } /** * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not. * * @since 2.1 * @param WP_User $user * @return null|WP_Error|WP_User */ public function authenticate( $user ) { // Allow access to the index by default if ( '/' === WC()->api->server->path ) { return new WP_User( 0 ); } try { if ( is_ssl() ) { $keys = $this->perform_ssl_authentication(); } else { $keys = $this->perform_oauth_authentication(); } // Check API key-specific permission $this->check_api_key_permissions( $keys['permissions'] ); $user = $this->get_user_by_id( $keys['user_id'] ); $this->update_api_key_last_access( $keys['key_id'] ); } catch ( Exception $e ) { $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); } return $user; } /** * SSL-encrypted requests are not subject to sniffing or man-in-the-middle * attacks, so the request can be authenticated by simply looking up the user * associated with the given consumer key and confirming the consumer secret * provided is valid * * @since 2.1 * @return array * @throws Exception */ private function perform_ssl_authentication() { $params = WC()->api->server->params['GET']; // if the $_GET parameters are present, use those first if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) { $keys = $this->get_keys_by_consumer_key( $params['consumer_key'] ); if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) { throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); } return $keys; } // if the above is not present, we will do full basic auth if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) { $this->exit_with_unauthorized_headers(); } $keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] ); if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) { $this->exit_with_unauthorized_headers(); } return $keys; } /** * If the consumer_key and consumer_secret $_GET parameters are NOT provided * and the Basic auth headers are either not present or the consumer secret does not match the consumer * key provided, then return the correct Basic headers and an error message. * * @since 2.4 */ private function exit_with_unauthorized_headers() { $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' ); header( 'HTTP/1.0 401 Unauthorized' ); throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 ); } /** * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests * * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP * * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: * * 1) There is no token associated with request/responses, only consumer keys/secrets are used * * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, * This is because there is no cross-OS function within PHP to get the raw Authorization header * * @link http://tools.ietf.org/html/rfc5849 for the full spec * @since 2.1 * @return array * @throws Exception */ private function perform_oauth_authentication() { $params = WC()->api->server->params['GET']; $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); // Check for required OAuth parameters foreach ( $param_names as $param_name ) { if ( empty( $params[ $param_name ] ) ) { throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); } } // Fetch WP user by consumer key $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); // Perform OAuth validation $this->check_oauth_signature( $keys, $params ); $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); // Authentication successful, return user return $keys; } /** * Return the keys for the given consumer key * * @since 2.4.0 * @param string $consumer_key * @return array * @throws Exception */ private function get_keys_by_consumer_key( $consumer_key ) { global $wpdb; $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); $keys = $wpdb->get_row( $wpdb->prepare( " SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = '%s' ", $consumer_key ), ARRAY_A ); if ( empty( $keys ) ) { throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); } return $keys; } /** * Get user by ID * * @since 2.4.0 * * @param int $user_id * * @return WP_User * @throws Exception */ private function get_user_by_id( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user ) { throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); } return $user; } /** * Check if the consumer secret provided for the given user is valid * * @since 2.1 * @param string $keys_consumer_secret * @param string $consumer_secret * @return bool */ private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { return hash_equals( $keys_consumer_secret, $consumer_secret ); } /** * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer * has a valid key/secret * * @param array $keys * @param array $params the request parameters * @throws Exception */ private function check_oauth_signature( $keys, $params ) { $http_method = strtoupper( WC()->api->server->method ); $server_path = WC()->api->server->path; // if the requested URL has a trailingslash, make sure our base URL does as well if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) { $server_path .= '/'; } $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path ); // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); unset( $params['oauth_signature'] ); // Sort parameters if ( ! uksort( $params, 'strcmp' ) ) { throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); } // Normalize parameter key/values $params = $this->normalize_parameters( $params ); $query_parameters = array(); foreach ( $params as $param_key => $param_value ) { if ( is_array( $param_value ) ) { foreach ( $param_value as $param_key_inner => $param_value_inner ) { $query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner; } } else { $query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign } } $query_string = implode( '%26', $query_parameters ); // join with ampersand $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); $secret = $keys['consumer_secret'] . '&'; $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); } } /** * Normalize each parameter by assuming each parameter may have already been * encoded, so attempt to decode, and then re-encode according to RFC 3986 * * Note both the key and value is normalized so a filter param like: * * 'filter[period]' => 'week' * * is encoded to: * * 'filter%5Bperiod%5D' => 'week' * * This conforms to the OAuth 1.0a spec which indicates the entire query string * should be URL encoded * * @since 2.1 * @see rawurlencode() * @param array $parameters un-normalized parameters * @return array normalized parameters */ private function normalize_parameters( $parameters ) { $keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) ); $values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) ); $parameters = array_combine( $keys, $values ); return $parameters; } /** * Encodes a value according to RFC 3986. Supports multidimensional arrays. * * @since 2.4 * @param string|array $value The value to encode * @return string|array Encoded values */ public static function urlencode_rfc3986( $value ) { if ( is_array( $value ) ) { return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value ); } else { // Percent symbols (%) must be double-encoded return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); } } /** * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where * an attacker could attempt to re-send an intercepted request at a later time. * * - A timestamp is valid if it is within 15 minutes of now * - A nonce is valid if it has not been used within the last 15 minutes * * @param array $keys * @param int $timestamp the unix timestamp for when the request was made * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated * @throws Exception */ private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { global $wpdb; $valid_window = 15 * 60; // 15 minute window if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 ); } $used_nonces = maybe_unserialize( $keys['nonces'] ); if ( empty( $used_nonces ) ) { $used_nonces = array(); } if ( in_array( $nonce, $used_nonces ) ) { throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); } $used_nonces[ $timestamp ] = $nonce; // Remove expired nonces foreach ( $used_nonces as $nonce_timestamp => $nonce ) { if ( $nonce_timestamp < ( time() - $valid_window ) ) { unset( $used_nonces[ $nonce_timestamp ] ); } } $used_nonces = maybe_serialize( $used_nonces ); $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'nonces' => $used_nonces ), array( 'key_id' => $keys['key_id'] ), array( '%s' ), array( '%d' ) ); } /** * Check that the API keys provided have the proper key-specific permissions to either read or write API resources * * @param string $key_permissions * @throws Exception if the permission check fails */ public function check_api_key_permissions( $key_permissions ) { switch ( WC()->api->server->method ) { case 'HEAD': case 'GET': if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); } break; case 'POST': case 'PUT': case 'PATCH': case 'DELETE': if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); } break; } } /** * Updated API Key last access datetime * * @since 2.4.0 * * @param int $key_id */ private function update_api_key_last_access( $key_id ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'last_access' => current_time( 'mysql' ) ), array( 'key_id' => $key_id ), array( '%s' ), array( '%d' ) ); } } includes/legacy/api/v3/class-wc-api-reports.php 0000644 00000023064 15132754524 0015424 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Reports extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/reports'; /** @var WC_Admin_Report instance */ private $report; /** * Register the routes for this class * * GET /reports * GET /reports/sales * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /reports $routes[ $this->base ] = array( array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), ); # GET /reports/sales $routes[ $this->base . '/sales' ] = array( array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), ); # GET /reports/sales/top_sellers $routes[ $this->base . '/sales/top_sellers' ] = array( array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get a simple listing of available reports * * @since 2.1 * @return array */ public function get_reports() { return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); } /** * Get the sales report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_sales_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); // check for WP_Error if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); // new customers $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), 'role' => 'customer', ) ); $customers = $users_query->get_results(); foreach ( $customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { unset( $customers[ $key ] ); } } $total_customers = count( $customers ); $report_data = $this->report->get_report_data(); $period_totals = array(); // setup period totals by ensuring each period in the interval has data for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { switch ( $this->report->chart_groupby ) { case 'day' : $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); break; default : $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); break; } // set the customer signups for each period $customer_count = 0; foreach ( $customers as $customer ) { if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { $customer_count++; } } $period_totals[ $time ] = array( 'sales' => wc_format_decimal( 0.00, 2 ), 'orders' => 0, 'items' => 0, 'tax' => wc_format_decimal( 0.00, 2 ), 'shipping' => wc_format_decimal( 0.00, 2 ), 'discount' => wc_format_decimal( 0.00, 2 ), 'customers' => $customer_count, ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $report_data->orders as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); } foreach ( $report_data->order_counts as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['orders'] = (int) $order->count; } // add total order items for each period foreach ( $report_data->order_items as $order_item ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // add total discount for each period foreach ( $report_data->coupons as $discount ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } $sales_data = array( 'total_sales' => $report_data->total_sales, 'net_sales' => $report_data->net_sales, 'average_sales' => $report_data->average_sales, 'total_orders' => $report_data->total_orders, 'total_items' => $report_data->total_items, 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), 'total_shipping' => $report_data->total_shipping, 'total_refunds' => $report_data->total_refunds, 'total_discount' => $report_data->total_coupons, 'totals_grouped_by' => $this->report->chart_groupby, 'totals' => $period_totals, 'total_customers' => $total_customers, ); return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); } /** * Get the top sellers report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_top_sellers_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); $top_sellers = $this->report->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); $top_sellers_data = array(); foreach ( $top_sellers as $top_seller ) { $product = wc_get_product( $top_seller->product_id ); if ( $product ) { $top_sellers_data[] = array( 'title' => $product->get_name(), 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->order_item_qty, ); } } return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); } /** * Setup the report object and parse any date filtering * * @since 2.1 * @param array $filter date filtering */ private function setup_report( $filter ) { include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); $this->report = new WC_Report_Sales_By_Date(); if ( empty( $filter['period'] ) ) { // custom date range $filter['period'] = 'custom'; if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; } else { // default custom range to today $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { // ensure period is valid if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { $filter['period'] = 'week'; } // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods // allow "week" for period instead of "7day" if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; } } $this->report->calculate_current_range( $filter['period'] ); } /** * Verify that the current user has permission to view reports * * @since 2.1 * @see WC_API_Resource::validate_request() * * @param null $id unused * @param null $type unused * @param null $context unused * * @return true|WP_Error */ protected function validate_request( $id = null, $type = null, $context = null ) { if ( current_user_can( 'view_woocommerce_reports' ) ) { return true; } return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); } } includes/legacy/api/v3/class-wc-api-resource.php 0000644 00000033363 15132754524 0015560 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Resource { /** @var WC_API_Server the API server */ protected $server; /** @var string sub-classes override this to set a resource-specific base route */ protected $base; /** * Setup class * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { $this->server = $server; // automatically register routes for sub-classes add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); // maybe add meta to top-level resource responses foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); } $response_names = array( 'order', 'coupon', 'customer', 'product', 'report', 'customer_orders', 'customer_downloads', 'order_note', 'order_refund', 'product_reviews', 'product_category', 'tax', 'tax_class', ); foreach ( $response_names as $name ) { /** * Remove fields from responses when requests specify certain fields * note these are hooked at a later priority so data added via * filters (e.g. customer data to the order response) still has the * fields filtered properly */ add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid post object and matches the provided post type * 3) the current user has the proper permissions to read/edit/delete the post * * @since 2.1 * @param string|int $id the post ID * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid post ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } $id = absint( $id ); // Validate ID if ( empty( $id ) ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); } // Only custom post types have per-post type/permission checks if ( 'customer' !== $type ) { $post = get_post( $id ); if ( null === $post ) { return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); } // For checking permissions, product variations are the same as the product post type $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; // Validate post type if ( $type !== $post_type ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); } // Validate permissions switch ( $context ) { case 'read': if ( ! $this->is_readable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! $this->is_editable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! $this->is_deletable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; } } return $id; } /** * Add common request arguments to argument list before WP_Query is run * * @since 2.1 * @param array $base_args required arguments for the query (e.g. `post_type`, etc) * @param array $request_args arguments provided in the request * @return array */ protected function merge_query_args( $base_args, $request_args ) { $args = array(); // date if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'] = array(); // resources created after specified date if ( ! empty( $request_args['created_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $request_args['created_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); } // resources updated after specified date if ( ! empty( $request_args['updated_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); } // resources updated before specified date if ( ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); } } // search if ( ! empty( $request_args['q'] ) ) { $args['s'] = $request_args['q']; } // resources per response if ( ! empty( $request_args['limit'] ) ) { $args['posts_per_page'] = $request_args['limit']; } // resource offset if ( ! empty( $request_args['offset'] ) ) { $args['offset'] = $request_args['offset']; } // order (ASC or DESC, ASC by default) if ( ! empty( $request_args['order'] ) ) { $args['order'] = $request_args['order']; } // orderby if ( ! empty( $request_args['orderby'] ) ) { $args['orderby'] = $request_args['orderby']; // allow sorting by meta value if ( ! empty( $request_args['orderby_meta_key'] ) ) { $args['meta_key'] = $request_args['orderby_meta_key']; } } // allow post status change if ( ! empty( $request_args['post_status'] ) ) { $args['post_status'] = $request_args['post_status']; unset( $request_args['post_status'] ); } // filter by a list of post id if ( ! empty( $request_args['in'] ) ) { $args['post__in'] = explode( ',', $request_args['in'] ); unset( $request_args['in'] ); } // exclude by a list of post id if ( ! empty( $request_args['not_in'] ) ) { $args['post__not_in'] = explode( ',', $request_args['not_in'] ); unset( $request_args['not_in'] ); } // resource page $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); return array_merge( $base_args, $args ); } /** * Add meta to resources when requested by the client. Meta is added as a top-level * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs * * @since 2.1 * @param array $data the resource data * @param object $resource the resource object (e.g WC_Order) * @return mixed */ public function maybe_add_meta( $data, $resource ) { if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { // don't attempt to add meta more than once if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { return $data; } // define the top-level property name for the meta switch ( get_class( $resource ) ) { case 'WC_Order': $meta_name = 'order_meta'; break; case 'WC_Coupon': $meta_name = 'coupon_meta'; break; case 'WP_User': $meta_name = 'customer_meta'; break; default: $meta_name = 'product_meta'; break; } if ( is_a( $resource, 'WP_User' ) ) { // customer meta $meta = (array) get_user_meta( $resource->ID ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->get_id() ); } foreach ( $meta as $meta_key => $meta_value ) { // don't add hidden meta by default if ( ! is_protected_meta( $meta_key ) ) { $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); } } } return $data; } /** * Restrict the fields included in the response if the request specified certain only certain fields should be returned * * @since 2.1 * @param array $data the response data * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order * @param array|string the requested list of fields to include in the response * @return array response data */ public function filter_response_fields( $data, $resource, $fields ) { if ( ! is_array( $data ) || empty( $fields ) ) { return $data; } $fields = explode( ',', $fields ); $sub_fields = array(); // get sub fields foreach ( $fields as $field ) { if ( false !== strpos( $field, '.' ) ) { list( $name, $value ) = explode( '.', $field ); $sub_fields[ $name ] = $value; } } // iterate through top-level fields foreach ( $data as $data_field => $data_value ) { // if a field has sub-fields and the top-level field has sub-fields to filter if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { // iterate through each sub-field foreach ( $data_value as $sub_field => $sub_field_value ) { // remove non-matching sub-fields if ( ! in_array( $sub_field, $sub_fields ) ) { unset( $data[ $data_field ][ $sub_field ] ); } } } else { // remove non-matching top-level fields if ( ! in_array( $data_field, $fields ) ) { unset( $data[ $data_field ] ); } } } return $data; } /** * Delete a given resource * * @since 2.1 * @param int $id the resource ID * @param string $type the resource post type, or `customer` * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) * @return array|WP_Error */ protected function delete( $id, $type, $force = false ) { if ( 'shop_order' === $type || 'shop_coupon' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } if ( 'customer' === $type ) { $result = wp_delete_user( $id ); if ( $result ) { return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); } else { return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); } } else { // delete order/coupon/product/webhook $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); if ( ! $result ) { return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); } if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); } } } /** * Checks if the given post is readable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_readable( $post ) { return $this->check_permission( $post, 'read' ); } /** * Checks if the given post is editable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_editable( $post ) { return $this->check_permission( $post, 'edit' ); } /** * Checks if the given post is deletable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_deletable( $post ) { return $this->check_permission( $post, 'delete' ); } /** * Checks the permissions for the current user given a post and context * * @since 2.1 * @param WP_Post|int $post * @param string $context the type of permission to check, either `read`, `write`, or `delete` * @return bool true if the current user has the permissions to perform the context on the post */ private function check_permission( $post, $context ) { $permission = false; if ( ! is_a( $post, 'WP_Post' ) ) { $post = get_post( $post ); } if ( is_null( $post ) ) { return $permission; } $post_type = get_post_type_object( $post->post_type ); if ( 'read' === $context ) { $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); } elseif ( 'edit' === $context ) { $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); } elseif ( 'delete' === $context ) { $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); } return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); } } includes/legacy/api/v3/class-wc-api-orders.php 0000644 00000172262 15132754524 0015231 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Orders extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/orders'; /** @var string $post_type the custom post type */ protected $post_type = 'shop_order'; /** * Register the routes for this class * * GET|POST /orders * GET /orders/count * GET|PUT|DELETE /orders/<id> * GET /orders/<id>/notes * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET|POST /orders $routes[ $this->base ] = array( array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /orders/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), ); # GET /orders/statuses $routes[ $this->base . '/statuses' ] = array( array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), ); # GET|PUT|DELETE /orders/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_order' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), ); # GET|POST /orders/<id>/notes $routes[ $this->base . '/(?P<order_id>\d+)/notes' ] = array( array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET|PUT|DELETE /orders/<order_id>/notes/<id> $routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array( array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), ); # GET|POST /orders/<order_id>/refunds $routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array( array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET|PUT|DELETE /orders/<order_id>/refunds/<id> $routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array( array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), ); # POST|PUT /orders/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all orders * * @since 2.1 * @param string $fields * @param array $filter * @param string $status * @param int $page * @return array */ public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $filter['page'] = $page; $query = $this->query_orders( $filter ); $orders = array(); foreach ( $query->posts as $order_id ) { if ( ! $this->is_readable( $order_id ) ) { continue; } $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); } $this->server->add_pagination_headers( $query ); return array( 'orders' => $orders ); } /** * Get the order for the given ID. * * @since 2.1 * @param int $id The order ID. * @param array $fields Request fields. * @param array $filter Request filters. * @return array|WP_Error */ public function get_order( $id, $fields = null, $filter = array() ) { // Ensure order ID is valid & user has permission to read. $id = $this->validate_request( $id, $this->post_type, 'read' ); if ( is_wp_error( $id ) ) { return $id; } // Get the decimal precession. $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); $order = wc_get_order( $id ); $expand = array(); if ( ! empty( $filter['expand'] ) ) { $expand = explode( ',', $filter['expand'] ); } $order_data = array( 'id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'order_key' => $order->get_order_key(), 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. 'status' => $order->get_status(), 'currency' => $order->get_currency(), 'total' => wc_format_decimal( $order->get_total(), $dp ), 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), 'total_line_items_quantity' => $order->get_item_count(), 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->get_payment_method(), 'method_title' => $order->get_payment_method_title(), 'paid' => ! is_null( $order->get_date_paid() ), ), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), 'note' => $order->get_customer_note(), 'customer_ip' => $order->get_customer_ip_address(), 'customer_user_agent' => $order->get_customer_user_agent(), 'customer_id' => $order->get_user_id(), 'view_order_url' => $order->get_view_order_url(), 'line_items' => array(), 'shipping_lines' => array(), 'tax_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), ); // Add line items. foreach ( $order->get_items() as $item_id => $item ) { $product = $item->get_product(); $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; $item_meta = $item->get_formatted_meta_data( $hideprefix ); foreach ( $item_meta as $key => $values ) { $item_meta[ $key ]->label = $values->display_key; unset( $item_meta[ $key ]->display_key ); unset( $item_meta[ $key ]->display_value ); } $line_item = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'quantity' => $item->get_quantity(), 'tax_class' => $item->get_tax_class(), 'name' => $item->get_name(), 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => array_values( $item_meta ), ); if ( in_array( 'products', $expand ) && is_object( $product ) ) { $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); if ( isset( $_product_data['product'] ) ) { $line_item['product_data'] = $_product_data['product']; } } $order_data['line_items'][] = $line_item; } // Add shipping. foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $order_data['shipping_lines'][] = array( 'id' => $shipping_item_id, 'method_id' => $shipping_item->get_method_id(), 'method_title' => $shipping_item->get_name(), 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), ); } // Add taxes. foreach ( $order->get_tax_totals() as $tax_code => $tax ) { $tax_line = array( 'id' => $tax->id, 'rate_id' => $tax->rate_id, 'code' => $tax_code, 'title' => $tax->label, 'total' => wc_format_decimal( $tax->amount, $dp ), 'compound' => (bool) $tax->is_compound, ); if ( in_array( 'taxes', $expand ) ) { $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); if ( isset( $_rate_data['tax'] ) ) { $tax_line['rate_data'] = $_rate_data['tax']; } } $order_data['tax_lines'][] = $tax_line; } // Add fees. foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $order_data['fee_lines'][] = array( 'id' => $fee_item_id, 'title' => $fee_item->get_name(), 'tax_class' => $fee_item->get_tax_class(), 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), ); } // Add coupons. foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $coupon_line = array( 'id' => $coupon_item_id, 'code' => $coupon_item->get_code(), 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), ); if ( in_array( 'coupons', $expand ) ) { $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { $coupon_line['coupon_data'] = $_coupon_data['coupon']; } } $order_data['coupon_lines'][] = $coupon_line; } return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); } /** * Get the total number of orders * * @since 2.4 * * @param string $status * @param array $filter * * @return array|WP_Error */ public function get_orders_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); } if ( ! empty( $status ) ) { if ( 'any' === $status ) { $order_statuses = array(); foreach ( wc_get_order_statuses() as $slug => $name ) { $filter['status'] = str_replace( 'wc-', '', $slug ); $query = $this->query_orders( $filter ); $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; } return array( 'count' => $order_statuses ); } else { $filter['status'] = $status; } } $query = $this->query_orders( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a list of valid order statuses * * Note this requires no specific permissions other than being an authenticated * API user. Order statuses (particularly custom statuses) could be considered * private information which is why it's not in the API index. * * @since 2.1 * @return array */ public function get_order_statuses() { $order_statuses = array(); foreach ( wc_get_order_statuses() as $slug => $name ) { $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; } return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); } /** * Create an order * * @since 2.2 * @param array $data raw order data * @return array|WP_Error */ public function create_order( $data ) { global $wpdb; try { if ( ! isset( $data['order'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); } $data = $data['order']; // permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); // default order args, note that status is checked for validity in wc_create_order() $default_order_args = array( 'status' => isset( $data['status'] ) ? $data['status'] : '', 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, ); // if creating order for existing customer if ( ! empty( $data['customer_id'] ) ) { // make sure customer exists if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } $default_order_args['customer_id'] = $data['customer_id']; } // create the pending order $order = $this->create_base_order( $default_order_args, $data ); if ( is_wp_error( $order ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); } // billing/shipping addresses $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $set_item = "set_{$line_type}"; foreach ( $data[ $line ] as $item ) { $this->$set_item( $order, $item, 'create' ); } } } // set is vat exempt if ( isset( $data['is_vat_exempt'] ) ) { update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); } // calculate totals and set them $order->calculate_totals(); // payment method (and payment_complete() if `paid` == true) if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // method ID & title are required if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); // mark as paid if set if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // set order currency if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); } // set order meta if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->get_id(), $data['order_meta'] ); } // HTTP 201 Created $this->server->send_status( 201 ); wc_delete_shop_order_transients( $order ); do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); do_action( 'woocommerce_new_order', $order->get_id() ); return $this->get_order( $order->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Creates new WC_Order. * * Requires a separate function for classes that extend WC_API_Orders. * * @since 2.3 * * @param $args array * @param $data * * @return WC_Order */ protected function create_base_order( $args, $data ) { return wc_create_order( $args ); } /** * Edit an order * * @since 2.2 * @param int $id the order ID * @param array $data * @return array|WP_Error */ public function edit_order( $id, $data ) { try { if ( ! isset( $data['order'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); } $data = $data['order']; $update_totals = false; $id = $this->validate_request( $id, $this->post_type, 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); $order = wc_get_order( $id ); if ( empty( $order ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); } $order_args = array( 'order_id' => $order->get_id() ); // Customer note. if ( isset( $data['note'] ) ) { $order_args['customer_note'] = $data['note']; } // Customer ID. if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { // Make sure customer exists. if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); } // Billing/shipping address. $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $update_totals = true; foreach ( $data[ $line ] as $item ) { // Item ID is always required. if ( ! array_key_exists( 'id', $item ) ) { $item['id'] = null; } // Create item. if ( is_null( $item['id'] ) ) { $this->set_item( $order, $line_type, $item, 'create' ); } elseif ( $this->item_is_null( $item ) ) { // Delete item. wc_delete_order_item( $item['id'] ); } else { // Update item. $this->set_item( $order, $line_type, $item, 'update' ); } } } } // Payment method (and payment_complete() if `paid` == true and order needs payment). if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // Method ID. if ( isset( $data['payment_details']['method_id'] ) ) { update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); } // Method title. if ( isset( $data['payment_details']['method_title'] ) ) { update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); } // Mark as paid if set. if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // Set order currency. if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); } // If items have changed, recalculate order totals. if ( $update_totals ) { $order->calculate_totals(); } // Update order meta. if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->get_id(), $data['order_meta'] ); } // Update the order post to set customer note/modified date. wc_update_order( $order_args ); // Order status. if ( ! empty( $data['status'] ) ) { // Refresh the order instance. $order = wc_get_order( $order->get_id() ); $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); } wc_delete_shop_order_transients( $order ); do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); do_action( 'woocommerce_update_order', $order->get_id() ); return $this->get_order( $id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete an order * * @param int $id the order ID * @param bool $force true to permanently delete order, false to move to trash * @return array|WP_Error */ public function delete_order( $id, $force = false ) { $id = $this->validate_request( $id, $this->post_type, 'delete' ); if ( is_wp_error( $id ) ) { return $id; } wc_delete_shop_order_transients( $id ); do_action( 'woocommerce_api_delete_order', $id, $this ); return $this->delete( $id, 'order', ( 'true' === $force ) ); } /** * Helper method to get order post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ protected function query_orders( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => $this->post_type, 'post_status' => array_keys( wc_get_order_statuses() ), ); // add status argument if ( ! empty( $args['status'] ) ) { $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); $statuses = explode( ',', $statuses ); $query_args['post_status'] = $statuses; unset( $args['status'] ); } if ( ! empty( $args['customer_id'] ) ) { $query_args['meta_query'] = array( array( 'key' => '_customer_user', 'value' => absint( $args['customer_id'] ), 'compare' => '=', ), ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Helper method to set/update the billing & shipping addresses for * an order * * @since 2.1 * @param \WC_Order $order * @param array $data */ protected function set_order_addresses( $order, $data ) { $address_fields = array( 'first_name', 'last_name', 'company', 'email', 'phone', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ); $billing_address = $shipping_address = array(); // billing address if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['billing_address'][ $field ] ) ) { $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); } } unset( $address_fields['email'] ); unset( $address_fields['phone'] ); } // shipping address if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['shipping_address'][ $field ] ) ) { $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); } } } $this->update_address( $order, $billing_address, 'billing' ); $this->update_address( $order, $shipping_address, 'shipping' ); // update user meta if ( $order->get_user_id() ) { foreach ( $billing_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); } foreach ( $shipping_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); } } } /** * Update address. * * @param WC_Order $order * @param array $posted * @param string $type */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { $order->{"set_{$type}_{$key}"}( $value ); } } } /** * Helper method to add/update order meta, with two restrictions: * * 1) Only non-protected meta (no leading underscore) can be set * 2) Meta values must be scalar (int, string, bool) * * @since 2.2 * @param int $order_id valid order ID * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format */ protected function set_order_meta( $order_id, $order_meta ) { foreach ( $order_meta as $meta_key => $meta_value ) { if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { update_post_meta( $order_id, $meta_key, $meta_value ); } } } /** * Helper method to check if the resource ID associated with the provided item is null * * Items can be deleted by setting the resource ID to null * * @since 2.2 * @param array $item item provided in the request body * @return bool true if the item resource ID is null, false otherwise */ protected function item_is_null( $item ) { $keys = array( 'product_id', 'method_id', 'title', 'code' ); foreach ( $keys as $key ) { if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { return true; } } return false; } /** * Wrapper method to create/update order items * * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @since 2.2 * @param \WC_Order $order order * @param string $item_type * @param array $item item provided in the request body * @param string $action either 'create' or 'update' * @throws WC_API_Exception if item ID is not associated with order */ protected function set_item( $order, $item_type, $item, $action ) { global $wpdb; $set_method = "set_{$item_type}"; // verify provided line item ID is associated with order if ( 'update' === $action ) { $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", absint( $item['id'] ), absint( $order->get_id() ) ) ); if ( is_null( $result ) ) { throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); } } $this->$set_method( $order, $item, $action ); } /** * Create or update a line item * * @since 2.2 * @param \WC_Order $order * @param array $item line item data * @param string $action 'create' to add line item or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_line_item( $order, $item, $action ) { $creating = ( 'create' === $action ); // product is always required if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); } // when updating, ensure product ID provided matches if ( 'update' === $action ) { $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); } } if ( isset( $item['product_id'] ) ) { $product_id = $item['product_id']; } elseif ( isset( $item['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $item['sku'] ); } // variations must each have a key & value $variation_id = 0; if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { foreach ( $item['variations'] as $key => $value ) { if ( ! $key || ! $value ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); } } $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); } $product = wc_get_product( $variation_id ? $variation_id : $product_id ); // must be a valid WC_Product if ( ! is_object( $product ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); } // quantity must be positive float if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); } // quantity is required when creating if ( $creating && ! isset( $item['quantity'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); } // quantity if ( $creating ) { $line_item = new WC_Order_Item_Product(); } else { $line_item = new WC_Order_Item_Product( $item['id'] ); } $line_item->set_product( $product ); $line_item->set_order_id( $order->get_id() ); if ( isset( $item['quantity'] ) ) { $line_item->set_quantity( $item['quantity'] ); } if ( isset( $item['total'] ) ) { $line_item->set_total( floatval( $item['total'] ) ); } elseif ( $creating ) { $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); $line_item->set_total( $total ); $line_item->set_subtotal( $total ); } if ( isset( $item['total_tax'] ) ) { $line_item->set_total_tax( floatval( $item['total_tax'] ) ); } if ( isset( $item['subtotal'] ) ) { $line_item->set_subtotal( floatval( $item['subtotal'] ) ); } if ( isset( $item['subtotal_tax'] ) ) { $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); } if ( $variation_id ) { $line_item->set_variation_id( $variation_id ); $line_item->set_variation( $item['variations'] ); } // Save or add to order. if ( $creating ) { $order->add_item( $line_item ); } else { $item_id = $line_item->save(); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); } } } /** * Given a product ID & API provided variations, find the correct variation ID to use for calculation * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass * the cheapest variation ID but provide other information so we have to look up the variation ID. * * @param WC_Product $product Product instance * @param array $variations * * @return int Returns an ID if a valid variation was found for this product */ public function get_variation_id( $product, $variations = array() ) { $variation_id = null; $variations_normalized = array(); if ( $product->is_type( 'variable' ) && $product->has_child() ) { if ( isset( $variations ) && is_array( $variations ) ) { // start by normalizing the passed variations foreach ( $variations as $key => $value ) { $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php $variations_normalized[ $key ] = strtolower( $value ); } // now search through each product child and see if our passed variations match anything foreach ( $product->get_children() as $variation ) { $meta = array(); foreach ( get_post_meta( $variation ) as $key => $value ) { $value = $value[0]; $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); $meta[ $key ] = strtolower( $value ); } // if the variation array is a part of the $meta array, we found our match if ( $this->array_contains( $variations_normalized, $meta ) ) { $variation_id = $variation; break; } } } } return $variation_id; } /** * Utility function to see if the meta array contains data from variations * * @param array $needles * @param array $haystack * * @return bool */ protected function array_contains( $needles, $haystack ) { foreach ( $needles as $key => $value ) { if ( $haystack[ $key ] !== $value ) { return false; } } return true; } /** * Create or update an order shipping method * * @since 2.2 * @param \WC_Order $order * @param array $shipping item data * @param string $action 'create' to add shipping or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_shipping( $order, $shipping, $action ) { // total must be a positive float if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // method ID is required if ( ! isset( $shipping['method_id'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); } $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); $item = new WC_Order_Item_Shipping(); $item->set_order_id( $order->get_id() ); $item->set_shipping_rate( $rate ); $order->add_item( $item ); } else { $item = new WC_Order_Item_Shipping( $shipping['id'] ); if ( isset( $shipping['method_id'] ) ) { $item->set_method_id( $shipping['method_id'] ); } if ( isset( $shipping['method_title'] ) ) { $item->set_method_title( $shipping['method_title'] ); } if ( isset( $shipping['total'] ) ) { $item->set_total( floatval( $shipping['total'] ) ); } $shipping_id = $item->save(); if ( ! $shipping_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order fee * * @since 2.2 * @param \WC_Order $order * @param array $fee item data * @param string $action 'create' to add fee or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_fee( $order, $fee, $action ) { if ( 'create' === $action ) { // fee title is required if ( ! isset( $fee['title'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); } $item = new WC_Order_Item_Fee(); $item->set_order_id( $order->get_id() ); $item->set_name( wc_clean( $fee['title'] ) ); $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); // if taxable, tax class and total are required if ( ! empty( $fee['taxable'] ) ) { if ( ! isset( $fee['tax_class'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); } $item->set_tax_status( 'taxable' ); $item->set_tax_class( $fee['tax_class'] ); if ( isset( $fee['total_tax'] ) ) { $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); } if ( isset( $fee['tax_data'] ) ) { $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); } } $order->add_item( $item ); } else { $item = new WC_Order_Item_Fee( $fee['id'] ); if ( isset( $fee['title'] ) ) { $item->set_name( wc_clean( $fee['title'] ) ); } if ( isset( $fee['tax_class'] ) ) { $item->set_tax_class( $fee['tax_class'] ); } if ( isset( $fee['total'] ) ) { $item->set_total( floatval( $fee['total'] ) ); } if ( isset( $fee['total_tax'] ) ) { $item->set_total_tax( floatval( $fee['total_tax'] ) ); } $fee_id = $item->save(); if ( ! $fee_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order coupon * * @since 2.2 * @param \WC_Order $order * @param array $coupon item data * @param string $action 'create' to add coupon or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_coupon( $order, $coupon, $action ) { // coupon amount must be positive float if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // coupon code is required if ( empty( $coupon['code'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } $item = new WC_Order_Item_Coupon(); $item->set_props( array( 'code' => $coupon['code'], 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, 'discount_tax' => 0, 'order_id' => $order->get_id(), ) ); $order->add_item( $item ); } else { $item = new WC_Order_Item_Coupon( $coupon['id'] ); if ( isset( $coupon['code'] ) ) { $item->set_code( $coupon['code'] ); } if ( isset( $coupon['amount'] ) ) { $item->set_discount( floatval( $coupon['amount'] ) ); } $coupon_id = $item->save(); if ( ! $coupon_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); } } } /** * Get the admin order notes for an order * * @since 2.1 * @param string $order_id order ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_order_notes( $order_id, $fields = null ) { // ensure ID is valid order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $args = array( 'post_id' => $order_id, 'approve' => 'approve', 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $order_notes = array(); foreach ( $notes as $note ) { $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); } return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); } /** * Get an order note for the given order ID and ID * * @since 2.2 * * @param string $order_id order ID * @param string $id order note ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_order_note( $order_id, $id, $fields = null ) { try { // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } $order_note = array( 'id' => $note->comment_ID, 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new order note for the given order * * @since 2.2 * @param string $order_id order ID * @param array $data raw request data * @return WP_Error|array error or created note response data */ public function create_order_note( $order_id, $data ) { try { if ( ! isset( $data['order_note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); } $data = $data['order_note']; // permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); } $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $order = wc_get_order( $order_id ); $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); // note content is required if ( ! isset( $data['note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); } $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); // create the note $note_id = $order->add_order_note( $data['note'], $is_customer_note ); if ( ! $note_id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); } // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); return $this->get_order_note( $order->get_id(), $note_id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit the order note * * @since 2.2 * @param string $order_id order ID * @param string $id note ID * @param array $data parsed request data * @return WP_Error|array error or edited note response data */ public function edit_order_note( $order_id, $id, $data ) { try { if ( ! isset( $data['order_note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); } $data = $data['order_note']; // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $order = wc_get_order( $order_id ); // Validate note ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } // Ensure note ID is valid $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } // Ensure note ID is associated with given order if ( $note->comment_post_ID != $order->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); // Note content if ( isset( $data['note'] ) ) { wp_update_comment( array( 'comment_ID' => $note->comment_ID, 'comment_content' => $data['note'], ) ); } // Customer note if ( isset( $data['customer_note'] ) ) { update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); } do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); return $this->get_order_note( $order->get_id(), $note->comment_ID ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete order note * * @since 2.2 * @param string $order_id order ID * @param string $id note ID * @return WP_Error|array error or deleted message */ public function delete_order_note( $order_id, $id ) { try { $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate note ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } // Ensure note ID is valid $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } // Ensure note ID is associated with given order if ( $note->comment_post_ID != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); } // Force delete since trashed order notes could not be managed through comments list table $result = wc_delete_order_note( $note->comment_ID ); if ( ! $result ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); } do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the order refunds for an order * * @since 2.2 * @param string $order_id order ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_order_refunds( $order_id, $fields = null ) { // Ensure ID is valid order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $refund_items = wc_get_orders( array( 'type' => 'shop_order_refund', 'parent' => $order_id, 'limit' => -1, 'return' => 'ids', ) ); $order_refunds = array(); foreach ( $refund_items as $refund_id ) { $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); } return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); } /** * Get an order refund for the given order ID and ID * * @since 2.2 * * @param string $order_id order ID * @param int $id * @param string|null $fields fields to limit response to * @param array $filter * * @return array|WP_Error */ public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { try { // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } $order = wc_get_order( $order_id ); $refund = wc_get_order( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } $line_items = array(); // Add line items foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { $product = $item->get_product(); $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; $item_meta = $item->get_formatted_meta_data( $hideprefix ); foreach ( $item_meta as $key => $values ) { $item_meta[ $key ]->label = $values->display_key; unset( $item_meta[ $key ]->display_key ); unset( $item_meta[ $key ]->display_value ); } $line_items[] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), 'quantity' => $item->get_quantity(), 'tax_class' => $item->get_tax_class(), 'name' => $item->get_name(), 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => array_values( $item_meta ), 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), ); } $order_refund = array( 'id' => $refund->get_id(), 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), 'reason' => $refund->get_reason(), 'line_items' => $line_items, ); return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new order refund for the given order * * @since 2.2 * @param string $order_id order ID * @param array $data raw request data * @param bool $api_refund do refund using a payment gateway API * @return WP_Error|array error or created refund response data */ public function create_order_refund( $order_id, $data, $api_refund = true ) { try { if ( ! isset( $data['order_refund'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); } $data = $data['order_refund']; // Permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); } $order_id = absint( $order_id ); if ( empty( $order_id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); // Refund amount is required if ( ! isset( $data['amount'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); } elseif ( 0 > $data['amount'] ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); } $data['order_id'] = $order_id; $data['refund_id'] = 0; // Create the refund $refund = wc_create_refund( $data ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } // Refund via API if ( $api_refund ) { if ( WC()->payment_gateways() ) { $payment_gateways = WC()->payment_gateways->payment_gateways(); } $order = wc_get_order( $order_id ); if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); if ( is_wp_error( $result ) ) { return $result; } elseif ( ! $result ) { throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); } } } // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); return $this->get_order_refund( $order_id, $refund->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit an order refund * * @since 2.2 * @param string $order_id order ID * @param string $id refund ID * @param array $data parsed request data * @return WP_Error|array error or edited refund response data */ public function edit_order_refund( $order_id, $id, $data ) { try { if ( ! isset( $data['order_refund'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); } $data = $data['order_refund']; // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate refund ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } // Ensure order ID is valid $refund = get_post( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } // Ensure refund ID is associated with given order if ( $refund->post_parent != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); // Update reason if ( isset( $data['reason'] ) ) { $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); if ( is_wp_error( $updated_refund ) ) { return $updated_refund; } } // Update refund amount if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); } do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); return $this->get_order_refund( $order_id, $refund->ID ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete order refund * * @since 2.2 * @param string $order_id order ID * @param string $id refund ID * @return WP_Error|array error or deleted message */ public function delete_order_refund( $order_id, $id ) { try { $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate refund ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } // Ensure refund ID is valid $refund = get_post( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } // Ensure refund ID is associated with given order if ( $refund->post_parent != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); } wc_delete_shop_order_transients( $order_id ); do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); return $this->delete( $refund->ID, 'refund', true ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Bulk update or insert orders * Accepts an array with orders in the formats supported by * WC_API_Orders->create_order() and WC_API_Orders->edit_order() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['orders'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); } $data = $data['orders']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $orders = array(); foreach ( $data as $_order ) { $order_id = 0; // Try to get the order ID if ( isset( $_order['id'] ) ) { $order_id = intval( $_order['id'] ); } if ( $order_id ) { // Order exists / edit order $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); if ( is_wp_error( $edit ) ) { $orders[] = array( 'id' => $order_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $orders[] = $edit['order']; } } else { // Order don't exists / create order $new = $this->create_order( array( 'order' => $_order ) ); if ( is_wp_error( $new ) ) { $orders[] = array( 'id' => $order_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $orders[] = $new['order']; } } } return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v3/class-wc-api-customers.php 0000644 00000061021 15132754524 0015745 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Customers extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/customers'; /** @var string $created_at_min for date filtering */ private $created_at_min = null; /** @var string $created_at_max for date filtering */ private $created_at_max = null; /** * Setup class, overridden to provide customer data to order response * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { parent::__construct( $server ); // add customer data to order responses add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 ); // modify WP_User_Query to support created_at date filtering add_action( 'pre_user_query', array( $this, 'modify_user_query' ) ); } /** * Register the routes for this class * * GET /customers * GET /customers/count * GET /customers/<id> * GET /customers/<id>/orders * * @since 2.2 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /customers $routes[ $this->base ] = array( array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /customers/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), ); # GET/PUT/DELETE /customers/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ), ); # GET /customers/email/<email> $routes[ $this->base . '/email/(?P<email>.+)' ] = array( array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id>/orders $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array( array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id>/downloads $routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array( array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ), ); # POST|PUT /customers/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all customers * * @since 2.1 * @param array $fields * @param array $filter * @param int $page * @return array */ public function get_customers( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_customers( $filter ); $customers = array(); foreach ( $query->get_results() as $user_id ) { if ( ! $this->is_readable( $user_id ) ) { continue; } $customers[] = current( $this->get_customer( $user_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'customers' => $customers ); } /** * Get the customer for the given ID * * @since 2.1 * @param int $id the customer ID * @param array $fields * @return array|WP_Error */ public function get_customer( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $customer = new WC_Customer( $id ); $last_order = $customer->get_last_order(); $customer_data = array( 'id' => $customer->get_id(), 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'last_update' => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. 'email' => $customer->get_email(), 'first_name' => $customer->get_first_name(), 'last_name' => $customer->get_last_name(), 'username' => $customer->get_username(), 'role' => $customer->get_role(), 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. 'orders_count' => $customer->get_order_count(), 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), 'avatar_url' => $customer->get_avatar_url(), 'billing_address' => array( 'first_name' => $customer->get_billing_first_name(), 'last_name' => $customer->get_billing_last_name(), 'company' => $customer->get_billing_company(), 'address_1' => $customer->get_billing_address_1(), 'address_2' => $customer->get_billing_address_2(), 'city' => $customer->get_billing_city(), 'state' => $customer->get_billing_state(), 'postcode' => $customer->get_billing_postcode(), 'country' => $customer->get_billing_country(), 'email' => $customer->get_billing_email(), 'phone' => $customer->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $customer->get_shipping_first_name(), 'last_name' => $customer->get_shipping_last_name(), 'company' => $customer->get_shipping_company(), 'address_1' => $customer->get_shipping_address_1(), 'address_2' => $customer->get_shipping_address_2(), 'city' => $customer->get_shipping_city(), 'state' => $customer->get_shipping_state(), 'postcode' => $customer->get_shipping_postcode(), 'country' => $customer->get_shipping_country(), ), ); return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); } /** * Get the customer for the given email * * @since 2.1 * * @param string $email the customer email * @param array $fields * * @return array|WP_Error */ public function get_customer_by_email( $email, $fields = null ) { try { if ( is_email( $email ) ) { $customer = get_user_by( 'email', $email ); if ( ! is_object( $customer ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); } } else { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); } return $this->get_customer( $customer->ID, $fields ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of customers * * @since 2.1 * * @param array $filter * * @return array|WP_Error */ public function get_customers_count( $filter = array() ) { try { if ( ! current_user_can( 'list_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 ); } $query = $this->query_customers( $filter ); return array( 'count' => $query->get_total() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get customer billing address fields. * * @since 2.2 * @return array */ protected function get_customer_billing_address() { $billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone', ) ); return $billing_address; } /** * Get customer shipping address fields. * * @since 2.2 * @return array */ protected function get_customer_shipping_address() { $shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ) ); return $shipping_address; } /** * Add/Update customer data. * * @since 2.2 * @param int $id the customer ID * @param array $data * @param WC_Customer $customer */ protected function update_customer_data( $id, $data, $customer ) { // Customer first name. if ( isset( $data['first_name'] ) ) { $customer->set_first_name( wc_clean( $data['first_name'] ) ); } // Customer last name. if ( isset( $data['last_name'] ) ) { $customer->set_last_name( wc_clean( $data['last_name'] ) ); } // Customer billing address. if ( isset( $data['billing_address'] ) ) { foreach ( $this->get_customer_billing_address() as $field ) { if ( isset( $data['billing_address'][ $field ] ) ) { if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) { $customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] ); } else { $customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) ); } } } } // Customer shipping address. if ( isset( $data['shipping_address'] ) ) { foreach ( $this->get_customer_shipping_address() as $field ) { if ( isset( $data['shipping_address'][ $field ] ) ) { if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { $customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] ); } else { $customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) ); } } } } do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer ); } /** * Create a customer * * @since 2.2 * * @param array $data * * @return array|WP_Error */ public function create_customer( $data ) { try { if ( ! isset( $data['customer'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 ); } $data = $data['customer']; // Checks with can create new users. if ( ! current_user_can( 'create_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this ); // Checks with the email is missing. if ( ! isset( $data['email'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 ); } // Create customer. $customer = new WC_Customer; $customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' ); $customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' ); $customer->set_email( $data['email'] ); $customer->save(); if ( ! $customer->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); } // Added customer data. $this->update_customer_data( $customer->get_id(), $data, $customer ); $customer->save(); do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data ); $this->server->send_status( 201 ); return $this->get_customer( $customer->get_id() ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a customer * * @since 2.2 * * @param int $id the customer ID * @param array $data * * @return array|WP_Error */ public function edit_customer( $id, $data ) { try { if ( ! isset( $data['customer'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 ); } $data = $data['customer']; // Validate the customer ID. $id = $this->validate_request( $id, 'customer', 'edit' ); // Return the validate error. if ( is_wp_error( $id ) ) { throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); } $data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this ); $customer = new WC_Customer( $id ); // Customer email. if ( isset( $data['email'] ) ) { $customer->set_email( $data['email'] ); } // Customer password. if ( isset( $data['password'] ) ) { $customer->set_password( $data['password'] ); } // Update customer data. $this->update_customer_data( $customer->get_id(), $data, $customer ); $customer->save(); do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data ); return $this->get_customer( $customer->get_id() ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a customer * * @since 2.2 * @param int $id the customer ID * @return array|WP_Error */ public function delete_customer( $id ) { // Validate the customer ID. $id = $this->validate_request( $id, 'customer', 'delete' ); // Return the validate error. if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_customer', $id, $this ); return $this->delete( $id, 'customer' ); } /** * Get the orders for a customer * * @since 2.1 * @param int $id the customer ID * @param string $fields fields to include in response * @param array $filter filters * @return array|WP_Error */ public function get_customer_orders( $id, $fields = null, $filter = array() ) { $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $filter['customer_id'] = $id; $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 ); return $orders; } /** * Get the available downloads for a customer * * @since 2.2 * @param int $id the customer ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_customer_downloads( $id, $fields = null ) { $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $downloads = array(); $_downloads = wc_get_customer_available_downloads( $id ); foreach ( $_downloads as $key => $download ) { $downloads[] = array( 'download_url' => $download['download_url'], 'download_id' => $download['download_id'], 'product_id' => $download['product_id'], 'download_name' => $download['download_name'], 'order_id' => $download['order_id'], 'order_key' => $download['order_key'], 'downloads_remaining' => $download['downloads_remaining'], 'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null, 'file' => $download['file'], ); } return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) ); } /** * Helper method to get customer user objects * * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited * pagination support * * The filter for role can only be a single role in a string. * * @since 2.3 * @param array $args request arguments for filtering query * @return WP_User_Query */ private function query_customers( $args = array() ) { // default users per page $users_per_page = get_option( 'posts_per_page' ); // Set base query arguments $query_args = array( 'fields' => 'ID', 'role' => 'customer', 'orderby' => 'registered', 'number' => $users_per_page, ); // Custom Role if ( ! empty( $args['role'] ) ) { $query_args['role'] = $args['role']; // Show users on all roles if ( 'all' === $query_args['role'] ) { unset( $query_args['role'] ); } } // Search if ( ! empty( $args['q'] ) ) { $query_args['search'] = $args['q']; } // Limit number of users returned if ( ! empty( $args['limit'] ) ) { if ( -1 == $args['limit'] ) { unset( $query_args['number'] ); } else { $query_args['number'] = absint( $args['limit'] ); $users_per_page = absint( $args['limit'] ); } } else { $args['limit'] = $query_args['number']; } // Page $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; // Offset if ( ! empty( $args['offset'] ) ) { $query_args['offset'] = absint( $args['offset'] ); } else { $query_args['offset'] = $users_per_page * ( $page - 1 ); } // Created date if ( ! empty( $args['created_at_min'] ) ) { $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); } if ( ! empty( $args['created_at_max'] ) ) { $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); } // Order (ASC or DESC, ASC by default) if ( ! empty( $args['order'] ) ) { $query_args['order'] = $args['order']; } // Order by if ( ! empty( $args['orderby'] ) ) { $query_args['orderby'] = $args['orderby']; // Allow sorting by meta value if ( ! empty( $args['orderby_meta_key'] ) ) { $query_args['meta_key'] = $args['orderby_meta_key']; } } $query = new WP_User_Query( $query_args ); // Helper members for pagination headers $query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page ); $query->page = $page; return $query; } /** * Add customer data to orders * * @since 2.1 * @param $order_data * @param $order * @return array */ public function add_customer_data( $order_data, $order ) { if ( 0 == $order->get_user_id() ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->get_billing_email(), 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); } return $order_data; } /** * Modify the WP_User_Query to support filtering on the date the customer was created * * @since 2.1 * @param WP_User_Query $query */ public function modify_user_query( $query ) { if ( $this->created_at_min ) { $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) ); } if ( $this->created_at_max ) { $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid WP_User * 3) the current user has the proper permissions * * @since 2.1 * @see WC_API_Resource::validate_request() * @param integer $id the customer ID * @param string $type the request type, unused because this method overrides the parent class * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid user ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { try { $id = absint( $id ); // validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 ); } // non-existent IDs return a valid WP_User object with the user ID = 0 $customer = new WP_User( $id ); if ( 0 === $customer->ID ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 ); } // validate permissions switch ( $context ) { case 'read': if ( ! current_user_can( 'list_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 ); } break; case 'edit': if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 ); } break; case 'delete': if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 ); } break; } return $id; } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Check if the current user can read users * * @since 2.1 * @see WC_API_Resource::is_readable() * @param int|WP_Post $post unused * @return bool true if the current user can read users, false otherwise */ protected function is_readable( $post ) { return current_user_can( 'list_users' ); } /** * Bulk update or insert customers * Accepts an array with customers in the formats supported by * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['customers'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 ); } $data = $data['customers']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $customers = array(); foreach ( $data as $_customer ) { $customer_id = 0; // Try to get the customer ID if ( isset( $_customer['id'] ) ) { $customer_id = intval( $_customer['id'] ); } if ( $customer_id ) { // Customer exists / edit customer $edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) ); if ( is_wp_error( $edit ) ) { $customers[] = array( 'id' => $customer_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $customers[] = $edit['customer']; } } else { // Customer don't exists / create customer $new = $this->create_customer( array( 'customer' => $_customer ) ); if ( is_wp_error( $new ) ) { $customers[] = array( 'id' => $customer_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $customers[] = $new['customer']; } } } return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v3/class-wc-api-exception.php 0000644 00000002213 15132754524 0015715 0 ustar 00 <?php /** * WooCommerce API Exception Class * * Extends Exception to provide additional data * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Exception extends Exception { /** @var string sanitized error code */ protected $error_code; /** * Setup exception, requires 3 params: * * error code - machine-readable, e.g. `woocommerce_invalid_product_id` * error message - friendly message, e.g. 'Product ID is invalid' * http status code - proper HTTP status code to respond with, e.g. 400 * * @since 2.2 * @param string $error_code * @param string $error_message user-friendly translated error message * @param int $http_status_code HTTP status code to respond with */ public function __construct( $error_code, $error_message, $http_status_code ) { $this->error_code = $error_code; parent::__construct( $error_message, $http_status_code ); } /** * Returns the error code * * @since 2.2 * @return string */ public function getErrorCode() { return $this->error_code; } } includes/legacy/api/v3/class-wc-api-products.php 0000644 00000330554 15132754524 0015576 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Products extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/products'; /** * Register the routes for this class * * GET/POST /products * GET /products/count * GET/PUT/DELETE /products/<id> * GET /products/<id>/reviews * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /products $routes[ $this->base ] = array( array( array( $this, 'get_products' ), WC_API_Server::READABLE ), array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /products/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), ); # GET/PUT/DELETE /products/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_product' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), ); # GET /products/<id>/reviews $routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array( array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), ); # GET /products/<id>/orders $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array( array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), ); # GET/POST /products/categories $routes[ $this->base . '/categories' ] = array( array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /products/categories/<id> $routes[ $this->base . '/categories/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), ); # GET/POST /products/tags $routes[ $this->base . '/tags' ] = array( array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /products/tags/<id> $routes[ $this->base . '/tags/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), ); # GET/POST /products/shipping_classes $routes[ $this->base . '/shipping_classes' ] = array( array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /products/shipping_classes/<id> $routes[ $this->base . '/shipping_classes/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), ); # GET/POST /products/attributes $routes[ $this->base . '/attributes' ] = array( array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /products/attributes/<id> $routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), ); # GET/POST /products/attributes/<attribute_id>/terms $routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms' ] = array( array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /products/attributes/<attribute_id>/terms/<id> $routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), ); # POST|PUT /products/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all products * * @since 2.1 * @param string $fields * @param string $type * @param array $filter * @param int $page * @return array */ public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { if ( ! empty( $type ) ) { $filter['type'] = $type; } $filter['page'] = $page; $query = $this->query_products( $filter ); $products = array(); foreach ( $query->posts as $product_id ) { if ( ! $this->is_readable( $product_id ) ) { continue; } $products[] = current( $this->get_product( $product_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'products' => $products ); } /** * Get the product for the given ID * * @since 2.1 * @param int $id the product ID * @param string $fields * @return array|WP_Error */ public function get_product( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); // add data that applies to every product type $product_data = $this->get_product_data( $product ); // add variations to variable products if ( $product->is_type( 'variable' ) && $product->has_child() ) { $product_data['variations'] = $this->get_variation_data( $product ); } // add the parent product data to an individual variation if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); } // Add grouped products data if ( $product->is_type( 'grouped' ) && $product->has_child() ) { $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); } if ( $product->is_type( 'simple' ) ) { $parent_id = $product->get_parent_id(); if ( ! empty( $parent_id ) ) { $_product = wc_get_product( $parent_id ); $product_data['parent'] = $this->get_product_data( $_product ); } } return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); } /** * Get the total number of products * * @since 2.1 * * @param string $type * @param array $filter * * @return array|WP_Error */ public function get_products_count( $type = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_products' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); } if ( ! empty( $type ) ) { $filter['type'] = $type; } $query = $this->query_products( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product. * * @since 2.2 * * @param array $data posted data * * @return array|WP_Error */ public function create_product( $data ) { $id = 0; try { if ( ! isset( $data['product'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); } $data = $data['product']; // Check permissions. if ( ! current_user_can( 'publish_products' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); // Check if product title is specified. if ( ! isset( $data['title'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); } // Check product type. if ( ! isset( $data['type'] ) ) { $data['type'] = 'simple'; } // Set visible visibility when not sent. if ( ! isset( $data['catalog_visibility'] ) ) { $data['catalog_visibility'] = 'visible'; } // Validate the product type. if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); } // Enable description html tags. $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { $post_content = wp_filter_post_kses( $data['description'] ); } // Enable short description html tags. $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { $post_excerpt = wp_filter_post_kses( $data['short_description'] ); } $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); if ( ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } $product = new $classname(); $product->set_name( wc_clean( $data['title'] ) ); $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); $product->set_description( isset( $data['description'] ) ? $post_content : '' ); $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); if ( ! empty( $data['name'] ) ) { $product->set_slug( sanitize_title( $data['name'] ) ); } // Attempts to create the new product. $product->save(); $id = $product->get_id(); // Checks for an error in the product creation. if ( 0 >= $id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); } // Check for featured/gallery images, upload it and set it. if ( isset( $data['images'] ) ) { $product = $this->save_product_images( $product, $data['images'] ); } // Save product meta fields. $product = $this->save_product_meta( $product, $data ); $product->save(); // Save variations. if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $product, $data ); } do_action( 'woocommerce_api_create_product', $id, $data ); // Clear cache/transients. wc_delete_product_transients( $id ); $this->server->send_status( 201 ); return $this->get_product( $id ); } catch ( WC_Data_Exception $e ) { $this->clear_product( $id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } catch ( WC_API_Exception $e ) { $this->clear_product( $id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product * * @since 2.2 * * @param int $id the product ID * @param array $data * * @return array|WP_Error */ public function edit_product( $id, $data ) { try { if ( ! isset( $data['product'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); } $data = $data['product']; $id = $this->validate_request( $id, 'product', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); // Product title. if ( isset( $data['title'] ) ) { $product->set_name( wc_clean( $data['title'] ) ); } // Product name (slug). if ( isset( $data['name'] ) ) { $product->set_slug( wc_clean( $data['name'] ) ); } // Product status. if ( isset( $data['status'] ) ) { $product->set_status( wc_clean( $data['status'] ) ); } // Product short description. if ( isset( $data['short_description'] ) ) { // Enable short description html tags. $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); $product->set_short_description( $post_excerpt ); } // Product description. if ( isset( $data['description'] ) ) { // Enable description html tags. $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); $product->set_description( $post_content ); } // Validate the product type. if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); } // Menu order. if ( isset( $data['menu_order'] ) ) { $product->set_menu_order( intval( $data['menu_order'] ) ); } // Check for featured/gallery images, upload it and set it. if ( isset( $data['images'] ) ) { $product = $this->save_product_images( $product, $data['images'] ); } // Save product meta fields. $product = $this->save_product_meta( $product, $data ); // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $product, $data ); } else { // Just sync variations. $product = WC_Product_Variable::sync( $product, false ); } } $product->save(); do_action( 'woocommerce_api_edit_product', $id, $data ); // Clear cache/transients. wc_delete_product_transients( $id ); return $this->get_product( $id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product. * * @since 2.2 * * @param int $id the product ID. * @param bool $force true to permanently delete order, false to move to trash. * * @return array|WP_Error */ public function delete_product( $id, $force = false ) { $id = $this->validate_request( $id, 'product', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); do_action( 'woocommerce_api_delete_product', $id, $this ); // If we're forcing, then delete permanently. if ( $force ) { if ( $product->is_type( 'variable' ) ) { foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->delete( true ); } } } else { // For other product types, if the product has children, remove the relationship. foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->set_parent_id( 0 ); $child->save(); } } } $product->delete( true ); $result = ! ( $product->get_id() > 0 ); } else { $product->delete(); $result = 'trash' === $product->get_status(); } if ( ! $result ) { return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); } // Delete parent product transients. if ( $parent_id = wp_get_post_parent_id( $id ) ) { wc_delete_product_transients( $parent_id ); } if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); } } /** * Get the reviews for a product * * @since 2.1 * @param int $id the product ID to get reviews for * @param string $fields fields to include in response * @return array|WP_Error */ public function get_product_reviews( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $comments = get_approved_comments( $id ); $reviews = array(); foreach ( $comments as $comment ) { $reviews[] = array( 'id' => intval( $comment->comment_ID ), 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), 'review' => $comment->comment_content, 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'reviewer_name' => $comment->comment_author, 'reviewer_email' => $comment->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), ); } return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); } /** * Get the orders for a product * * @since 2.4.0 * @param int $id the product ID to get orders for * @param string fields fields to retrieve * @param array $filter filters to include in response * @param string $status the order status to retrieve * @param $page $page page to retrieve * @return array|WP_Error */ public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { global $wpdb; $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $order_ids = $wpdb->get_col( $wpdb->prepare( " SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) AND order_item_type = 'line_item' ", $id ) ); if ( empty( $order_ids ) ) { return array( 'orders' => array() ); } $filter = array_merge( $filter, array( 'in' => implode( ',', $order_ids ), ) ); $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); } /** * Get a listing of product categories * * @since 2.2 * * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_categories( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); } $product_categories = array(); $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); foreach ( $terms as $term_id ) { $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); } return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product category for the given ID * * @since 2.2 * * @param string $id product category term ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_category( $id, $fields = null ) { try { $id = absint( $id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); } $term = get_term( $id, 'product_cat' ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); } $term_id = intval( $term->term_id ); // Get category display type $display_type = get_term_meta( $term_id, 'display_type', true ); // Get category image $image = ''; if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { $image = wp_get_attachment_url( $image_id ); } $product_category = array( 'id' => $term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent, 'description' => $term->description, 'display' => $display_type ? $display_type : 'default', 'image' => $image ? esc_url( $image ) : '', 'count' => intval( $term->count ), ); return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product category. * * @since 2.5.0 * @param array $data Posted data * @return array|WP_Error Product category if succeed, otherwise WP_Error * will be returned */ public function create_product_category( $data ) { global $wpdb; try { if ( ! isset( $data['product_category'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); } $defaults = array( 'name' => '', 'slug' => '', 'description' => '', 'parent' => 0, 'display' => 'default', 'image' => '', ); $data = wp_parse_args( $data['product_category'], $defaults ); $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); // Check parent. $data['parent'] = absint( $data['parent'] ); if ( $data['parent'] ) { $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); if ( ! $parent ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); } } // If value of image is numeric, assume value as image_id. $image = $data['image']; $image_id = 0; if ( is_numeric( $image ) ) { $image_id = absint( $image ); } elseif ( ! empty( $image ) ) { $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); $image_id = $this->set_product_category_image_as_attachment( $upload ); } $insert = wp_insert_term( $data['name'], 'product_cat', $data ); if ( is_wp_error( $insert ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); } $id = $insert['term_id']; update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); // Check if image_id is a valid image attachment before updating the term meta. if ( $image_id && wp_attachment_is_image( $image_id ) ) { update_term_meta( $id, 'thumbnail_id', $image_id ); } do_action( 'woocommerce_api_create_product_category', $id, $data ); $this->server->send_status( 201 ); return $this->get_product_category( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product category. * * @since 2.5.0 * @param int $id Product category term ID * @param array $data Posted data * @return array|WP_Error Product category if succeed, otherwise WP_Error * will be returned */ public function edit_product_category( $id, $data ) { global $wpdb; try { if ( ! isset( $data['product_category'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); } $id = absint( $id ); $data = $data['product_category']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); $category = $this->get_product_category( $id ); if ( is_wp_error( $category ) ) { return $category; } if ( isset( $data['image'] ) ) { $image_id = 0; // If value of image is numeric, assume value as image_id. $image = $data['image']; if ( is_numeric( $image ) ) { $image_id = absint( $image ); } elseif ( ! empty( $image ) ) { $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); $image_id = $this->set_product_category_image_as_attachment( $upload ); } // In case client supplies invalid image or wants to unset category image. if ( ! wp_attachment_is_image( $image_id ) ) { $image_id = ''; } } $update = wp_update_term( $id, 'product_cat', $data ); if ( is_wp_error( $update ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); } if ( ! empty( $data['display'] ) ) { update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); } if ( isset( $image_id ) ) { update_term_meta( $id, 'thumbnail_id', $image_id ); } do_action( 'woocommerce_api_edit_product_category', $id, $data ); return $this->get_product_category( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product category. * * @since 2.5.0 * @param int $id Product category term ID * @return array|WP_Error Success message if succeed, otherwise WP_Error * will be returned */ public function delete_product_category( $id ) { global $wpdb; try { // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); } $id = absint( $id ); $deleted = wp_delete_term( $id, 'product_cat' ); if ( ! $deleted || is_wp_error( $deleted ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); } do_action( 'woocommerce_api_delete_product_category', $id, $this ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a listing of product tags. * * @since 2.5.0 * * @param string|null $fields Fields to limit response to * * @return array|WP_Error */ public function get_product_tags( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); } $product_tags = array(); $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); foreach ( $terms as $term_id ) { $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); } return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product tag for the given ID. * * @since 2.5.0 * * @param string $id Product tag term ID * @param string|null $fields Fields to limit response to * * @return array|WP_Error */ public function get_product_tag( $id, $fields = null ) { try { $id = absint( $id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); } $term = get_term( $id, 'product_tag' ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); } $term_id = intval( $term->term_id ); $tag = array( 'id' => $term_id, 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, 'count' => intval( $term->count ), ); return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product tag. * * @since 2.5.0 * @param array $data Posted data * @return array|WP_Error Product tag if succeed, otherwise WP_Error * will be returned */ public function create_product_tag( $data ) { try { if ( ! isset( $data['product_tag'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); } $defaults = array( 'name' => '', 'slug' => '', 'description' => '', ); $data = wp_parse_args( $data['product_tag'], $defaults ); $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); $insert = wp_insert_term( $data['name'], 'product_tag', $data ); if ( is_wp_error( $insert ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); } $id = $insert['term_id']; do_action( 'woocommerce_api_create_product_tag', $id, $data ); $this->server->send_status( 201 ); return $this->get_product_tag( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product tag. * * @since 2.5.0 * @param int $id Product tag term ID * @param array $data Posted data * @return array|WP_Error Product tag if succeed, otherwise WP_Error * will be returned */ public function edit_product_tag( $id, $data ) { try { if ( ! isset( $data['product_tag'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); } $id = absint( $id ); $data = $data['product_tag']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); $tag = $this->get_product_tag( $id ); if ( is_wp_error( $tag ) ) { return $tag; } $update = wp_update_term( $id, 'product_tag', $data ); if ( is_wp_error( $update ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); } do_action( 'woocommerce_api_edit_product_tag', $id, $data ); return $this->get_product_tag( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product tag. * * @since 2.5.0 * @param int $id Product tag term ID * @return array|WP_Error Success message if succeed, otherwise WP_Error * will be returned */ public function delete_product_tag( $id ) { try { // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); } $id = absint( $id ); $deleted = wp_delete_term( $id, 'product_tag' ); if ( ! $deleted || is_wp_error( $deleted ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); } do_action( 'woocommerce_api_delete_product_tag', $id, $this ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Helper method to get product post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_products( $args ) { // Set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'product', 'post_status' => 'publish', 'meta_query' => array(), ); // Taxonomy query to filter products by type, category, tag, shipping class, and // attribute. $tax_query = array(); // Map between taxonomy name and arg's key. $taxonomies_arg_map = array( 'product_type' => 'type', 'product_cat' => 'category', 'product_tag' => 'tag', 'product_shipping_class' => 'shipping_class', ); // Add attribute taxonomy names into the map. foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { $taxonomies_arg_map[ $attribute_name ] = $attribute_name; } // Set tax_query for each passed arg. foreach ( $taxonomies_arg_map as $tax_name => $arg ) { if ( ! empty( $args[ $arg ] ) ) { $terms = explode( ',', $args[ $arg ] ); $tax_query[] = array( 'taxonomy' => $tax_name, 'field' => 'slug', 'terms' => $terms, ); unset( $args[ $arg ] ); } } if ( ! empty( $tax_query ) ) { $query_args['tax_query'] = $tax_query; } // Filter by specific sku if ( ! empty( $args['sku'] ) ) { if ( ! is_array( $query_args['meta_query'] ) ) { $query_args['meta_query'] = array(); } $query_args['meta_query'][] = array( 'key' => '_sku', 'value' => $args['sku'], 'compare' => '=', ); $query_args['post_type'] = array( 'product', 'product_variation' ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Get standard product data that applies to every product type * * @since 2.1 * @param WC_Product|int $product * * @return array */ private function get_product_data( $product ) { if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } if ( ! is_a( $product, 'WC_Product' ) ) { return array(); } return array( 'title' => $product->get_name(), 'id' => $product->get_id(), 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), 'type' => $product->get_type(), 'status' => $product->get_status(), 'downloadable' => $product->is_downloadable(), 'virtual' => $product->is_virtual(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, 'price_html' => $product->get_price_html(), 'taxable' => $product->is_taxable(), 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'managing_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'purchaseable' => $product->is_purchasable(), 'featured' => $product->is_featured(), 'visible' => $product->is_visible(), 'catalog_visibility' => $product->get_catalog_visibility(), 'on_sale' => $product->is_on_sale(), 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'weight' => $product->get_weight() ? $product->get_weight() : null, 'dimensions' => array( 'length' => $product->get_length(), 'width' => $product->get_width(), 'height' => $product->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, 'description' => wpautop( do_shortcode( $product->get_description() ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), 'reviews_allowed' => $product->get_reviews_allowed(), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), 'parent_id' => $product->get_parent_id(), 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), 'images' => $this->get_images( $product ), 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => $product->get_download_limit(), 'download_expiry' => $product->get_download_expiry(), 'download_type' => 'standard', 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), 'total_sales' => $product->get_total_sales(), 'variations' => array(), 'parent' => array(), 'grouped_products' => array(), 'menu_order' => $this->get_product_menu_order( $product ), ); } /** * Get product menu order. * * @since 2.5.3 * @param WC_Product $product * @return int */ private function get_product_menu_order( $product ) { $menu_order = $product->get_menu_order(); return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); } /** * Get an individual variation's data. * * @since 2.1 * @param WC_Product $product * @return array */ private function get_variation_data( $product ) { $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = wc_get_product( $child_id ); if ( ! $variation || ! $variation->exists() ) { continue; } $variations[] = array( 'id' => $variation->get_id(), 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), 'downloadable' => $variation->is_downloadable(), 'virtual' => $variation->is_virtual(), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, 'taxable' => $variation->is_taxable(), 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'managing_stock' => $variation->managing_stock(), 'stock_quantity' => $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backorders_allowed' => $variation->backorders_allowed(), 'backordered' => $variation->is_on_backorder(), 'purchaseable' => $variation->is_purchasable(), 'visible' => $variation->variation_is_visible(), 'on_sale' => $variation->is_on_sale(), 'weight' => $variation->get_weight() ? $variation->get_weight() : null, 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => (int) $product->get_download_limit(), 'download_expiry' => (int) $product->get_download_expiry(), ); } return $variations; } /** * Get grouped products data * * @since 2.5.0 * @param WC_Product $product * * @return array */ private function get_grouped_products_data( $product ) { $products = array(); foreach ( $product->get_children() as $child_id ) { $_product = wc_get_product( $child_id ); if ( ! $_product || ! $_product->exists() ) { continue; } $products[] = $this->get_product_data( $_product ); } return $products; } /** * Save default attributes. * * @since 3.0.0 * * @param WC_Product $product * @param WP_REST_Request $request * @return WC_Product */ protected function save_default_attributes( $product, $request ) { // Update default attributes options setting. if ( isset( $request['default_attribute'] ) ) { $request['default_attributes'] = $request['default_attribute']; } if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { $attributes = $product->get_attributes(); $default_attributes = array(); foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { if ( ! isset( $default_attr['name'] ) ) { continue; } $taxonomy = sanitize_title( $default_attr['name'] ); if ( isset( $default_attr['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; if ( $_attribute['is_variation'] ) { $value = ''; if ( isset( $default_attr['option'] ) ) { if ( $_attribute['is_taxonomy'] ) { // Don't use wc_clean as it destroys sanitized characters $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); } else { $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); } } if ( $value ) { $default_attributes[ $taxonomy ] = $value; } } } } $product->set_default_attributes( $default_attributes ); } return $product; } /** * Save product meta. * * @since 2.2 * @param WC_Product $product * @param array $data * @return WC_Product * @throws WC_API_Exception */ protected function save_product_meta( $product, $data ) { global $wpdb; // Virtual. if ( isset( $data['virtual'] ) ) { $product->set_virtual( $data['virtual'] ); } // Tax status. if ( isset( $data['tax_status'] ) ) { $product->set_tax_status( wc_clean( $data['tax_status'] ) ); } // Tax Class. if ( isset( $data['tax_class'] ) ) { $product->set_tax_class( wc_clean( $data['tax_class'] ) ); } // Catalog Visibility. if ( isset( $data['catalog_visibility'] ) ) { $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); } // Purchase Note. if ( isset( $data['purchase_note'] ) ) { $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); } // Featured Product. if ( isset( $data['featured'] ) ) { $product->set_featured( $data['featured'] ); } // Shipping data. $product = $this->save_product_shipping_data( $product, $data ); // SKU. if ( isset( $data['sku'] ) ) { $sku = $product->get_sku(); $new_sku = wc_clean( $data['sku'] ); if ( '' == $new_sku ) { $product->set_sku( '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); if ( ! $unique_sku ) { throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); } else { $product->set_sku( $new_sku ); } } else { $product->set_sku( '' ); } } } // Attributes. if ( isset( $data['attributes'] ) ) { $attributes = array(); foreach ( $data['attributes'] as $attribute ) { $is_taxonomy = 0; $taxonomy = 0; if ( ! isset( $attribute['name'] ) ) { continue; } $attribute_slug = sanitize_title( $attribute['name'] ); if ( isset( $attribute['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); $attribute_slug = sanitize_title( $attribute['slug'] ); } if ( $taxonomy ) { $is_taxonomy = 1; } if ( $is_taxonomy ) { $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names. $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } // Update post terms if ( taxonomy_exists( $taxonomy ) ) { wp_set_object_terms( $product->get_id(), $values, $taxonomy ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $taxonomy ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Array based. if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; // Text based, separate by pipe. } else { $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); } // Custom attribute - Add attribute to array and set the values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute['name'] ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices. $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $data['regular_price'] ) ) { $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; $product->set_regular_price( $regular_price ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; $product->set_sale_price( $sale_price ); } if ( isset( $data['sale_price_dates_from'] ) ) { $date_from = $data['sale_price_dates_from']; } else { $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; } if ( isset( $data['sale_price_dates_to'] ) ) { $date_to = $data['sale_price_dates_to']; } else { $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; } if ( $date_to && ! $date_from ) { $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); } $product->set_date_on_sale_to( $date_to ); $product->set_date_on_sale_from( $date_from ); if ( $product->is_on_sale( 'edit' ) ) { $product->set_price( $product->get_sale_price( 'edit' ) ); } else { $product->set_price( $product->get_regular_price( 'edit' ) ); } } // Product parent ID for groups. if ( isset( $data['parent_id'] ) ) { $product->set_parent_id( absint( $data['parent_id'] ) ); } // Sold Individually. if ( isset( $data['sold_individually'] ) ) { $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); } // Stock status. if ( isset( $data['in_stock'] ) ) { $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; } else { $stock_status = $product->get_stock_status(); if ( '' === $stock_status ) { $stock_status = 'instock'; } } // Stock Data. if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $data['managing_stock'] ) ) { $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; $product->set_manage_stock( $managing_stock ); } else { $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; } // Backorders. if ( isset( $data['backorders'] ) ) { if ( 'notify' === $data['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; } $product->set_backorders( $backorders ); } else { $backorders = $product->get_backorders(); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( 'yes' == $managing_stock ) { $product->set_backorders( $backorders ); // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity. if ( isset( $data['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); } elseif ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_backorders( $backorders ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells. if ( isset( $data['upsell_ids'] ) ) { $upsells = array(); $ids = $data['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } $product->set_upsell_ids( $upsells ); } else { $product->set_upsell_ids( array() ); } } // Cross sells. if ( isset( $data['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $data['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } $product->set_cross_sell_ids( $crosssells ); } else { $product->set_cross_sell_ids( array() ); } } // Product categories. if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { $product->set_category_ids( $data['categories'] ); } // Product tags. if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { $product->set_tag_ids( $data['tags'] ); } // Downloadable. if ( isset( $data['downloadable'] ) ) { $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; $product->set_downloadable( $is_downloadable ); } else { $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; } // Downloadable options. if ( 'yes' == $is_downloadable ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { $product->set_download_limit( $data['download_limit'] ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { $product->set_download_expiry( $data['download_expiry'] ); } } // Product url. if ( $product->is_type( 'external' ) ) { if ( isset( $data['product_url'] ) ) { $product->set_product_url( $data['product_url'] ); } if ( isset( $data['button_text'] ) ) { $product->set_button_text( $data['button_text'] ); } } // Reviews allowed. if ( isset( $data['reviews_allowed'] ) ) { $product->set_reviews_allowed( $data['reviews_allowed'] ); } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $data ); } // Do action for product type do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); return $product; } /** * Save variations. * * @since 2.2 * * @param WC_Product $product * @param array $request * * @return bool * @throws WC_API_Exception */ protected function save_variations( $product, $request ) { global $wpdb; $id = $product->get_id(); $variations = $request['variations']; $attributes = $product->get_attributes(); foreach ( $variations as $menu_order => $data ) { $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; $variation = new WC_Product_Variation( $variation_id ); // Create initial name and status. if ( ! $variation->get_slug() ) { /* translators: 1: variation id 2: product name */ $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); } // Parent ID. $variation->set_parent_id( $product->get_id() ); // Menu order. $variation->set_menu_order( $menu_order ); // Status. if ( isset( $data['visible'] ) ) { $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); } // SKU. if ( isset( $data['sku'] ) ) { $variation->set_sku( wc_clean( $data['sku'] ) ); } // Thumbnail. if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { $image = current( $data['image'] ); if ( is_array( $image ) ) { $image['position'] = 0; } $variation = $this->save_product_images( $variation, array( $image ) ); } // Virtual variation. if ( isset( $data['virtual'] ) ) { $variation->set_virtual( $data['virtual'] ); } // Downloadable variation. if ( isset( $data['downloadable'] ) ) { $is_downloadable = $data['downloadable']; $variation->set_downloadable( $is_downloadable ); } else { $is_downloadable = $variation->get_downloadable(); } // Downloads. if ( $is_downloadable ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { $variation->set_download_limit( $data['download_limit'] ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { $variation->set_download_expiry( $data['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $data ); // Stock handling. $manage_stock = (bool) $variation->get_manage_stock(); if ( isset( $data['managing_stock'] ) ) { $manage_stock = $data['managing_stock']; } $variation->set_manage_stock( $manage_stock ); $stock_status = $variation->get_stock_status(); if ( isset( $data['in_stock'] ) ) { $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; } $variation->set_stock_status( $stock_status ); $backorders = $variation->get_backorders(); if ( isset( $data['backorders'] ) ) { $backorders = $data['backorders']; } $variation->set_backorders( $backorders ); if ( $manage_stock ) { if ( isset( $data['stock_quantity'] ) ) { $variation->set_stock_quantity( $data['stock_quantity'] ); } elseif ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); $variation->set_stock_quantity( $stock_quantity ); } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); } // Regular Price. if ( isset( $data['regular_price'] ) ) { $variation->set_regular_price( $data['regular_price'] ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $variation->set_sale_price( $data['sale_price'] ); } if ( isset( $data['sale_price_dates_from'] ) ) { $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); } if ( isset( $data['sale_price_dates_to'] ) ) { $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); } // Tax class. if ( isset( $data['tax_class'] ) ) { $variation->set_tax_class( $data['tax_class'] ); } // Description. if ( isset( $data['description'] ) ) { $variation->set_description( wp_kses_post( $data['description'] ) ); } // Update taxonomies. if ( isset( $data['attributes'] ) ) { $_attributes = array(); foreach ( $data['attributes'] as $attribute_key => $attribute ) { if ( ! isset( $attribute['name'] ) ) { continue; } $taxonomy = 0; $_attribute = array(); if ( isset( $attribute['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); } if ( ! $taxonomy ) { $taxonomy = sanitize_title( $attribute['name'] ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; } if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { $_attribute_key = sanitize_title( $_attribute['name'] ); if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { // Don't use wc_clean as it destroys sanitized characters. $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; } else { $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; } $_attributes[ $_attribute_key ] = $_attribute_value; } } $variation->set_attributes( $_attributes ); } $variation->save(); do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); } return true; } /** * Save product shipping data * * @since 2.2 * @param WC_Product $product * @param array $data * @return WC_Product */ private function save_product_shipping_data( $product, $data ) { if ( isset( $data['weight'] ) ) { $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); } // Product dimensions if ( isset( $data['dimensions'] ) ) { // Height if ( isset( $data['dimensions']['height'] ) ) { $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width if ( isset( $data['dimensions']['width'] ) ) { $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); } // Length if ( isset( $data['dimensions']['length'] ) ) { $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Virtual if ( isset( $data['virtual'] ) ) { $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; if ( 'yes' == $virtual ) { $product->set_weight( '' ); $product->set_height( '' ); $product->set_length( '' ); $product->set_width( '' ); } } // Shipping class if ( isset( $data['shipping_class'] ) ) { $data_store = $product->get_data_store(); $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } return $product; } /** * Save downloadable files * * @since 2.2 * @param WC_Product $product * @param array $downloads * @param int $deprecated Deprecated since 3.0. * @return WC_Product */ private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { if ( $deprecated ) { wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); } $files = array(); foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( empty( $file['file'] ) ) { continue; } $download = new WC_Product_Download(); $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); $files[] = $download; } $product->set_downloads( $files ); return $product; } /** * Get attribute taxonomy by slug. * * @since 2.2 * @param string $slug * @return string|null */ private function get_attribute_taxonomy_by_slug( $slug ) { $taxonomy = null; $attribute_taxonomies = wc_get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $key => $tax ) { if ( $slug == $tax->attribute_name ) { $taxonomy = 'pa_' . $tax->attribute_name; break; } } return $taxonomy; } /** * Get the images for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_images( $product ) { $images = $attachment_ids = array(); $product_image = $product->get_image_id(); // Add featured image. if ( ! empty( $product_image ) ) { $attachment_ids[] = $product_image; } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => (int) $position, ); } // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'created_at' => $this->server->format_datetime( time() ), // Default to now. 'updated_at' => $this->server->format_datetime( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Save product images. * * @since 2.2 * @param WC_Product $product * @param array $images * @throws WC_API_Exception * @return WC_Product */ protected function save_product_images( $product, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { if ( isset( $image['position'] ) && 0 == $image['position'] ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); } $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); } $product->set_image_id( $attachment_id ); } else { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); } $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); } $gallery[] = $attachment_id; } // Set the image alt if present. if ( ! empty( $image['alt'] ) && $attachment_id ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image title if present. if ( ! empty( $image['title'] ) && $attachment_id ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); } } if ( ! empty( $gallery ) ) { $product->set_gallery_image_ids( $gallery ); } } else { $product->set_image_id( '' ); $product->set_gallery_image_ids( array() ); } return $product; } /** * Upload image from URL * * @since 2.2 * @param string $image_url * @return int|WP_Error attachment id */ public function upload_product_image( $image_url ) { return $this->upload_image_from_url( $image_url, 'product_image' ); } /** * Upload product category image from URL. * * @since 2.5.0 * @param string $image_url * @return int|WP_Error attachment id */ public function upload_product_category_image( $image_url ) { return $this->upload_image_from_url( $image_url, 'product_category_image' ); } /** * Upload image from URL. * * @throws WC_API_Exception * * @since 2.5.0 * @param string $image_url * @param string $upload_for * @return array */ protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { $upload = wc_rest_upload_image_from_url( $image_url ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); } do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); return $upload; } /** * Sets product image as attachment and returns the attachment ID. * * @since 2.2 * @param array $upload * @param int $id * @return int */ protected function set_product_image_as_attachment( $upload, $id ) { return $this->set_uploaded_image_as_attachment( $upload, $id ); } /** * Sets uploaded category image as attachment and returns the attachment ID. * * @since 2.5.0 * @param integer $upload Upload information from wp_upload_bits * @return int Attachment ID */ protected function set_product_category_image_as_attachment( $upload ) { return $this->set_uploaded_image_as_attachment( $upload ); } /** * Set uploaded image as attachment. * * @since 2.5.0 * @param array $upload Upload information from wp_upload_bits * @param int $id Post ID. Default to 0. * @return int Attachment ID */ protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { $info = wp_check_filetype( $upload['file'] ); $title = ''; $content = ''; if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = wc_clean( $image_meta['title'] ); } if ( trim( $image_meta['caption'] ) ) { $content = wc_clean( $image_meta['caption'] ); } } $attachment = array( 'post_mime_type' => $info['type'], 'guid' => $upload['url'], 'post_parent' => $id, 'post_title' => $title, 'post_content' => $content, ); $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); if ( ! is_wp_error( $attachment_id ) ) { wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); } return $attachment_id; } /** * Get attribute options. * * @param int $product_id * @param array $attribute * @return array */ protected function get_attribute_options( $product_id, $attribute ) { if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); } elseif ( isset( $attribute['value'] ) ) { return array_map( 'trim', explode( '|', $attribute['value'] ) ); } return array(); } /** * Get the attributes for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // variation attributes foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` $attributes[] = array( 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => wc_attribute_label( $attribute['name'], $product ), 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } } return $attributes; } /** * Get the downloads for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_downloads() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // do not cast as int as this is a hash 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get a listing of product attributes * * @since 2.5.0 * * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_attributes( $fields = null ) { try { // Permissions check. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); } $product_attributes = array(); $attribute_taxonomies = wc_get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $attribute ) { $product_attributes[] = array( 'id' => intval( $attribute->attribute_id ), 'name' => $attribute->attribute_label, 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), 'type' => $attribute->attribute_type, 'order_by' => $attribute->attribute_orderby, 'has_archives' => (bool) $attribute->attribute_public, ); } return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product attribute for the given ID * * @since 2.5.0 * * @param string $id product attribute term ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_attribute( $id, $fields = null ) { global $wpdb; try { $id = absint( $id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); } $attribute = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $product_attribute = array( 'id' => intval( $attribute->attribute_id ), 'name' => $attribute->attribute_label, 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), 'type' => $attribute->attribute_type, 'order_by' => $attribute->attribute_orderby, 'has_archives' => (bool) $attribute->attribute_public, ); return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Validate attribute data. * * @since 2.5.0 * @param string $name * @param string $slug * @param string $type * @param string $order_by * @param bool $new_data * @return bool * @throws WC_API_Exception */ protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { if ( empty( $name ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); } if ( strlen( $slug ) >= 28 ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); } // Validate the attribute type if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); } // Validate the attribute order by if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); } return true; } /** * Create a new product attribute. * * @since 2.5.0 * * @param array $data Posted data. * * @return array|WP_Error */ public function create_product_attribute( $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); } $data = $data['product_attribute']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); if ( ! isset( $data['name'] ) ) { $data['name'] = ''; } // Set the attribute slug. if ( ! isset( $data['slug'] ) ) { $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); } else { $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); } // Set attribute type when not sent. if ( ! isset( $data['type'] ) ) { $data['type'] = 'select'; } // Set order by when not sent. if ( ! isset( $data['order_by'] ) ) { $data['order_by'] = 'menu_order'; } // Validate the attribute data. $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); $insert = $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_label' => $data['name'], 'attribute_name' => $data['slug'], 'attribute_type' => $data['type'], 'attribute_orderby' => $data['order_by'], 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, ), array( '%s', '%s', '%s', '%s', '%d' ) ); // Checks for an error in the product creation. if ( is_wp_error( $insert ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); } $id = $wpdb->insert_id; do_action( 'woocommerce_api_create_product_attribute', $id, $data ); // Clear transients. wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); $this->server->send_status( 201 ); return $this->get_product_attribute( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product attribute. * * @since 2.5.0 * * @param int $id the attribute ID. * @param array $data * * @return array|WP_Error */ public function edit_product_attribute( $id, $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); } $id = absint( $id ); $data = $data['product_attribute']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); $attribute = $this->get_product_attribute( $id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; if ( isset( $data['slug'] ) ) { $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); } else { $attribute_slug = $attribute['product_attribute']['slug']; } $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); if ( isset( $data['has_archives'] ) ) { $attribute_public = true === $data['has_archives'] ? 1 : 0; } else { $attribute_public = $attribute['product_attribute']['has_archives']; } // Validate the attribute data. $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); $update = $wpdb->update( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_label' => $attribute_name, 'attribute_name' => $attribute_slug, 'attribute_type' => $attribute_type, 'attribute_orderby' => $attribute_order_by, 'attribute_public' => $attribute_public, ), array( 'attribute_id' => $id ), array( '%s', '%s', '%s', '%s', '%d' ), array( '%d' ) ); // Checks for an error in the product creation. if ( false === $update ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); } do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); // Clear transients. wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return $this->get_product_attribute( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product attribute. * * @since 2.5.0 * * @param int $id the product attribute ID. * * @return array|WP_Error */ public function delete_product_attribute( $id ) { global $wpdb; try { // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); } $id = absint( $id ); $attribute_name = $wpdb->get_var( $wpdb->prepare( " SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); if ( is_null( $attribute_name ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $deleted = $wpdb->delete( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_id' => $id ), array( '%d' ) ); if ( false === $deleted ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); if ( taxonomy_exists( $taxonomy ) ) { $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); foreach ( $terms as $term ) { wp_delete_term( $term->term_id, $taxonomy ); } } do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); // Clear transients. wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a listing of product attribute terms. * * @since 2.5.0 * * @param int $attribute_id Attribute ID. * @param string|null $fields Fields to limit response to. * * @return array|WP_Error */ public function get_product_attribute_terms( $attribute_id, $fields = null ) { try { // Permissions check. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); } $attribute_id = absint( $attribute_id ); $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); if ( ! $taxonomy ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); $attribute_terms = array(); foreach ( $terms as $term ) { $attribute_terms[] = array( 'id' => $term->term_id, 'slug' => $term->slug, 'name' => $term->name, 'count' => $term->count, ); } return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product attribute term for the given ID. * * @since 2.5.0 * * @param int $attribute_id Attribute ID. * @param string $id Product attribute term ID. * @param string|null $fields Fields to limit response to. * * @return array|WP_Error */ public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { global $wpdb; try { $id = absint( $id ); $attribute_id = absint( $attribute_id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); if ( ! $taxonomy ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $term = get_term( $id, $taxonomy ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); } $attribute_term = array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'count' => $term->count, ); return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product attribute term. * * @since 2.5.0 * * @param int $attribute_id Attribute ID. * @param array $data Posted data. * * @return array|WP_Error */ public function create_product_attribute_term( $attribute_id, $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute_term'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); } $data = $data['product_attribute_term']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); if ( ! $taxonomy ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); // Check if attribute term name is specified. if ( ! isset( $data['name'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); } $args = array(); // Set the attribute term slug. if ( isset( $data['slug'] ) ) { $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); } $term = wp_insert_term( $data['name'], $taxonomy, $args ); // Checks for an error in the term creation. if ( is_wp_error( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); } $id = $term['term_id']; do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); $this->server->send_status( 201 ); return $this->get_product_attribute_term( $attribute_id, $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product attribute term. * * @since 2.5.0 * * @param int $attribute_id Attribute ID. * @param int $id the attribute ID. * @param array $data * * @return array|WP_Error */ public function edit_product_attribute_term( $attribute_id, $id, $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute_term'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); } $id = absint( $id ); $data = $data['product_attribute_term']; // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); if ( ! $taxonomy ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); $args = array(); // Update name. if ( isset( $data['name'] ) ) { $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); } // Update slug. if ( isset( $data['slug'] ) ) { $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); } $term = wp_update_term( $id, $taxonomy, $args ); if ( is_wp_error( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); } do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); return $this->get_product_attribute_term( $attribute_id, $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product attribute term. * * @since 2.5.0 * * @param int $attribute_id Attribute ID. * @param int $id the product attribute ID. * * @return array|WP_Error */ public function delete_product_attribute_term( $attribute_id, $id ) { global $wpdb; try { // Check permissions. if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); if ( ! $taxonomy ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $id = absint( $id ); $term = wp_delete_term( $id, $taxonomy ); if ( ! $term ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); } elseif ( is_wp_error( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); } do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Clear product * * @param int $product_id */ protected function clear_product( $product_id ) { if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { return; } // Delete product attachments $attachments = get_children( array( 'post_parent' => $product_id, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product $product = wc_get_product( $product_id ); $product->delete( true ); } /** * Bulk update or insert products * Accepts an array with products in the formats supported by * WC_API_Products->create_product() and WC_API_Products->edit_product() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['products'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); } $data = $data['products']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $products = array(); foreach ( $data as $_product ) { $product_id = 0; $product_sku = ''; // Try to get the product ID if ( isset( $_product['id'] ) ) { $product_id = intval( $_product['id'] ); } if ( ! $product_id && isset( $_product['sku'] ) ) { $product_sku = wc_clean( $_product['sku'] ); $product_id = wc_get_product_id_by_sku( $product_sku ); } if ( $product_id ) { // Product exists / edit product $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); if ( is_wp_error( $edit ) ) { $products[] = array( 'id' => $product_id, 'sku' => $product_sku, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $products[] = $edit['product']; } } else { // Product don't exists / create product $new = $this->create_product( array( 'product' => $_product ) ); if ( is_wp_error( $new ) ) { $products[] = array( 'id' => $product_id, 'sku' => $product_sku, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $products[] = $new['product']; } } } return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a listing of product shipping classes. * * @since 2.5.0 * @param string|null $fields Fields to limit response to * @return array|WP_Error List of product shipping classes if succeed, * otherwise WP_Error will be returned */ public function get_product_shipping_classes( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); } $product_shipping_classes = array(); $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); foreach ( $terms as $term_id ) { $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); } return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product shipping class for the given ID. * * @since 2.5.0 * @param string $id Product shipping class term ID * @param string|null $fields Fields to limit response to * @return array|WP_Error Product shipping class if succeed, otherwise * WP_Error will be returned */ public function get_product_shipping_class( $id, $fields = null ) { try { $id = absint( $id ); if ( ! $id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); } $term = get_term( $id, 'product_shipping_class' ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); } $term_id = intval( $term->term_id ); $product_shipping_class = array( 'id' => $term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent, 'description' => $term->description, 'count' => intval( $term->count ), ); return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product shipping class. * * @since 2.5.0 * @param array $data Posted data * @return array|WP_Error Product shipping class if succeed, otherwise * WP_Error will be returned */ public function create_product_shipping_class( $data ) { global $wpdb; try { if ( ! isset( $data['product_shipping_class'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); } $defaults = array( 'name' => '', 'slug' => '', 'description' => '', 'parent' => 0, ); $data = wp_parse_args( $data['product_shipping_class'], $defaults ); $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); // Check parent. $data['parent'] = absint( $data['parent'] ); if ( $data['parent'] ) { $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); if ( ! $parent ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); } } $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); if ( is_wp_error( $insert ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); } $id = $insert['term_id']; do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); $this->server->send_status( 201 ); return $this->get_product_shipping_class( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product shipping class. * * @since 2.5.0 * @param int $id Product shipping class term ID * @param array $data Posted data * @return array|WP_Error Product shipping class if succeed, otherwise * WP_Error will be returned */ public function edit_product_shipping_class( $id, $data ) { global $wpdb; try { if ( ! isset( $data['product_shipping_class'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); } $id = absint( $id ); $data = $data['product_shipping_class']; // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); $shipping_class = $this->get_product_shipping_class( $id ); if ( is_wp_error( $shipping_class ) ) { return $shipping_class; } $update = wp_update_term( $id, 'product_shipping_class', $data ); if ( is_wp_error( $update ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); } do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); return $this->get_product_shipping_class( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product shipping class. * * @since 2.5.0 * @param int $id Product shipping class term ID * @return array|WP_Error Success message if succeed, otherwise WP_Error * will be returned */ public function delete_product_shipping_class( $id ) { global $wpdb; try { // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); } $id = absint( $id ); $deleted = wp_delete_term( $id, 'product_shipping_class' ); if ( ! $deleted || is_wp_error( $deleted ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); } do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v3/class-wc-api-webhooks.php 0000644 00000036334 15132754524 0015553 0 ustar 00 <?php /** * WooCommerce API Webhooks class * * Handles requests to the /webhooks endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Webhooks extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/webhooks'; /** * Register the routes for this class * * @since 2.2 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET|POST /webhooks $routes[ $this->base ] = array( array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ), array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /webhooks/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ), ); # GET|PUT|DELETE /webhooks/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ), array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ), ); # GET /webhooks/<id>/deliveries $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array( array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), ); # GET /webhooks/<webhook_id>/deliveries/<id> $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array( array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get all webhooks * * @since 2.2 * * @param array $fields * @param array $filter * @param string $status * @param int $page * * @return array */ public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $filter['page'] = $page; $query = $this->query_webhooks( $filter ); $webhooks = array(); foreach ( $query['results'] as $webhook_id ) { $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); } $this->server->add_pagination_headers( $query['headers'] ); return array( 'webhooks' => $webhooks ); } /** * Get the webhook for the given ID * * @since 2.2 * @param int $id webhook ID * @param array $fields * @return array|WP_Error */ public function get_webhook( $id, $fields = null ) { // ensure webhook ID is valid & user has permission to read $id = $this->validate_request( $id, 'shop_webhook', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $webhook = wc_get_webhook( $id ); $webhook_data = array( 'id' => $webhook->get_id(), 'name' => $webhook->get_name(), 'status' => $webhook->get_status(), 'topic' => $webhook->get_topic(), 'resource' => $webhook->get_resource(), 'event' => $webhook->get_event(), 'hooks' => $webhook->get_hooks(), 'delivery_url' => $webhook->get_delivery_url(), 'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. ); return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) ); } /** * Get the total number of webhooks * * @since 2.2 * * @param string $status * @param array $filter * * @return array|WP_Error */ public function get_webhooks_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 ); } if ( ! empty( $status ) ) { $filter['status'] = $status; } $query = $this->query_webhooks( $filter ); return array( 'count' => $query['headers']->total ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create an webhook * * @since 2.2 * * @param array $data parsed webhook data * * @return array|WP_Error */ public function create_webhook( $data ) { try { if ( ! isset( $data['webhook'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 ); } $data = $data['webhook']; // permission check if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this ); // validate topic if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 ); } // validate delivery URL if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); } $webhook_data = apply_filters( 'woocommerce_new_webhook_data', array( 'post_type' => 'shop_webhook', 'post_status' => 'publish', 'ping_status' => 'closed', 'post_author' => get_current_user_id(), 'post_password' => 'webhook_' . wp_generate_password(), 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ), $data, $this ); $webhook = new WC_Webhook(); $webhook->set_name( $webhook_data['post_title'] ); $webhook->set_user_id( $webhook_data['post_author'] ); $webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' ); $webhook->set_topic( $data['topic'] ); $webhook->set_delivery_url( $data['delivery_url'] ); $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) ); $webhook->set_api_version( 'legacy_v3' ); $webhook->save(); $webhook->deliver_ping(); // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this ); return $this->get_webhook( $webhook->get_id() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a webhook * * @since 2.2 * * @param int $id webhook ID * @param array $data parsed webhook data * * @return array|WP_Error */ public function edit_webhook( $id, $data ) { try { if ( ! isset( $data['webhook'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 ); } $data = $data['webhook']; $id = $this->validate_request( $id, 'shop_webhook', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this ); $webhook = wc_get_webhook( $id ); // update topic if ( ! empty( $data['topic'] ) ) { if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { $webhook->set_topic( $data['topic'] ); } else { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 ); } } // update delivery URL if ( ! empty( $data['delivery_url'] ) ) { if ( wc_is_valid_url( $data['delivery_url'] ) ) { $webhook->set_delivery_url( $data['delivery_url'] ); } else { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); } } // update secret if ( ! empty( $data['secret'] ) ) { $webhook->set_secret( $data['secret'] ); } // update status if ( ! empty( $data['status'] ) ) { $webhook->set_status( $data['status'] ); } // update name if ( ! empty( $data['name'] ) ) { $webhook->set_name( $data['name'] ); } $webhook->save(); do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this ); return $this->get_webhook( $webhook->get_id() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a webhook * * @since 2.2 * @param int $id webhook ID * @return array|WP_Error */ public function delete_webhook( $id ) { $id = $this->validate_request( $id, 'shop_webhook', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_webhook', $id, $this ); $webhook = wc_get_webhook( $id ); return $webhook->delete( true ); } /** * Helper method to get webhook post objects * * @since 2.2 * @param array $args Request arguments for filtering query. * @return array */ private function query_webhooks( $args ) { $args = $this->merge_query_args( array(), $args ); $args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) ); if ( empty( $args['offset'] ) ) { $args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0; } $page = $args['paged']; unset( $args['paged'], $args['posts_per_page'] ); if ( isset( $args['s'] ) ) { $args['search'] = $args['s']; unset( $args['s'] ); } // Post type to webhook status. if ( ! empty( $args['post_status'] ) ) { $args['status'] = $args['post_status']; unset( $args['post_status'] ); } if ( ! empty( $args['post__in'] ) ) { $args['include'] = $args['post__in']; unset( $args['post__in'] ); } if ( ! empty( $args['date_query'] ) ) { foreach ( $args['date_query'] as $date_query ) { if ( 'post_date_gmt' === $date_query['column'] ) { $args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; $args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; } elseif ( 'post_modified_gmt' === $date_query['column'] ) { $args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; $args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; } } unset( $args['date_query'] ); } $args['paginate'] = true; // Get the webhooks. $data_store = WC_Data_Store::load( 'webhook' ); $results = $data_store->search_webhooks( $args ); // Get total items. $headers = new stdClass; $headers->page = $page; $headers->total = $results->total; $headers->is_single = $args['limit'] > $headers->total; $headers->total_pages = $results->max_num_pages; return array( 'results' => $results->webhooks, 'headers' => $headers, ); } /** * Get deliveries for a webhook * * @since 2.2 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @param string $webhook_id webhook ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_webhook_deliveries( $webhook_id, $fields = null ) { // Ensure ID is valid webhook ID $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); if ( is_wp_error( $webhook_id ) ) { return $webhook_id; } return array( 'webhook_deliveries' => array() ); } /** * Get the delivery log for the given webhook ID and delivery ID * * @since 2.2 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @param string $webhook_id webhook ID * @param string $id delivery log ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_webhook_delivery( $webhook_id, $id, $fields = null ) { try { // Validate webhook ID $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); if ( is_wp_error( $webhook_id ) ) { return $webhook_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 ); } $webhook = new WC_Webhook( $webhook_id ); $log = 0; if ( ! $log ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); } return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer. * 2) the ID returns a valid post object and matches the provided post type. * 3) the current user has the proper permissions to read/edit/delete the post. * * @since 3.3.0 * @param string|int $id The post ID * @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`. * @param string $context The context of the request, either `read`, `edit` or `delete`. * @return int|WP_Error Valid post ID or WP_Error if any of the checks fails. */ protected function validate_request( $id, $type, $context ) { $id = absint( $id ); // Validate ID. if ( empty( $id ) ) { return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); } $webhook = wc_get_webhook( $id ); if ( null === $webhook ) { return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) ); } // Validate permissions. switch ( $context ) { case 'read': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; } return $id; } } includes/legacy/api/v3/class-wc-api-json-handler.php 0000644 00000003656 15132754524 0016317 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_JSON_Handler implements WC_API_Handler { /** * Get the content type for the response * * @since 2.1 * @return string */ public function get_content_type() { return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) ); } /** * Parse the raw request body entity * * @since 2.1 * @param string $body the raw request body * @return array|mixed */ public function parse_body( $body ) { return json_decode( $body, true ); } /** * Generate a JSON response given an array of data * * @since 2.1 * @param array $data the response data * @return string */ public function generate_response( $data ) { if ( isset( $_GET['_jsonp'] ) ) { if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); } $jsonp_callback = $_GET['_jsonp']; if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); } WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks. // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; } return wp_json_encode( $data ); } } includes/legacy/api/v3/interface-wc-api-handler.php 0000644 00000001515 15132754524 0016173 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } interface WC_API_Handler { /** * Get the content type for the response * * This should return the proper HTTP content-type for the response * * @since 2.1 * @return string */ public function get_content_type(); /** * Parse the raw request body entity into an array * * @since 2.1 * @param string $data * @return array */ public function parse_body( $data ); /** * Generate a response from an array of data * * @since 2.1 * @param array $data * @return string */ public function generate_response( $data ); } includes/legacy/api/v3/class-wc-api-server.php 0000644 00000051035 15132754524 0015233 0 ustar 00 <?php /** * WooCommerce API * * Handles REST API requests * * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API) * Many thanks to Ryan McCue and any other contributors! * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } require_once ABSPATH . 'wp-admin/includes/admin.php'; class WC_API_Server { const METHOD_GET = 1; const METHOD_POST = 2; const METHOD_PUT = 4; const METHOD_PATCH = 8; const METHOD_DELETE = 16; const READABLE = 1; // GET const CREATABLE = 2; // POST const EDITABLE = 14; // POST | PUT | PATCH const DELETABLE = 16; // DELETE const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE /** * Does the endpoint accept a raw request body? */ const ACCEPT_RAW_DATA = 64; /** Does the endpoint accept a request body? (either JSON or XML) */ const ACCEPT_DATA = 128; /** * Should we hide this endpoint from the index? */ const HIDDEN_ENDPOINT = 256; /** * Map of HTTP verbs to constants * @var array */ public static $method_map = array( 'HEAD' => self::METHOD_GET, 'GET' => self::METHOD_GET, 'POST' => self::METHOD_POST, 'PUT' => self::METHOD_PUT, 'PATCH' => self::METHOD_PATCH, 'DELETE' => self::METHOD_DELETE, ); /** * Requested path (relative to the API root, wp-json.php) * * @var string */ public $path = ''; /** * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) * * @var string */ public $method = 'HEAD'; /** * Request parameters * * This acts as an abstraction of the superglobals * (GET => $_GET, POST => $_POST) * * @var array */ public $params = array( 'GET' => array(), 'POST' => array() ); /** * Request headers * * @var array */ public $headers = array(); /** * Request files (matches $_FILES) * * @var array */ public $files = array(); /** * Request/Response handler, either JSON by default * or XML if requested by client * * @var WC_API_Handler */ public $handler; /** * Setup class and set request/response handler * * @since 2.1 * @param $path */ public function __construct( $path ) { if ( empty( $path ) ) { if ( isset( $_SERVER['PATH_INFO'] ) ) { $path = $_SERVER['PATH_INFO']; } else { $path = '/'; } } $this->path = $path; $this->method = $_SERVER['REQUEST_METHOD']; $this->params['GET'] = $_GET; $this->params['POST'] = $_POST; $this->headers = $this->get_headers( $_SERVER ); $this->files = $_FILES; // Compatibility for clients that can't use PUT/PATCH/DELETE if ( isset( $_GET['_method'] ) ) { $this->method = strtoupper( $_GET['_method'] ); } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } // load response handler $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); $this->handler = new $handler_class(); } /** * Check authentication for the request * * @since 2.1 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login */ public function check_authentication() { // allow plugins to remove default authentication or add their own authentication $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); if ( is_a( $user, 'WP_User' ) ) { // API requests run under the context of the authenticated user wp_set_current_user( $user->ID ); } elseif ( ! is_wp_error( $user ) ) { // WP_Errors are handled in serve_request() $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); } return $user; } /** * Convert an error to an array * * This iterates over all error codes and messages to change it into a flat * array. This enables simpler client behaviour, as it is represented as a * list in JSON rather than an object/map * * @since 2.1 * @param WP_Error $error * @return array List of associative arrays with code and message keys */ protected function error_to_array( $error ) { $errors = array(); foreach ( (array) $error->errors as $code => $messages ) { foreach ( (array) $messages as $message ) { $errors[] = array( 'code' => $code, 'message' => $message ); } } return array( 'errors' => $errors ); } /** * Handle serving an API request * * Matches the current server URI to a route and runs the first matching * callback then outputs a JSON representation of the returned value. * * @since 2.1 * @uses WC_API_Server::dispatch() */ public function serve_request() { do_action( 'woocommerce_api_server_before_serve', $this ); $this->header( 'Content-Type', $this->handler->get_content_type(), true ); // the API is enabled by default if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { $this->send_status( 404 ); echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); return; } $result = $this->check_authentication(); // if authorization check was successful, dispatch the request if ( ! is_wp_error( $result ) ) { $result = $this->dispatch(); } // handle any dispatch errors if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); if ( is_array( $data ) && isset( $data['status'] ) ) { $this->send_status( $data['status'] ); } $result = $this->error_to_array( $result ); } // This is a filter rather than an action, since this is designed to be // re-entrant if needed $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); if ( ! $served ) { if ( 'HEAD' === $this->method ) { return; } echo $this->handler->generate_response( $result ); } } /** * Retrieve the route map * * The route map is an associative array with path regexes as the keys. The * value is an indexed array with the callback function/method as the first * item, and a bitmask of HTTP methods as the second item (see the class * constants). * * Each route can be mapped to more than one callback by using an array of * the indexed arrays. This allows mapping e.g. GET requests to one callback * and POST requests to another. * * Note that the path regexes (array keys) must have @ escaped, as this is * used as the delimiter with preg_match() * * @since 2.1 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` */ public function get_routes() { // index added by default $endpoints = array( '/' => array( array( $this, 'get_index' ), self::READABLE ), ); $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); // Normalise the endpoints foreach ( $endpoints as $route => &$handlers ) { if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { $handlers = array( $handlers ); } } return $endpoints; } /** * Match the request to a callback and call it * * @since 2.1 * @return mixed The value returned by the callback, or a WP_Error instance */ public function dispatch() { switch ( $this->method ) { case 'HEAD' : case 'GET' : $method = self::METHOD_GET; break; case 'POST' : $method = self::METHOD_POST; break; case 'PUT' : $method = self::METHOD_PUT; break; case 'PATCH' : $method = self::METHOD_PATCH; break; case 'DELETE' : $method = self::METHOD_DELETE; break; default : return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); } foreach ( $this->get_routes() as $route => $handlers ) { foreach ( $handlers as $handler ) { $callback = $handler[0]; $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; if ( ! ( $supported & $method ) ) { continue; } $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); if ( ! $match ) { continue; } if ( ! is_callable( $callback ) ) { return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); } $args = array_merge( $args, $this->params['GET'] ); if ( $method & self::METHOD_POST ) { $args = array_merge( $args, $this->params['POST'] ); } if ( $supported & self::ACCEPT_DATA ) { $data = $this->handler->parse_body( $this->get_raw_data() ); $args = array_merge( $args, array( 'data' => $data ) ); } elseif ( $supported & self::ACCEPT_RAW_DATA ) { $data = $this->get_raw_data(); $args = array_merge( $args, array( 'data' => $data ) ); } $args['_method'] = $method; $args['_route'] = $route; $args['_path'] = $this->path; $args['_headers'] = $this->headers; $args['_files'] = $this->files; $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); // Allow plugins to halt the request via this filter if ( is_wp_error( $args ) ) { return $args; } $params = $this->sort_callback_params( $callback, $args ); if ( is_wp_error( $params ) ) { return $params; } return call_user_func_array( $callback, $params ); } } return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); } /** * urldecode deep. * * @since 2.2 * @param string|array $value Data to decode with urldecode. * * @return string|array Decoded data. */ protected function urldecode_deep( $value ) { if ( is_array( $value ) ) { return array_map( array( $this, 'urldecode_deep' ), $value ); } else { return urldecode( $value ); } } /** * Sort parameters by order specified in method declaration * * Takes a callback and a list of available params, then filters and sorts * by the parameters the method actually needs, using the Reflection API * * @since 2.2 * * @param callable|array $callback the endpoint callback * @param array $provided the provided request parameters * * @return array|WP_Error */ protected function sort_callback_params( $callback, $provided ) { if ( is_array( $callback ) ) { $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); } else { $ref_func = new ReflectionFunction( $callback ); } $wanted = $ref_func->getParameters(); $ordered_parameters = array(); foreach ( $wanted as $param ) { if ( isset( $provided[ $param->getName() ] ) ) { // We have this parameters in the list to choose from if ( 'data' == $param->getName() ) { $ordered_parameters[] = $provided[ $param->getName() ]; continue; } $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] ); } elseif ( $param->isDefaultValueAvailable() ) { // We don't have this parameter, but it's optional $ordered_parameters[] = $param->getDefaultValue(); } else { // We don't have this parameter and it wasn't optional, abort! return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); } } return $ordered_parameters; } /** * Get the site index. * * This endpoint describes the capabilities of the site. * * @since 2.3 * @return array Index entity */ public function get_index() { // General site data $available = array( 'store' => array( 'name' => get_option( 'blogname' ), 'description' => get_option( 'blogdescription' ), 'URL' => get_option( 'siteurl' ), 'wc_version' => WC()->version, 'version' => WC_API::VERSION, 'routes' => array(), 'meta' => array( 'timezone' => wc_timezone_string(), 'currency' => get_woocommerce_currency(), 'currency_format' => get_woocommerce_currency_symbol(), 'currency_position' => get_option( 'woocommerce_currency_pos' ), 'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ), 'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ), 'price_num_decimals' => wc_get_price_decimals(), 'tax_included' => wc_prices_include_tax(), 'weight_unit' => get_option( 'woocommerce_weight_unit' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ), 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), 'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ), 'links' => array( 'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/', ), ), ), ); // Find the available routes foreach ( $this->get_routes() as $route => $callbacks ) { $data = array(); $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); foreach ( self::$method_map as $name => $bitmask ) { foreach ( $callbacks as $callback ) { // Skip to the next route if any callback is hidden if ( $callback[1] & self::HIDDEN_ENDPOINT ) { continue 3; } if ( $callback[1] & $bitmask ) { $data['supports'][] = $name; } if ( $callback[1] & self::ACCEPT_DATA ) { $data['accepts_data'] = true; } // For non-variable routes, generate links if ( strpos( $route, '<' ) === false ) { $data['meta'] = array( 'self' => get_woocommerce_api_url( $route ), ); } } } $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); } return apply_filters( 'woocommerce_api_index', $available ); } /** * Send a HTTP status code * * @since 2.1 * @param int $code HTTP status */ public function send_status( $code ) { status_header( $code ); } /** * Send a HTTP header * * @since 2.1 * @param string $key Header key * @param string $value Header value * @param boolean $replace Should we replace the existing header? */ public function header( $key, $value, $replace = true ) { header( sprintf( '%s: %s', $key, $value ), $replace ); } /** * Send a Link header * * @internal The $rel parameter is first, as this looks nicer when sending multiple * * @link http://tools.ietf.org/html/rfc5988 * @link http://www.iana.org/assignments/link-relations/link-relations.xml * * @since 2.1 * @param string $rel Link relation. Either a registered type, or an absolute URL * @param string $link Target IRI for the link * @param array $other Other parameters to send, as an associative array */ public function link_header( $rel, $link, $other = array() ) { $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); foreach ( $other as $key => $value ) { if ( 'title' == $key ) { $value = '"' . $value . '"'; } $header .= '; ' . $key . '=' . $value; } $this->header( 'Link', $header, false ); } /** * Send pagination headers for resources * * @since 2.1 * @param WP_Query|WP_User_Query|stdClass $query */ public function add_pagination_headers( $query ) { // WP_User_Query if ( is_a( $query, 'WP_User_Query' ) ) { $single = count( $query->get_results() ) == 1; $total = $query->get_total(); if ( $query->get( 'number' ) > 0 ) { $page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1; $total_pages = ceil( $total / $query->get( 'number' ) ); } else { $page = 1; $total_pages = 1; } } elseif ( is_a( $query, 'stdClass' ) ) { $page = $query->page; $single = $query->is_single; $total = $query->total; $total_pages = $query->total_pages; // WP_Query } else { $page = $query->get( 'paged' ); $single = $query->is_single(); $total = $query->found_posts; $total_pages = $query->max_num_pages; } if ( ! $page ) { $page = 1; } $next_page = absint( $page ) + 1; if ( ! $single ) { // first/prev if ( $page > 1 ) { $this->link_header( 'first', $this->get_paginated_url( 1 ) ); $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); } // next if ( $next_page <= $total_pages ) { $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); } // last if ( $page != $total_pages ) { $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); } } $this->header( 'X-WC-Total', $total ); $this->header( 'X-WC-TotalPages', $total_pages ); do_action( 'woocommerce_api_pagination_headers', $this, $query ); } /** * Returns the request URL with the page query parameter set to the specified page * * @since 2.1 * @param int $page * @return string */ private function get_paginated_url( $page ) { // remove existing page query param $request = remove_query_arg( 'page' ); // add provided page query param $request = urldecode( add_query_arg( 'page', $page, $request ) ); // get the home host $host = parse_url( get_home_url(), PHP_URL_HOST ); return set_url_scheme( "http://{$host}{$request}" ); } /** * Retrieve the raw request entity (body) * * @since 2.1 * @return string */ public function get_raw_data() { // @codingStandardsIgnoreStart // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6. if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; // @codingStandardsIgnoreEnd } /** * Parse an RFC3339 datetime into a MySQl datetime * * Invalid dates default to unix epoch * * @since 2.1 * @param string $datetime RFC3339 datetime * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) */ public function parse_datetime( $datetime ) { // Strip millisecond precision (a full stop followed by one or more digits) if ( strpos( $datetime, '.' ) !== false ) { $datetime = preg_replace( '/\.\d+/', '', $datetime ); } // default timezone to UTC $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); try { $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { $datetime = new DateTime( '@0' ); } return $datetime->format( 'Y-m-d H:i:s' ); } /** * Format a unix timestamp or MySQL datetime into an RFC3339 datetime * * @since 2.1 * @param int|string $timestamp unix timestamp or MySQL datetime * @param bool $convert_to_utc * @param bool $convert_to_gmt Use GMT timezone. * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { if ( $convert_to_gmt ) { if ( is_numeric( $timestamp ) ) { $timestamp = date( 'Y-m-d H:i:s', $timestamp ); } $timestamp = get_gmt_from_date( $timestamp ); } if ( $convert_to_utc ) { $timezone = new DateTimeZone( wc_timezone_string() ); } else { $timezone = new DateTimeZone( 'UTC' ); } try { if ( is_numeric( $timestamp ) ) { $date = new DateTime( "@{$timestamp}" ); } else { $date = new DateTime( $timestamp, $timezone ); } // convert to UTC by adjusting the time based on the offset of the site's timezone if ( $convert_to_utc ) { $date->modify( -1 * $date->getOffset() . ' seconds' ); } } catch ( Exception $e ) { $date = new DateTime( '@0' ); } return $date->format( 'Y-m-d\TH:i:s\Z' ); } /** * Extract headers from a PHP-style $_SERVER array * * @since 2.1 * @param array $server Associative array similar to $_SERVER * @return array Headers extracted from the input */ public function get_headers( $server ) { $headers = array(); // CONTENT_* headers are not prefixed with HTTP_ $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); foreach ( $server as $key => $value ) { if ( strpos( $key, 'HTTP_' ) === 0 ) { $headers[ substr( $key, 5 ) ] = $value; } elseif ( isset( $additional[ $key ] ) ) { $headers[ $key ] = $value; } } return $headers; } } includes/legacy/api/v3/class-wc-api-coupons.php 0000644 00000050102 15132754524 0015405 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Coupons extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/coupons'; /** * Register the routes for this class * * GET /coupons * GET /coupons/count * GET /coupons/<id> * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /coupons $routes[ $this->base ] = array( array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /coupons/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), ); # GET/PUT/DELETE /coupons/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ), ); # GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores $routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array( array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), ); # POST|PUT /coupons/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all coupons * * @since 2.1 * @param string $fields * @param array $filter * @param int $page * @return array */ public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_coupons( $filter ); $coupons = array(); foreach ( $query->posts as $coupon_id ) { if ( ! $this->is_readable( $coupon_id ) ) { continue; } $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'coupons' => $coupons ); } /** * Get the coupon for the given ID * * @since 2.1 * @param int $id the coupon ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_coupon( $id, $fields = null ) { try { $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $coupon = new WC_Coupon( $id ); if ( 0 === $coupon->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); } $coupon_data = array( 'id' => $coupon->get_id(), 'code' => $coupon->get_code(), 'type' => $coupon->get_discount_type(), 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), 'individual_use' => $coupon->get_individual_use(), 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), 'usage_count' => (int) $coupon->get_usage_count(), 'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times. 'enable_free_shipping' => $coupon->get_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), 'exclude_sale_items' => $coupon->get_exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), 'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ), 'customer_emails' => $coupon->get_email_restrictions(), 'description' => $coupon->get_description(), ); return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of coupons * * @since 2.1 * @param array $filter * @return array|WP_Error */ public function get_coupons_count( $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_coupons' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 ); } $query = $this->query_coupons( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the coupon for the given code * * @since 2.1 * @param string $code the coupon code * @param string $fields fields to include in response * @return int|WP_Error */ public function get_coupon_by_code( $code, $fields = null ) { global $wpdb; try { $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); if ( is_null( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 ); } return $this->get_coupon( $id, $fields ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a coupon * * @since 2.2 * * @param array $data * * @return array|WP_Error */ public function create_coupon( $data ) { global $wpdb; try { if ( ! isset( $data['coupon'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 ); } $data = $data['coupon']; // Check user permission if ( ! current_user_can( 'publish_shop_coupons' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this ); // Check if coupon code is specified if ( ! isset( $data['code'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 ); } $coupon_code = wc_format_coupon_code( $data['code'] ); $id_from_code = wc_get_coupon_id_by_code( $coupon_code ); if ( $id_from_code ) { throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); } $defaults = array( 'type' => 'fixed_cart', 'amount' => 0, 'individual_use' => false, 'product_ids' => array(), 'exclude_product_ids' => array(), 'usage_limit' => '', 'usage_limit_per_user' => '', 'limit_usage_to_x_items' => '', 'usage_count' => '', 'expiry_date' => '', 'enable_free_shipping' => false, 'product_category_ids' => array(), 'exclude_product_category_ids' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'customer_emails' => array(), 'description' => '', ); $coupon_data = wp_parse_args( $data, $defaults ); // Validate coupon types if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); } $new_coupon = array( 'post_title' => $coupon_code, 'post_content' => '', 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_type' => 'shop_coupon', 'post_excerpt' => $coupon_data['description'], ); $id = wp_insert_post( $new_coupon, true ); if ( is_wp_error( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 ); } // Set coupon meta update_post_meta( $id, 'discount_type', $coupon_data['type'] ); update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) ); update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) ); update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) ); update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) ); do_action( 'woocommerce_api_create_coupon', $id, $data ); do_action( 'woocommerce_new_coupon', $id ); $this->server->send_status( 201 ); return $this->get_coupon( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a coupon * * @since 2.2 * * @param int $id the coupon ID * @param array $data * * @return array|WP_Error */ public function edit_coupon( $id, $data ) { try { if ( ! isset( $data['coupon'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 ); } $data = $data['coupon']; $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this ); if ( isset( $data['code'] ) ) { global $wpdb; $coupon_code = wc_format_coupon_code( $data['code'] ); $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); } $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) ); if ( 0 === $updated ) { throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); } } if ( isset( $data['description'] ) ) { $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) ); if ( 0 === $updated ) { throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); } } if ( isset( $data['type'] ) ) { // Validate coupon types if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); } update_post_meta( $id, 'discount_type', $data['type'] ); } if ( isset( $data['amount'] ) ) { update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); } if ( isset( $data['individual_use'] ) ) { update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); } if ( isset( $data['product_ids'] ) ) { update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); } if ( isset( $data['exclude_product_ids'] ) ) { update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); } if ( isset( $data['usage_limit'] ) ) { update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); } if ( isset( $data['usage_limit_per_user'] ) ) { update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); } if ( isset( $data['limit_usage_to_x_items'] ) ) { update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); } if ( isset( $data['usage_count'] ) ) { update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); } if ( isset( $data['expiry_date'] ) ) { update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) ); } if ( isset( $data['enable_free_shipping'] ) ) { update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); } if ( isset( $data['product_category_ids'] ) ) { update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); } if ( isset( $data['exclude_product_category_ids'] ) ) { update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); } if ( isset( $data['exclude_sale_items'] ) ) { update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); } if ( isset( $data['minimum_amount'] ) ) { update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); } if ( isset( $data['maximum_amount'] ) ) { update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); } if ( isset( $data['customer_emails'] ) ) { update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); } do_action( 'woocommerce_api_edit_coupon', $id, $data ); do_action( 'woocommerce_update_coupon', $id ); return $this->get_coupon( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a coupon * * @since 2.2 * * @param int $id the coupon ID * @param bool $force true to permanently delete coupon, false to move to trash * * @return array|int|WP_Error */ public function delete_coupon( $id, $force = false ) { $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_coupon', $id, $this ); return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); } /** * expiry_date format * * @since 2.3.0 * @param string $expiry_date * @param bool $as_timestamp (default: false) * @return string|int */ protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) { if ( '' != $expiry_date ) { if ( $as_timestamp ) { return strtotime( $expiry_date ); } return date( 'Y-m-d', strtotime( $expiry_date ) ); } return ''; } /** * Helper method to get coupon post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_coupons( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_coupon', 'post_status' => 'publish', ); $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Bulk update or insert coupons * Accepts an array with coupons in the formats supported by * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['coupons'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 ); } $data = $data['coupons']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $coupons = array(); foreach ( $data as $_coupon ) { $coupon_id = 0; // Try to get the coupon ID if ( isset( $_coupon['id'] ) ) { $coupon_id = intval( $_coupon['id'] ); } if ( $coupon_id ) { // Coupon exists / edit coupon $edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) ); if ( is_wp_error( $edit ) ) { $coupons[] = array( 'id' => $coupon_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $coupons[] = $edit['coupon']; } } else { // Coupon don't exists / create coupon $new = $this->create_coupon( array( 'coupon' => $_coupon ) ); if ( is_wp_error( $new ) ) { $coupons[] = array( 'id' => $coupon_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $coupons[] = $new['coupon']; } } } return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v3/class-wc-api-taxes.php 0000644 00000044210 15132754524 0015046 0 ustar 00 <?php /** * WooCommerce API Taxes Class * * Handles requests to the /taxes endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.5.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Taxes extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/taxes'; /** * Register the routes for this class * * GET /taxes * GET /taxes/count * GET /taxes/<id> * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /taxes $routes[ $this->base ] = array( array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ), array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /taxes/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ), ); # GET/PUT/DELETE /taxes/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_tax' ), WC_API_Server::READABLE ), array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ), ); # GET/POST /taxes/classes $routes[ $this->base . '/classes' ] = array( array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ), array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /taxes/classes/count $routes[ $this->base . '/classes/count' ] = array( array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ), ); # GET /taxes/classes/<slug> $routes[ $this->base . '/classes/(?P<slug>\w[\w\s\-]*)' ] = array( array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ), ); # POST|PUT /taxes/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all taxes * * @since 2.5.0 * * @param string $fields * @param array $filter * @param string $class * @param int $page * * @return array */ public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) { if ( ! empty( $class ) ) { $filter['tax_rate_class'] = $class; } $filter['page'] = $page; $query = $this->query_tax_rates( $filter ); $taxes = array(); foreach ( $query['results'] as $tax ) { $taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) ); } // Set pagination headers $this->server->add_pagination_headers( $query['headers'] ); return array( 'taxes' => $taxes ); } /** * Get the tax for the given ID * * @since 2.5.0 * * @param int $id The tax ID * @param string $fields fields to include in response * * @return array|WP_Error */ public function get_tax( $id, $fields = null ) { global $wpdb; try { $id = absint( $id ); // Permissions check if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 ); } // Get tax rate details $tax = WC_Tax::_get_tax_rate( $id ); if ( is_wp_error( $tax ) || empty( $tax ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 ); } $tax_data = array( 'id' => (int) $tax['tax_rate_id'], 'country' => $tax['tax_rate_country'], 'state' => $tax['tax_rate_state'], 'postcode' => '', 'city' => '', 'rate' => $tax['tax_rate'], 'name' => $tax['tax_rate_name'], 'priority' => (int) $tax['tax_rate_priority'], 'compound' => (bool) $tax['tax_rate_compound'], 'shipping' => (bool) $tax['tax_rate_shipping'], 'order' => (int) $tax['tax_rate_order'], 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard', ); // Get locales from a tax rate $locales = $wpdb->get_results( $wpdb->prepare( " SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d ", $id ) ); if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { foreach ( $locales as $locale ) { $tax_data[ $locale->location_type ] = $locale->location_code; } } return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a tax * * @since 2.5.0 * * @param array $data * * @return array|WP_Error */ public function create_tax( $data ) { try { if ( ! isset( $data['tax'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this ); $tax_data = array( 'tax_rate_country' => '', 'tax_rate_state' => '', 'tax_rate' => '', 'tax_rate_name' => '', 'tax_rate_priority' => 1, 'tax_rate_compound' => 0, 'tax_rate_shipping' => 1, 'tax_rate_order' => 0, 'tax_rate_class' => '', ); foreach ( $tax_data as $key => $value ) { $new_key = str_replace( 'tax_rate_', '', $key ); $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; if ( isset( $data[ $new_key ] ) ) { if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { $tax_data[ $key ] = $data[ $new_key ] ? 1 : 0; } else { $tax_data[ $key ] = $data[ $new_key ]; } } } // Create tax rate $id = WC_Tax::_insert_tax_rate( $tax_data ); // Add locales if ( ! empty( $data['postcode'] ) ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); } if ( ! empty( $data['city'] ) ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); } do_action( 'woocommerce_api_create_tax', $id, $data ); $this->server->send_status( 201 ); return $this->get_tax( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a tax * * @since 2.5.0 * * @param int $id The tax ID * @param array $data * * @return array|WP_Error */ public function edit_tax( $id, $data ) { try { if ( ! isset( $data['tax'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 ); } $data = $data['tax']; // Get current tax rate data $tax = $this->get_tax( $id ); if ( is_wp_error( $tax ) ) { $error_data = $tax->get_error_data(); throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] ); } $current_data = $tax['tax']; $data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this ); $tax_data = array(); $default_fields = array( 'tax_rate_country', 'tax_rate_state', 'tax_rate', 'tax_rate_name', 'tax_rate_priority', 'tax_rate_compound', 'tax_rate_shipping', 'tax_rate_order', 'tax_rate_class', ); foreach ( $data as $key => $value ) { $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; // Check if the key is valid if ( ! in_array( $new_key, $default_fields ) ) { continue; } // Test new data against current data if ( $value === $current_data[ $key ] ) { continue; } // Fix compound and shipping values if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { $value = $value ? 1 : 0; } $tax_data[ $new_key ] = $value; } // Update tax rate WC_Tax::_update_tax_rate( $id, $tax_data ); // Update locales if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); } if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); } do_action( 'woocommerce_api_edit_tax_rate', $id, $data ); return $this->get_tax( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a tax * * @since 2.5.0 * * @param int $id The tax ID * * @return array|WP_Error */ public function delete_tax( $id ) { global $wpdb; try { // Check permissions if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 ); } $id = absint( $id ); WC_Tax::_delete_tax_rate( $id ); if ( 0 === $wpdb->rows_affected ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 ); } return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of taxes * * @since 2.5.0 * * @param string $class * @param array $filter * * @return array|WP_Error */ public function get_taxes_count( $class = null, $filter = array() ) { try { if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 ); } if ( ! empty( $class ) ) { $filter['tax_rate_class'] = $class; } $query = $this->query_tax_rates( $filter, true ); return array( 'count' => (int) $query['headers']->total ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Helper method to get tax rates objects * * @since 2.5.0 * * @param array $args * @param bool $count_only * * @return array */ protected function query_tax_rates( $args, $count_only = false ) { global $wpdb; $results = ''; // Set args $args = $this->merge_query_args( $args, array() ); $query = " SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates WHERE 1 = 1 "; // Filter by tax class if ( ! empty( $args['tax_rate_class'] ) ) { $tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : ''; $query .= " AND tax_rate_class = '$tax_rate_class'"; } // Order tax rates $order_by = ' ORDER BY tax_rate_order'; // Pagination $per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ); $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); if ( ! $count_only ) { $results = $wpdb->get_results( $query . $order_by . $pagination ); } $wpdb->get_results( $query ); $headers = new stdClass; $headers->page = $args['paged']; $headers->total = (int) $wpdb->num_rows; $headers->is_single = $per_page > $headers->total; $headers->total_pages = ceil( $headers->total / $per_page ); return array( 'results' => $results, 'headers' => $headers, ); } /** * Bulk update or insert taxes * Accepts an array with taxes in the formats supported by * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax() * * @since 2.5.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['taxes'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 ); } $data = $data['taxes']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $taxes = array(); foreach ( $data as $_tax ) { $tax_id = 0; // Try to get the tax rate ID if ( isset( $_tax['id'] ) ) { $tax_id = intval( $_tax['id'] ); } if ( $tax_id ) { // Tax rate exists / edit tax rate $edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) ); if ( is_wp_error( $edit ) ) { $taxes[] = array( 'id' => $tax_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $taxes[] = $edit['tax']; } } else { // Tax rate don't exists / create tax rate $new = $this->create_tax( array( 'tax' => $_tax ) ); if ( is_wp_error( $new ) ) { $taxes[] = array( 'id' => $tax_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $taxes[] = $new['tax']; } } } return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get all tax classes * * @since 2.5.0 * * @param string $fields * * @return array|WP_Error */ public function get_tax_classes( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 ); } $tax_classes = array(); // Add standard class $tax_classes[] = array( 'slug' => 'standard', 'name' => __( 'Standard rate', 'woocommerce' ), ); $classes = WC_Tax::get_tax_classes(); foreach ( $classes as $class ) { $tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array( 'slug' => sanitize_title( $class ), 'name' => $class, ), $class, $fields, $this ); } return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a tax class. * * @since 2.5.0 * * @param array $data * * @return array|WP_Error */ public function create_tax_class( $data ) { try { if ( ! isset( $data['tax_class'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 ); } // Check permissions if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 ); } $data = $data['tax_class']; if ( empty( $data['name'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); } $name = sanitize_text_field( $data['name'] ); $tax_class = WC_Tax::create_tax_class( $name ); if ( is_wp_error( $tax_class ) ) { return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 ); } do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data ); $this->server->send_status( 201 ); return array( 'tax_class' => $tax_class, ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a tax class * * @since 2.5.0 * * @param int $slug The tax class slug * * @return array|WP_Error */ public function delete_tax_class( $slug ) { global $wpdb; try { // Check permissions if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 ); } $slug = sanitize_title( $slug ); $tax_class = WC_Tax::get_tax_class_by( 'slug', $slug ); $deleted = WC_Tax::delete_tax_class_by( 'slug', $slug ); if ( is_wp_error( $deleted ) || ! $deleted ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); } return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of tax classes * * @since 2.5.0 * * @return array|WP_Error */ public function get_tax_classes_count() { try { if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 ); } $total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate return array( 'count' => $total ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/class-wc-rest-legacy-orders-controller.php 0000644 00000022351 15132754524 0020521 0 ustar 00 <?php /** * REST API Legacy Orders controller * * Handles requests to the /orders endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Legacy Orders controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Query args. * * @deprecated 3.0 * * @param array $args * @param WP_REST_Request $request * @return array */ public function query_args( $args, $request ) { global $wpdb; // Set post_status. if ( 'any' !== $request['status'] ) { $args['post_status'] = 'wc-' . $request['status']; } else { $args['post_status'] = 'any'; } if ( ! empty( $request['customer'] ) ) { if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); } $args['meta_query'][] = array( 'key' => '_customer_user', 'value' => $request['customer'], 'type' => 'NUMERIC', ); } // Search by product. if ( ! empty( $request['product'] ) ) { $order_ids = $wpdb->get_col( $wpdb->prepare( " SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) AND order_item_type = 'line_item' ", $request['product'] ) ); // Force WP_Query return empty if don't found any order. $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); $args['post__in'] = $order_ids; } // Search. if ( ! empty( $args['s'] ) ) { $order_ids = wc_order_search( $args['s'] ); if ( ! empty( $order_ids ) ) { unset( $args['s'] ); $args['post__in'] = array_merge( $order_ids, array( 0 ) ); } } return $args; } /** * Prepare a single order output for response. * * @deprecated 3.0 * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $data */ public function prepare_item_for_response( $post, $request ) { $this->request = $request; $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); $statuses = wc_get_order_statuses(); $order = wc_get_order( $post ); $data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() ); $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); // Format decimal values. foreach ( $format_decimal as $key ) { $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); } // Format date values. foreach ( $format_date as $key ) { $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false; } // Format the order status. $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; // Format line items. foreach ( $format_line_items as $key ) { $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); } // Refunds. $data['refunds'] = array(); foreach ( $order->get_refunds() as $refund ) { $data['refunds'][] = array( 'id' => $refund->get_id(), 'refund' => $refund->get_reason() ? $refund->get_reason() : '', 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $order, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Prepare a single order for create. * * @deprecated 3.0 * * @param WP_REST_Request $request Request object. * @return WP_Error|WC_Order $data Object. */ protected function prepare_item_for_database( $request ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $order = new WC_Order( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Handle all writable props foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'billing' : case 'shipping' : $this->update_address( $order, $value, $key ); break; case 'line_items' : case 'shipping_lines' : case 'fee_lines' : case 'coupon_lines' : if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( is_array( $item ) ) { if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { $order->remove_item( $item['id'] ); } else { $this->set_item( $order, $key, $item ); } } } } break; case 'meta_data' : if ( is_array( $value ) ) { foreach ( $value as $meta ) { $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; default : if ( is_callable( array( $order, "set_{$key}" ) ) ) { $order->{"set_{$key}"}( $value ); } break; } } } /** * Filter the data for the insert. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WC_Order $order The Order object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); } /** * Create base WC Order object. * * @deprecated 3.0.0 * * @param array $data * @return WC_Order */ protected function create_base_order( $data ) { return wc_create_order( $data ); } /** * Create order. * * @deprecated 3.0.0 * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function create_order( $request ) { try { // Make sure customer exists. if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } // Make sure customer is part of blog. if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); } $order = $this->prepare_item_for_database( $request ); $order->set_created_via( 'rest-api' ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->calculate_totals(); $order->save(); // Handle set paid. if ( true === $request['set_paid'] ) { $order->payment_complete( $request['transaction_id'] ); } return $order->get_id(); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update order. * * @deprecated 3.0.0 * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function update_order( $request ) { try { $order = $this->prepare_item_for_database( $request ); $order->save(); // Handle set paid. if ( $order->needs_payment() && true === $request['set_paid'] ) { $order->payment_complete( $request['transaction_id'] ); } // If items have changed, recalculate order totals. if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { $order->calculate_totals(); } return $order->get_id(); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/class-wc-rest-legacy-products-controller.php 0000644 00000055362 15132754524 0021076 0 ustar 00 <?php /** * REST API Legacy Products controller * * Handles requests to the /products endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Legacy Products controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v2'; /** * Query args. * * @deprecated 3.0.0 * * @param array $args Request args. * @param WP_REST_Request $request Request data. * @return array */ public function query_args( $args, $request ) { // Set post_status. $args['post_status'] = $request['status']; // Taxonomy query to filter products by type, category, // tag, shipping class, and attribute. $tax_query = array(); // Map between taxonomy name and arg's key. $taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', 'product_shipping_class' => 'shipping_class', ); // Set tax_query for each passed arg. foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], ); } } // Filter product type by slug. if ( ! empty( $request['type'] ) ) { $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], ); } // Filter by attribute and term. if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $tax_query[] = array( 'taxonomy' => $request['attribute'], 'field' => 'term_id', 'terms' => $request['attribute_term'], ); } } if ( ! empty( $tax_query ) ) { $args['tax_query'] = $tax_query; } // Filter featured. if ( is_bool( $request['featured'] ) ) { $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', ); } // Filter by sku. if ( ! empty( $request['sku'] ) ) { $skus = explode( ',', $request['sku'] ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $request['sku']; } $args['meta_query'] = $this->add_meta_query( $args, array( 'key' => '_sku', 'value' => $skus, 'compare' => 'IN', ) ); } // Filter by tax class. if ( ! empty( $request['tax_class'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, array( 'key' => '_tax_class', 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', ) ); } // Price filter. if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); } // Filter product in stock or out of stock. if ( is_bool( $request['in_stock'] ) ) { $args['meta_query'] = $this->add_meta_query( $args, array( 'key' => '_stock_status', 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', ) ); } // Filter by on sale products. if ( is_bool( $request['on_sale'] ) ) { $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; $args[ $on_sale_key ] += wc_get_product_ids_on_sale(); } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = array( 'product', 'product_variation' ); } else { $args['post_type'] = $this->post_type; } return $args; } /** * Prepare a single product output for response. * * @deprecated 3.0.0 * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_item_for_response( $post, $request ) { $product = wc_get_product( $post ); $data = $this->get_product_data( $product ); // Add variations to variable products. if ( $product->is_type( 'variable' ) && $product->has_child() ) { $data['variations'] = $product->get_children(); } // Add grouped products data. if ( $product->is_type( 'grouped' ) && $product->has_child() ) { $data['grouped_products'] = $product->get_children(); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $product, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Get product menu order. * * @deprecated 3.0.0 * @param WC_Product $product Product instance. * @return int */ protected function get_product_menu_order( $product ) { return $product->get_menu_order(); } /** * Save product meta. * * @deprecated 3.0.0 * @param WC_Product $product * @param WP_REST_Request $request * @return bool * @throws WC_REST_Exception */ protected function save_product_meta( $product, $request ) { $product = $this->set_product_meta( $product, $request ); $product->save(); return true; } /** * Set product meta. * * @deprecated 3.0.0 * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * @return WC_Product */ protected function set_product_meta( $product, $request ) { // Virtual. if ( isset( $request['virtual'] ) ) { $product->set_virtual( $request['virtual'] ); } // Tax status. if ( isset( $request['tax_status'] ) ) { $product->set_tax_status( $request['tax_status'] ); } // Tax Class. if ( isset( $request['tax_class'] ) ) { $product->set_tax_class( $request['tax_class'] ); } // Catalog Visibility. if ( isset( $request['catalog_visibility'] ) ) { $product->set_catalog_visibility( $request['catalog_visibility'] ); } // Purchase Note. if ( isset( $request['purchase_note'] ) ) { $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); } // Featured Product. if ( isset( $request['featured'] ) ) { $product->set_featured( $request['featured'] ); } // Shipping data. $product = $this->save_product_shipping_data( $product, $request ); // SKU. if ( isset( $request['sku'] ) ) { $product->set_sku( wc_clean( $request['sku'] ) ); } // Attributes. if ( isset( $request['attributes'] ) ) { $attributes = array(); foreach ( $request['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = wc_clean( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( $attribute_id ) { if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names. $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Custom attribute - Add attribute to array and set the values. if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; } else { $values = explode( WC_DELIMITER, $attribute['options'] ); } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $request['regular_price'] ) ) { $product->set_regular_price( $request['regular_price'] ); } // Sale Price. if ( isset( $request['sale_price'] ) ) { $product->set_sale_price( $request['sale_price'] ); } if ( isset( $request['date_on_sale_from'] ) ) { $product->set_date_on_sale_from( $request['date_on_sale_from'] ); } if ( isset( $request['date_on_sale_to'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to'] ); } } // Product parent ID for groups. if ( isset( $request['parent_id'] ) ) { $product->set_parent_id( $request['parent_id'] ); } // Sold individually. if ( isset( $request['sold_individually'] ) ) { $product->set_sold_individually( $request['sold_individually'] ); } // Stock status. if ( isset( $request['in_stock'] ) ) { $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; } else { $stock_status = $product->get_stock_status(); } // Stock data. if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $product->set_manage_stock( $request['manage_stock'] ); } // Backorders. if ( isset( $request['backorders'] ) ) { $product->set_backorders( $request['backorders'] ); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( $product->get_manage_stock() ) { // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity. if ( isset( $request['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells. if ( isset( $request['upsell_ids'] ) ) { $upsells = array(); $ids = $request['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } } $product->set_upsell_ids( $upsells ); } // Cross sells. if ( isset( $request['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $request['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } } $product->set_cross_sell_ids( $crosssells ); } // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); } // Downloadable. if ( isset( $request['downloadable'] ) ) { $product->set_downloadable( $request['downloadable'] ); } // Downloadable options. if ( $product->get_downloadable() ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { $product->set_download_limit( $request['download_limit'] ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { $product->set_download_expiry( $request['download_expiry'] ); } } // Product url and button text for external products. if ( $product->is_type( 'external' ) ) { if ( isset( $request['external_url'] ) ) { $product->set_product_url( $request['external_url'] ); } if ( isset( $request['button_text'] ) ) { $product->set_button_text( $request['button_text'] ); } } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $request ); } return $product; } /** * Save variations. * * @deprecated 3.0.0 * * @throws WC_REST_Exception REST API exceptions. * @param WC_Product $product Product instance. * @param WP_REST_Request $request Request data. * @return bool */ protected function save_variations_data( $product, $request ) { foreach ( $request['variations'] as $menu_order => $data ) { $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); // Create initial name and status. if ( ! $variation->get_slug() ) { /* translators: 1: variation id 2: product name */ $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); } // Parent ID. $variation->set_parent_id( $product->get_id() ); // Menu order. $variation->set_menu_order( $menu_order ); // Status. if ( isset( $data['visible'] ) ) { $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); } // SKU. if ( isset( $data['sku'] ) ) { $variation->set_sku( wc_clean( $data['sku'] ) ); } // Thumbnail. if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { $image = $data['image']; $image = current( $image ); if ( is_array( $image ) ) { $image['position'] = 0; } $variation = $this->set_product_images( $variation, array( $image ) ); } // Virtual variation. if ( isset( $data['virtual'] ) ) { $variation->set_virtual( $data['virtual'] ); } // Downloadable variation. if ( isset( $data['downloadable'] ) ) { $variation->set_downloadable( $data['downloadable'] ); } // Downloads. if ( $variation->get_downloadable() ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { $variation->set_download_limit( $data['download_limit'] ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { $variation->set_download_expiry( $data['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $data ); // Stock handling. if ( isset( $data['manage_stock'] ) ) { $variation->set_manage_stock( $data['manage_stock'] ); } if ( isset( $data['in_stock'] ) ) { $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); } if ( isset( $data['backorders'] ) ) { $variation->set_backorders( $data['backorders'] ); } if ( $variation->get_manage_stock() ) { if ( isset( $data['stock_quantity'] ) ) { $variation->set_stock_quantity( $data['stock_quantity'] ); } elseif ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); $variation->set_stock_quantity( $stock_quantity ); } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); } // Regular Price. if ( isset( $data['regular_price'] ) ) { $variation->set_regular_price( $data['regular_price'] ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $variation->set_sale_price( $data['sale_price'] ); } if ( isset( $data['date_on_sale_from'] ) ) { $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); } if ( isset( $data['date_on_sale_to'] ) ) { $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); } // Tax class. if ( isset( $data['tax_class'] ) ) { $variation->set_tax_class( $data['tax_class'] ); } // Description. if ( isset( $data['description'] ) ) { $variation->set_description( wp_kses_post( $data['description'] ) ); } // Update taxonomies. if ( isset( $data['attributes'] ) ) { $attributes = array(); $parent_attributes = $product->get_attributes(); foreach ( $data['attributes'] as $attribute ) { $attribute_id = 0; $attribute_name = ''; // Check ID for global attributes or name for product attributes. if ( ! empty( $attribute['id'] ) ) { $attribute_id = absint( $attribute['id'] ); $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } elseif ( ! empty( $attribute['name'] ) ) { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! $attribute_id && ! $attribute_name ) { continue; } if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { continue; } $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $attribute_value = $term->slug; } else { $attribute_value = sanitize_title( $attribute_value ); } } $attributes[ $attribute_key ] = $attribute_value; } $variation->set_attributes( $attributes ); } $variation->save(); do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); } return true; } /** * Add post meta fields. * * @deprecated 3.0.0 * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request data. * @return bool|WP_Error */ protected function add_post_meta_fields( $post, $request ) { return $this->update_post_meta_fields( $post, $request ); } /** * Update post meta fields. * * @param WP_Post $post Post data. * @param WP_REST_Request $request Request data. * @return bool|WP_Error */ protected function update_post_meta_fields( $post, $request ) { $product = wc_get_product( $post ); // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $product = $this->set_product_images( $product, $request['images'] ); } // Save product meta fields. $product = $this->set_product_meta( $product, $request ); // Save the product data. $product->save(); // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { $this->save_variations_data( $product, $request ); } } // Clear caches here so in sync with any new variations/children. wc_delete_product_transients( $product->get_id() ); wp_cache_delete( 'product-' . $product->get_id(), 'products' ); return true; } /** * Delete post. * * @deprecated 3.0.0 * * @param int|WP_Post $id Post ID or WP_Post instance. */ protected function delete_post( $id ) { if ( ! empty( $id->ID ) ) { $id = $id->ID; } elseif ( ! is_numeric( $id ) || 0 >= $id ) { return; } // Delete product attachments. $attachments = get_posts( array( 'post_parent' => $id, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product. $product = wc_get_product( $id ); $product->delete( true ); } /** * Get post types. * * @deprecated 3.0.0 * * @return array */ protected function get_post_types() { return array( 'product', 'product_variation' ); } /** * Save product images. * * @deprecated 3.0.0 * * @param int $product_id * @param array $images * @throws WC_REST_Exception */ protected function save_product_images( $product_id, $images ) { $product = wc_get_product( $product_id ); return set_product_images( $product, $images ); } } includes/legacy/api/v2/class-wc-api-json-handler.php 0000644 00000003656 15132754524 0016316 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_JSON_Handler implements WC_API_Handler { /** * Get the content type for the response * * @since 2.1 * @return string */ public function get_content_type() { return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) ); } /** * Parse the raw request body entity * * @since 2.1 * @param string $body the raw request body * @return array|mixed */ public function parse_body( $body ) { return json_decode( $body, true ); } /** * Generate a JSON response given an array of data * * @since 2.1 * @param array $data the response data * @return string */ public function generate_response( $data ) { if ( isset( $_GET['_jsonp'] ) ) { if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); } $jsonp_callback = $_GET['_jsonp']; if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); } WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks. // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; } return wp_json_encode( $data ); } } includes/legacy/api/v2/class-wc-api-server.php 0000644 00000050735 15132754524 0015240 0 ustar 00 <?php /** * WooCommerce API * * Handles REST API requests * * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API) * Many thanks to Ryan McCue and any other contributors! * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } require_once ABSPATH . 'wp-admin/includes/admin.php'; class WC_API_Server { const METHOD_GET = 1; const METHOD_POST = 2; const METHOD_PUT = 4; const METHOD_PATCH = 8; const METHOD_DELETE = 16; const READABLE = 1; // GET const CREATABLE = 2; // POST const EDITABLE = 14; // POST | PUT | PATCH const DELETABLE = 16; // DELETE const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE /** * Does the endpoint accept a raw request body? */ const ACCEPT_RAW_DATA = 64; /** Does the endpoint accept a request body? (either JSON or XML) */ const ACCEPT_DATA = 128; /** * Should we hide this endpoint from the index? */ const HIDDEN_ENDPOINT = 256; /** * Map of HTTP verbs to constants * @var array */ public static $method_map = array( 'HEAD' => self::METHOD_GET, 'GET' => self::METHOD_GET, 'POST' => self::METHOD_POST, 'PUT' => self::METHOD_PUT, 'PATCH' => self::METHOD_PATCH, 'DELETE' => self::METHOD_DELETE, ); /** * Requested path (relative to the API root, wp-json.php) * * @var string */ public $path = ''; /** * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) * * @var string */ public $method = 'HEAD'; /** * Request parameters * * This acts as an abstraction of the superglobals * (GET => $_GET, POST => $_POST) * * @var array */ public $params = array( 'GET' => array(), 'POST' => array() ); /** * Request headers * * @var array */ public $headers = array(); /** * Request files (matches $_FILES) * * @var array */ public $files = array(); /** * Request/Response handler, either JSON by default * or XML if requested by client * * @var WC_API_Handler */ public $handler; /** * Setup class and set request/response handler * * @since 2.1 * @param $path */ public function __construct( $path ) { if ( empty( $path ) ) { if ( isset( $_SERVER['PATH_INFO'] ) ) { $path = $_SERVER['PATH_INFO']; } else { $path = '/'; } } $this->path = $path; $this->method = $_SERVER['REQUEST_METHOD']; $this->params['GET'] = $_GET; $this->params['POST'] = $_POST; $this->headers = $this->get_headers( $_SERVER ); $this->files = $_FILES; // Compatibility for clients that can't use PUT/PATCH/DELETE if ( isset( $_GET['_method'] ) ) { $this->method = strtoupper( $_GET['_method'] ); } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } // load response handler $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); $this->handler = new $handler_class(); } /** * Check authentication for the request * * @since 2.1 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login */ public function check_authentication() { // allow plugins to remove default authentication or add their own authentication $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); if ( is_a( $user, 'WP_User' ) ) { // API requests run under the context of the authenticated user wp_set_current_user( $user->ID ); } elseif ( ! is_wp_error( $user ) ) { // WP_Errors are handled in serve_request() $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); } return $user; } /** * Convert an error to an array * * This iterates over all error codes and messages to change it into a flat * array. This enables simpler client behaviour, as it is represented as a * list in JSON rather than an object/map * * @since 2.1 * @param WP_Error $error * @return array List of associative arrays with code and message keys */ protected function error_to_array( $error ) { $errors = array(); foreach ( (array) $error->errors as $code => $messages ) { foreach ( (array) $messages as $message ) { $errors[] = array( 'code' => $code, 'message' => $message ); } } return array( 'errors' => $errors ); } /** * Handle serving an API request * * Matches the current server URI to a route and runs the first matching * callback then outputs a JSON representation of the returned value. * * @since 2.1 * @uses WC_API_Server::dispatch() */ public function serve_request() { do_action( 'woocommerce_api_server_before_serve', $this ); $this->header( 'Content-Type', $this->handler->get_content_type(), true ); // the API is enabled by default if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { $this->send_status( 404 ); echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); return; } $result = $this->check_authentication(); // if authorization check was successful, dispatch the request if ( ! is_wp_error( $result ) ) { $result = $this->dispatch(); } // handle any dispatch errors if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); if ( is_array( $data ) && isset( $data['status'] ) ) { $this->send_status( $data['status'] ); } $result = $this->error_to_array( $result ); } // This is a filter rather than an action, since this is designed to be // re-entrant if needed $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); if ( ! $served ) { if ( 'HEAD' === $this->method ) { return; } echo $this->handler->generate_response( $result ); } } /** * Retrieve the route map * * The route map is an associative array with path regexes as the keys. The * value is an indexed array with the callback function/method as the first * item, and a bitmask of HTTP methods as the second item (see the class * constants). * * Each route can be mapped to more than one callback by using an array of * the indexed arrays. This allows mapping e.g. GET requests to one callback * and POST requests to another. * * Note that the path regexes (array keys) must have @ escaped, as this is * used as the delimiter with preg_match() * * @since 2.1 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` */ public function get_routes() { // index added by default $endpoints = array( '/' => array( array( $this, 'get_index' ), self::READABLE ), ); $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); // Normalise the endpoints foreach ( $endpoints as $route => &$handlers ) { if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { $handlers = array( $handlers ); } } return $endpoints; } /** * Match the request to a callback and call it * * @since 2.1 * @return mixed The value returned by the callback, or a WP_Error instance */ public function dispatch() { switch ( $this->method ) { case 'HEAD' : case 'GET' : $method = self::METHOD_GET; break; case 'POST' : $method = self::METHOD_POST; break; case 'PUT' : $method = self::METHOD_PUT; break; case 'PATCH' : $method = self::METHOD_PATCH; break; case 'DELETE' : $method = self::METHOD_DELETE; break; default : return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); } foreach ( $this->get_routes() as $route => $handlers ) { foreach ( $handlers as $handler ) { $callback = $handler[0]; $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; if ( ! ( $supported & $method ) ) { continue; } $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); if ( ! $match ) { continue; } if ( ! is_callable( $callback ) ) { return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); } $args = array_merge( $args, $this->params['GET'] ); if ( $method & self::METHOD_POST ) { $args = array_merge( $args, $this->params['POST'] ); } if ( $supported & self::ACCEPT_DATA ) { $data = $this->handler->parse_body( $this->get_raw_data() ); $args = array_merge( $args, array( 'data' => $data ) ); } elseif ( $supported & self::ACCEPT_RAW_DATA ) { $data = $this->get_raw_data(); $args = array_merge( $args, array( 'data' => $data ) ); } $args['_method'] = $method; $args['_route'] = $route; $args['_path'] = $this->path; $args['_headers'] = $this->headers; $args['_files'] = $this->files; $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); // Allow plugins to halt the request via this filter if ( is_wp_error( $args ) ) { return $args; } $params = $this->sort_callback_params( $callback, $args ); if ( is_wp_error( $params ) ) { return $params; } return call_user_func_array( $callback, $params ); } } return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); } /** * urldecode deep. * * @since 2.2 * @param string|array $value Data to decode with urldecode. * @return string|array Decoded data. */ protected function urldecode_deep( $value ) { if ( is_array( $value ) ) { return array_map( array( $this, 'urldecode_deep' ), $value ); } else { return urldecode( $value ); } } /** * Sort parameters by order specified in method declaration * * Takes a callback and a list of available params, then filters and sorts * by the parameters the method actually needs, using the Reflection API * * @since 2.2 * * @param callable|array $callback the endpoint callback * @param array $provided the provided request parameters * * @return array|WP_Error */ protected function sort_callback_params( $callback, $provided ) { if ( is_array( $callback ) ) { $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); } else { $ref_func = new ReflectionFunction( $callback ); } $wanted = $ref_func->getParameters(); $ordered_parameters = array(); foreach ( $wanted as $param ) { if ( isset( $provided[ $param->getName() ] ) ) { // We have this parameters in the list to choose from if ( 'data' == $param->getName() ) { $ordered_parameters[] = $provided[ $param->getName() ]; continue; } $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] ); } elseif ( $param->isDefaultValueAvailable() ) { // We don't have this parameter, but it's optional $ordered_parameters[] = $param->getDefaultValue(); } else { // We don't have this parameter and it wasn't optional, abort! return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); } } return $ordered_parameters; } /** * Get the site index. * * This endpoint describes the capabilities of the site. * * @since 2.3 * @return array Index entity */ public function get_index() { // General site data $available = array( 'store' => array( 'name' => get_option( 'blogname' ), 'description' => get_option( 'blogdescription' ), 'URL' => get_option( 'siteurl' ), 'wc_version' => WC()->version, 'routes' => array(), 'meta' => array( 'timezone' => wc_timezone_string(), 'currency' => get_woocommerce_currency(), 'currency_format' => get_woocommerce_currency_symbol(), 'currency_position' => get_option( 'woocommerce_currency_pos' ), 'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ), 'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ), 'price_num_decimals' => wc_get_price_decimals(), 'tax_included' => wc_prices_include_tax(), 'weight_unit' => get_option( 'woocommerce_weight_unit' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), 'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ), 'links' => array( 'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/', ), ), ), ); // Find the available routes foreach ( $this->get_routes() as $route => $callbacks ) { $data = array(); $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); foreach ( self::$method_map as $name => $bitmask ) { foreach ( $callbacks as $callback ) { // Skip to the next route if any callback is hidden if ( $callback[1] & self::HIDDEN_ENDPOINT ) { continue 3; } if ( $callback[1] & $bitmask ) { $data['supports'][] = $name; } if ( $callback[1] & self::ACCEPT_DATA ) { $data['accepts_data'] = true; } // For non-variable routes, generate links if ( strpos( $route, '<' ) === false ) { $data['meta'] = array( 'self' => get_woocommerce_api_url( $route ), ); } } } $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); } return apply_filters( 'woocommerce_api_index', $available ); } /** * Send a HTTP status code * * @since 2.1 * @param int $code HTTP status */ public function send_status( $code ) { status_header( $code ); } /** * Send a HTTP header * * @since 2.1 * @param string $key Header key * @param string $value Header value * @param boolean $replace Should we replace the existing header? */ public function header( $key, $value, $replace = true ) { header( sprintf( '%s: %s', $key, $value ), $replace ); } /** * Send a Link header * * @internal The $rel parameter is first, as this looks nicer when sending multiple * * @link http://tools.ietf.org/html/rfc5988 * @link http://www.iana.org/assignments/link-relations/link-relations.xml * * @since 2.1 * @param string $rel Link relation. Either a registered type, or an absolute URL * @param string $link Target IRI for the link * @param array $other Other parameters to send, as an associative array */ public function link_header( $rel, $link, $other = array() ) { $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); foreach ( $other as $key => $value ) { if ( 'title' == $key ) { $value = '"' . $value . '"'; } $header .= '; ' . $key . '=' . $value; } $this->header( 'Link', $header, false ); } /** * Send pagination headers for resources * * @since 2.1 * @param WP_Query|WP_User_Query|stdClass $query */ public function add_pagination_headers( $query ) { // WP_User_Query if ( is_a( $query, 'WP_User_Query' ) ) { $single = count( $query->get_results() ) == 1; $total = $query->get_total(); if ( $query->get( 'number' ) > 0 ) { $page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1; $total_pages = ceil( $total / $query->get( 'number' ) ); } else { $page = 1; $total_pages = 1; } } elseif ( is_a( $query, 'stdClass' ) ) { $page = $query->page; $single = $query->is_single; $total = $query->total; $total_pages = $query->total_pages; // WP_Query } else { $page = $query->get( 'paged' ); $single = $query->is_single(); $total = $query->found_posts; $total_pages = $query->max_num_pages; } if ( ! $page ) { $page = 1; } $next_page = absint( $page ) + 1; if ( ! $single ) { // first/prev if ( $page > 1 ) { $this->link_header( 'first', $this->get_paginated_url( 1 ) ); $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); } // next if ( $next_page <= $total_pages ) { $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); } // last if ( $page != $total_pages ) { $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); } } $this->header( 'X-WC-Total', $total ); $this->header( 'X-WC-TotalPages', $total_pages ); do_action( 'woocommerce_api_pagination_headers', $this, $query ); } /** * Returns the request URL with the page query parameter set to the specified page * * @since 2.1 * @param int $page * @return string */ private function get_paginated_url( $page ) { // remove existing page query param $request = remove_query_arg( 'page' ); // add provided page query param $request = urldecode( add_query_arg( 'page', $page, $request ) ); // get the home host $host = parse_url( get_home_url(), PHP_URL_HOST ); return set_url_scheme( "http://{$host}{$request}" ); } /** * Retrieve the raw request entity (body) * * @since 2.1 * @return string */ public function get_raw_data() { // @codingStandardsIgnoreStart // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6. if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; // @codingStandardsIgnoreEnd } /** * Parse an RFC3339 datetime into a MySQl datetime * * Invalid dates default to unix epoch * * @since 2.1 * @param string $datetime RFC3339 datetime * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) */ public function parse_datetime( $datetime ) { // Strip millisecond precision (a full stop followed by one or more digits) if ( strpos( $datetime, '.' ) !== false ) { $datetime = preg_replace( '/\.\d+/', '', $datetime ); } // default timezone to UTC $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); try { $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { $datetime = new DateTime( '@0' ); } return $datetime->format( 'Y-m-d H:i:s' ); } /** * Format a unix timestamp or MySQL datetime into an RFC3339 datetime * * @since 2.1 * @param int|string $timestamp unix timestamp or MySQL datetime * @param bool $convert_to_utc * @param bool $convert_to_gmt Use GMT timezone. * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { if ( $convert_to_gmt ) { if ( is_numeric( $timestamp ) ) { $timestamp = date( 'Y-m-d H:i:s', $timestamp ); } $timestamp = get_gmt_from_date( $timestamp ); } if ( $convert_to_utc ) { $timezone = new DateTimeZone( wc_timezone_string() ); } else { $timezone = new DateTimeZone( 'UTC' ); } try { if ( is_numeric( $timestamp ) ) { $date = new DateTime( "@{$timestamp}" ); } else { $date = new DateTime( $timestamp, $timezone ); } // convert to UTC by adjusting the time based on the offset of the site's timezone if ( $convert_to_utc ) { $date->modify( -1 * $date->getOffset() . ' seconds' ); } } catch ( Exception $e ) { $date = new DateTime( '@0' ); } return $date->format( 'Y-m-d\TH:i:s\Z' ); } /** * Extract headers from a PHP-style $_SERVER array * * @since 2.1 * @param array $server Associative array similar to $_SERVER * @return array Headers extracted from the input */ public function get_headers( $server ) { $headers = array(); // CONTENT_* headers are not prefixed with HTTP_ $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); foreach ( $server as $key => $value ) { if ( strpos( $key, 'HTTP_' ) === 0 ) { $headers[ substr( $key, 5 ) ] = $value; } elseif ( isset( $additional[ $key ] ) ) { $headers[ $key ] = $value; } } return $headers; } } includes/legacy/api/v2/class-wc-api-customers.php 0000644 00000061142 15132754524 0015750 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Customers extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/customers'; /** @var string $created_at_min for date filtering */ private $created_at_min = null; /** @var string $created_at_max for date filtering */ private $created_at_max = null; /** * Setup class, overridden to provide customer data to order response * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { parent::__construct( $server ); // add customer data to order responses add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 ); // modify WP_User_Query to support created_at date filtering add_action( 'pre_user_query', array( $this, 'modify_user_query' ) ); } /** * Register the routes for this class * * GET /customers * GET /customers/count * GET /customers/<id> * GET /customers/<id>/orders * * @since 2.2 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /customers $routes[ $this->base ] = array( array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /customers/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), ); # GET/PUT/DELETE /customers/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ), ); # GET /customers/email/<email> $routes[ $this->base . '/email/(?P<email>.+)' ] = array( array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id>/orders $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array( array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id>/downloads $routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array( array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ), ); # POST|PUT /customers/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all customers * * @since 2.1 * @param array $fields * @param array $filter * @param int $page * @return array */ public function get_customers( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_customers( $filter ); $customers = array(); foreach ( $query->get_results() as $user_id ) { if ( ! $this->is_readable( $user_id ) ) { continue; } $customers[] = current( $this->get_customer( $user_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'customers' => $customers ); } /** * Get the customer for the given ID * * @since 2.1 * @param int $id the customer ID * @param array $fields * @return array|WP_Error */ public function get_customer( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $customer = new WC_Customer( $id ); $last_order = $customer->get_last_order(); $customer_data = array( 'id' => $customer->get_id(), 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'email' => $customer->get_email(), 'first_name' => $customer->get_first_name(), 'last_name' => $customer->get_last_name(), 'username' => $customer->get_username(), 'role' => $customer->get_role(), 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. 'orders_count' => $customer->get_order_count(), 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), 'avatar_url' => $customer->get_avatar_url(), 'billing_address' => array( 'first_name' => $customer->get_billing_first_name(), 'last_name' => $customer->get_billing_last_name(), 'company' => $customer->get_billing_company(), 'address_1' => $customer->get_billing_address_1(), 'address_2' => $customer->get_billing_address_2(), 'city' => $customer->get_billing_city(), 'state' => $customer->get_billing_state(), 'postcode' => $customer->get_billing_postcode(), 'country' => $customer->get_billing_country(), 'email' => $customer->get_billing_email(), 'phone' => $customer->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $customer->get_shipping_first_name(), 'last_name' => $customer->get_shipping_last_name(), 'company' => $customer->get_shipping_company(), 'address_1' => $customer->get_shipping_address_1(), 'address_2' => $customer->get_shipping_address_2(), 'city' => $customer->get_shipping_city(), 'state' => $customer->get_shipping_state(), 'postcode' => $customer->get_shipping_postcode(), 'country' => $customer->get_shipping_country(), ), ); return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); } /** * Get the customer for the given email * * @since 2.1 * * @param string $email the customer email * @param array $fields * * @return array|WP_Error */ public function get_customer_by_email( $email, $fields = null ) { try { if ( is_email( $email ) ) { $customer = get_user_by( 'email', $email ); if ( ! is_object( $customer ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); } } else { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); } return $this->get_customer( $customer->ID, $fields ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of customers * * @since 2.1 * * @param array $filter * * @return array|WP_Error */ public function get_customers_count( $filter = array() ) { try { if ( ! current_user_can( 'list_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 ); } $query = $this->query_customers( $filter ); return array( 'count' => $query->get_total() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get customer billing address fields. * * @since 2.2 * @return array */ protected function get_customer_billing_address() { $billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone', ) ); return $billing_address; } /** * Get customer shipping address fields. * * @since 2.2 * @return array */ protected function get_customer_shipping_address() { $shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ) ); return $shipping_address; } /** * Add/Update customer data. * * @since 2.2 * @param int $id the customer ID * @param array $data * @param WC_Customer $customer */ protected function update_customer_data( $id, $data, $customer ) { // Customer first name. if ( isset( $data['first_name'] ) ) { $customer->set_first_name( wc_clean( $data['first_name'] ) ); } // Customer last name. if ( isset( $data['last_name'] ) ) { $customer->set_last_name( wc_clean( $data['last_name'] ) ); } // Customer billing address. if ( isset( $data['billing_address'] ) ) { foreach ( $this->get_customer_billing_address() as $field ) { if ( isset( $data['billing_address'][ $field ] ) ) { if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) { $customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] ); } else { $customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) ); } } } } // Customer shipping address. if ( isset( $data['shipping_address'] ) ) { foreach ( $this->get_customer_shipping_address() as $field ) { if ( isset( $data['shipping_address'][ $field ] ) ) { if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { $customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] ); } else { $customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) ); } } } } do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer ); } /** * Create a customer * * @since 2.2 * * @param array $data * * @return array|WP_Error */ public function create_customer( $data ) { try { if ( ! isset( $data['customer'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 ); } $data = $data['customer']; // Checks with can create new users. if ( ! current_user_can( 'create_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this ); // Checks with the email is missing. if ( ! isset( $data['email'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 ); } // Create customer. $customer = new WC_Customer; $customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' ); $customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' ); $customer->set_email( $data['email'] ); $customer->save(); if ( ! $customer->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); } // Added customer data. $this->update_customer_data( $customer->get_id(), $data, $customer ); $customer->save(); do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data ); $this->server->send_status( 201 ); return $this->get_customer( $customer->get_id() ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a customer * * @since 2.2 * * @param int $id the customer ID * @param array $data * * @return array|WP_Error */ public function edit_customer( $id, $data ) { try { if ( ! isset( $data['customer'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 ); } $data = $data['customer']; // Validate the customer ID. $id = $this->validate_request( $id, 'customer', 'edit' ); // Return the validate error. if ( is_wp_error( $id ) ) { throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); } $data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this ); $customer = new WC_Customer( $id ); // Customer email. if ( isset( $data['email'] ) ) { $customer->set_email( $data['email'] ); } // Customer password. if ( isset( $data['password'] ) ) { $customer->set_password( $data['password'] ); } // Update customer data. $this->update_customer_data( $customer->get_id(), $data, $customer ); $customer->save(); do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data ); return $this->get_customer( $customer->get_id() ); } catch ( Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a customer * * @since 2.2 * @param int $id the customer ID * @return array|WP_Error */ public function delete_customer( $id ) { // Validate the customer ID. $id = $this->validate_request( $id, 'customer', 'delete' ); // Return the validate error. if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_customer', $id, $this ); return $this->delete( $id, 'customer' ); } /** * Get the orders for a customer * * @since 2.1 * @param int $id the customer ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_customer_orders( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $order_ids = wc_get_orders( array( 'customer' => $id, 'limit' => -1, 'orderby' => 'date', 'order' => 'ASC', 'return' => 'ids', ) ); if ( empty( $order_ids ) ) { return array( 'orders' => array() ); } $orders = array(); foreach ( $order_ids as $order_id ) { $orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) ); } return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) ); } /** * Get the available downloads for a customer * * @since 2.2 * @param int $id the customer ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_customer_downloads( $id, $fields = null ) { $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $downloads = array(); $_downloads = wc_get_customer_available_downloads( $id ); foreach ( $_downloads as $key => $download ) { $downloads[] = array( 'download_url' => $download['download_url'], 'download_id' => $download['download_id'], 'product_id' => $download['product_id'], 'download_name' => $download['download_name'], 'order_id' => $download['order_id'], 'order_key' => $download['order_key'], 'downloads_remaining' => $download['downloads_remaining'], 'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null, 'file' => $download['file'], ); } return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) ); } /** * Helper method to get customer user objects * * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited * pagination support * * The filter for role can only be a single role in a string. * * @since 2.3 * @param array $args request arguments for filtering query * @return WP_User_Query */ private function query_customers( $args = array() ) { // default users per page $users_per_page = get_option( 'posts_per_page' ); // Set base query arguments $query_args = array( 'fields' => 'ID', 'role' => 'customer', 'orderby' => 'registered', 'number' => $users_per_page, ); // Custom Role if ( ! empty( $args['role'] ) ) { $query_args['role'] = $args['role']; } // Search if ( ! empty( $args['q'] ) ) { $query_args['search'] = $args['q']; } // Limit number of users returned if ( ! empty( $args['limit'] ) ) { if ( -1 == $args['limit'] ) { unset( $query_args['number'] ); } else { $query_args['number'] = absint( $args['limit'] ); $users_per_page = absint( $args['limit'] ); } } else { $args['limit'] = $query_args['number']; } // Page $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; // Offset if ( ! empty( $args['offset'] ) ) { $query_args['offset'] = absint( $args['offset'] ); } else { $query_args['offset'] = $users_per_page * ( $page - 1 ); } // Created date if ( ! empty( $args['created_at_min'] ) ) { $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); } if ( ! empty( $args['created_at_max'] ) ) { $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); } // Order (ASC or DESC, ASC by default) if ( ! empty( $args['order'] ) ) { $query_args['order'] = $args['order']; } // Order by if ( ! empty( $args['orderby'] ) ) { $query_args['orderby'] = $args['orderby']; // Allow sorting by meta value if ( ! empty( $args['orderby_meta_key'] ) ) { $query_args['meta_key'] = $args['orderby_meta_key']; } } $query = new WP_User_Query( $query_args ); // Helper members for pagination headers $query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page ); $query->page = $page; return $query; } /** * Add customer data to orders * * @since 2.1 * @param $order_data * @param $order * @return array */ public function add_customer_data( $order_data, $order ) { if ( 0 == $order->get_user_id() ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->get_billing_email(), 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); } return $order_data; } /** * Modify the WP_User_Query to support filtering on the date the customer was created * * @since 2.1 * @param WP_User_Query $query */ public function modify_user_query( $query ) { if ( $this->created_at_min ) { $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) ); } if ( $this->created_at_max ) { $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid WP_User * 3) the current user has the proper permissions * * @since 2.1 * @see WC_API_Resource::validate_request() * @param integer $id the customer ID * @param string $type the request type, unused because this method overrides the parent class * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid user ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { try { $id = absint( $id ); // validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 ); } // non-existent IDs return a valid WP_User object with the user ID = 0 $customer = new WP_User( $id ); if ( 0 === $customer->ID ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 ); } // validate permissions switch ( $context ) { case 'read': if ( ! current_user_can( 'list_users' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 ); } break; case 'edit': if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 ); } break; case 'delete': if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 ); } break; } return $id; } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Check if the current user can read users * * @since 2.1 * @see WC_API_Resource::is_readable() * @param int|WP_Post $post unused * @return bool true if the current user can read users, false otherwise */ protected function is_readable( $post ) { return current_user_can( 'list_users' ); } /** * Bulk update or insert customers * Accepts an array with customers in the formats supported by * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['customers'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 ); } $data = $data['customers']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $customers = array(); foreach ( $data as $_customer ) { $customer_id = 0; // Try to get the customer ID if ( isset( $_customer['id'] ) ) { $customer_id = intval( $_customer['id'] ); } // Customer exists / edit customer if ( $customer_id ) { $edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) ); if ( is_wp_error( $edit ) ) { $customers[] = array( 'id' => $customer_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $customers[] = $edit['customer']; } } else { // Customer don't exists / create customer $new = $this->create_customer( array( 'customer' => $_customer ) ); if ( is_wp_error( $new ) ) { $customers[] = array( 'id' => $customer_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $customers[] = $new['customer']; } } } return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v2/class-wc-api-exception.php 0000644 00000002213 15132754524 0015714 0 ustar 00 <?php /** * WooCommerce API Exception Class * * Extends Exception to provide additional data * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Exception extends Exception { /** @var string sanitized error code */ protected $error_code; /** * Setup exception, requires 3 params: * * error code - machine-readable, e.g. `woocommerce_invalid_product_id` * error message - friendly message, e.g. 'Product ID is invalid' * http status code - proper HTTP status code to respond with, e.g. 400 * * @since 2.2 * @param string $error_code * @param string $error_message user-friendly translated error message * @param int $http_status_code HTTP status code to respond with */ public function __construct( $error_code, $error_message, $http_status_code ) { $this->error_code = $error_code; parent::__construct( $error_message, $http_status_code ); } /** * Returns the error code * * @since 2.2 * @return string */ public function getErrorCode() { return $this->error_code; } } includes/legacy/api/v2/class-wc-api-webhooks.php 0000644 00000036334 15132754524 0015552 0 ustar 00 <?php /** * WooCommerce API Webhooks class * * Handles requests to the /webhooks endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.2 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Webhooks extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/webhooks'; /** * Register the routes for this class * * @since 2.2 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET|POST /webhooks $routes[ $this->base ] = array( array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ), array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /webhooks/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ), ); # GET|PUT|DELETE /webhooks/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ), array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ), ); # GET /webhooks/<id>/deliveries $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array( array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), ); # GET /webhooks/<webhook_id>/deliveries/<id> $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array( array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get all webhooks * * @since 2.2 * * @param array $fields * @param array $filter * @param string $status * @param int $page * * @return array */ public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $filter['page'] = $page; $query = $this->query_webhooks( $filter ); $webhooks = array(); foreach ( $query['results'] as $webhook_id ) { $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); } $this->server->add_pagination_headers( $query['headers'] ); return array( 'webhooks' => $webhooks ); } /** * Get the webhook for the given ID * * @since 2.2 * @param int $id webhook ID * @param array $fields * @return array|WP_Error */ public function get_webhook( $id, $fields = null ) { // ensure webhook ID is valid & user has permission to read $id = $this->validate_request( $id, 'shop_webhook', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $webhook = wc_get_webhook( $id ); $webhook_data = array( 'id' => $webhook->get_id(), 'name' => $webhook->get_name(), 'status' => $webhook->get_status(), 'topic' => $webhook->get_topic(), 'resource' => $webhook->get_resource(), 'event' => $webhook->get_event(), 'hooks' => $webhook->get_hooks(), 'delivery_url' => $webhook->get_delivery_url(), 'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. ); return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) ); } /** * Get the total number of webhooks * * @since 2.2 * * @param string $status * @param array $filter * * @return array|WP_Error */ public function get_webhooks_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 ); } if ( ! empty( $status ) ) { $filter['status'] = $status; } $query = $this->query_webhooks( $filter ); return array( 'count' => $query['headers']->total ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create an webhook * * @since 2.2 * * @param array $data parsed webhook data * * @return array|WP_Error */ public function create_webhook( $data ) { try { if ( ! isset( $data['webhook'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 ); } $data = $data['webhook']; // permission check if ( ! current_user_can( 'manage_woocommerce' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this ); // validate topic if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 ); } // validate delivery URL if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); } $webhook_data = apply_filters( 'woocommerce_new_webhook_data', array( 'post_type' => 'shop_webhook', 'post_status' => 'publish', 'ping_status' => 'closed', 'post_author' => get_current_user_id(), 'post_password' => 'webhook_' . wp_generate_password(), 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ), $data, $this ); $webhook = new WC_Webhook(); $webhook->set_name( $webhook_data['post_title'] ); $webhook->set_user_id( $webhook_data['post_author'] ); $webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' ); $webhook->set_topic( $data['topic'] ); $webhook->set_delivery_url( $data['delivery_url'] ); $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) ); $webhook->set_api_version( 'legacy_v3' ); $webhook->save(); $webhook->deliver_ping(); // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this ); return $this->get_webhook( $webhook->get_id() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a webhook * * @since 2.2 * * @param int $id webhook ID * @param array $data parsed webhook data * * @return array|WP_Error */ public function edit_webhook( $id, $data ) { try { if ( ! isset( $data['webhook'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 ); } $data = $data['webhook']; $id = $this->validate_request( $id, 'shop_webhook', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this ); $webhook = wc_get_webhook( $id ); // update topic if ( ! empty( $data['topic'] ) ) { if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { $webhook->set_topic( $data['topic'] ); } else { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 ); } } // update delivery URL if ( ! empty( $data['delivery_url'] ) ) { if ( wc_is_valid_url( $data['delivery_url'] ) ) { $webhook->set_delivery_url( $data['delivery_url'] ); } else { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); } } // update secret if ( ! empty( $data['secret'] ) ) { $webhook->set_secret( $data['secret'] ); } // update status if ( ! empty( $data['status'] ) ) { $webhook->set_status( $data['status'] ); } // update name if ( ! empty( $data['name'] ) ) { $webhook->set_name( $data['name'] ); } $webhook->save(); do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this ); return $this->get_webhook( $webhook->get_id() ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a webhook * * @since 2.2 * @param int $id webhook ID * @return array|WP_Error */ public function delete_webhook( $id ) { $id = $this->validate_request( $id, 'shop_webhook', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_webhook', $id, $this ); $webhook = wc_get_webhook( $id ); return $webhook->delete( true ); } /** * Helper method to get webhook post objects * * @since 2.2 * @param array $args Request arguments for filtering query. * @return array */ private function query_webhooks( $args ) { $args = $this->merge_query_args( array(), $args ); $args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) ); if ( empty( $args['offset'] ) ) { $args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0; } $page = $args['paged']; unset( $args['paged'], $args['posts_per_page'] ); if ( isset( $args['s'] ) ) { $args['search'] = $args['s']; unset( $args['s'] ); } // Post type to webhook status. if ( ! empty( $args['post_status'] ) ) { $args['status'] = $args['post_status']; unset( $args['post_status'] ); } if ( ! empty( $args['post__in'] ) ) { $args['include'] = $args['post__in']; unset( $args['post__in'] ); } if ( ! empty( $args['date_query'] ) ) { foreach ( $args['date_query'] as $date_query ) { if ( 'post_date_gmt' === $date_query['column'] ) { $args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; $args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; } elseif ( 'post_modified_gmt' === $date_query['column'] ) { $args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; $args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; } } unset( $args['date_query'] ); } $args['paginate'] = true; // Get the webhooks. $data_store = WC_Data_Store::load( 'webhook' ); $results = $data_store->search_webhooks( $args ); // Get total items. $headers = new stdClass; $headers->page = $page; $headers->total = $results->total; $headers->is_single = $args['limit'] > $headers->total; $headers->total_pages = $results->max_num_pages; return array( 'results' => $results->webhooks, 'headers' => $headers, ); } /** * Get deliveries for a webhook * * @since 2.2 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @param string $webhook_id webhook ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_webhook_deliveries( $webhook_id, $fields = null ) { // Ensure ID is valid webhook ID $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); if ( is_wp_error( $webhook_id ) ) { return $webhook_id; } return array( 'webhook_deliveries' => array() ); } /** * Get the delivery log for the given webhook ID and delivery ID * * @since 2.2 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. * @param string $webhook_id webhook ID * @param string $id delivery log ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_webhook_delivery( $webhook_id, $id, $fields = null ) { try { // Validate webhook ID $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); if ( is_wp_error( $webhook_id ) ) { return $webhook_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 ); } $webhook = new WC_Webhook( $webhook_id ); $log = 0; if ( ! $log ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); } return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer. * 2) the ID returns a valid post object and matches the provided post type. * 3) the current user has the proper permissions to read/edit/delete the post. * * @since 3.3.0 * @param string|int $id The post ID * @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`. * @param string $context The context of the request, either `read`, `edit` or `delete`. * @return int|WP_Error Valid post ID or WP_Error if any of the checks fails. */ protected function validate_request( $id, $type, $context ) { $id = absint( $id ); // Validate ID. if ( empty( $id ) ) { return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); } $webhook = wc_get_webhook( $id ); if ( null === $webhook ) { return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) ); } // Validate permissions. switch ( $context ) { case 'read': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! current_user_can( 'manage_woocommerce' ) ) { return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); } break; } return $id; } } includes/legacy/api/v2/class-wc-api-reports.php 0000644 00000023071 15132754524 0015421 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Reports extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/reports'; /** @var WC_Admin_Report instance */ private $report; /** * Register the routes for this class * * GET /reports * GET /reports/sales * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /reports $routes[ $this->base ] = array( array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), ); # GET /reports/sales $routes[ $this->base . '/sales' ] = array( array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), ); # GET /reports/sales/top_sellers $routes[ $this->base . '/sales/top_sellers' ] = array( array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get a simple listing of available reports * * @since 2.1 * @return array */ public function get_reports() { return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); } /** * Get the sales report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_sales_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); // check for WP_Error if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); // new customers $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), 'role' => 'customer', ) ); $customers = $users_query->get_results(); foreach ( $customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { unset( $customers[ $key ] ); } } $total_customers = count( $customers ); $report_data = $this->report->get_report_data(); $period_totals = array(); // setup period totals by ensuring each period in the interval has data for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { switch ( $this->report->chart_groupby ) { case 'day' : $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); break; default : $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); break; } // set the customer signups for each period $customer_count = 0; foreach ( $customers as $customer ) { if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { $customer_count++; } } $period_totals[ $time ] = array( 'sales' => wc_format_decimal( 0.00, 2 ), 'orders' => 0, 'items' => 0, 'tax' => wc_format_decimal( 0.00, 2 ), 'shipping' => wc_format_decimal( 0.00, 2 ), 'discount' => wc_format_decimal( 0.00, 2 ), 'customers' => $customer_count, ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $report_data->orders as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); } foreach ( $report_data->order_counts as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['orders'] = (int) $order->count; } // add total order items for each period foreach ( $report_data->order_items as $order_item ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // add total discount for each period foreach ( $report_data->coupons as $discount ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } $sales_data = array( 'total_sales' => $report_data->total_sales, 'net_sales' => $report_data->net_sales, 'average_sales' => $report_data->average_sales, 'total_orders' => $report_data->total_orders, 'total_items' => $report_data->total_items, 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), 'total_shipping' => $report_data->total_shipping, 'total_refunds' => $report_data->total_refunds, 'total_discount' => $report_data->total_coupons, 'totals_grouped_by' => $this->report->chart_groupby, 'totals' => $period_totals, 'total_customers' => $total_customers, ); return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); } /** * Get the top sellers report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_top_sellers_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); $top_sellers = $this->report->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); $top_sellers_data = array(); foreach ( $top_sellers as $top_seller ) { $product = wc_get_product( $top_seller->product_id ); if ( $product ) { $top_sellers_data[] = array( 'title' => $product->get_name(), 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->order_item_qty, ); } } return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); } /** * Setup the report object and parse any date filtering * * @since 2.1 * @param array $filter date filtering */ private function setup_report( $filter ) { include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); $this->report = new WC_Report_Sales_By_Date(); if ( empty( $filter['period'] ) ) { // custom date range $filter['period'] = 'custom'; if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; } else { // default custom range to today $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { // ensure period is valid if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { $filter['period'] = 'week'; } // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods // allow "week" for period instead of "7day" if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; } } $this->report->calculate_current_range( $filter['period'] ); } /** * Verify that the current user has permission to view reports * * @since 2.1 * @see WC_API_Resource::validate_request() * * @param null $id unused * @param null $type unused * @param null $context unused * * @return bool|WP_Error */ protected function validate_request( $id = null, $type = null, $context = null ) { if ( ! current_user_can( 'view_woocommerce_reports' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); } else { return true; } } } includes/legacy/api/v2/class-wc-api-orders.php 0000644 00000167337 15132754524 0015237 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Orders extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/orders'; /** @var string $post_type the custom post type */ protected $post_type = 'shop_order'; /** * Register the routes for this class * * GET|POST /orders * GET /orders/count * GET|PUT|DELETE /orders/<id> * GET /orders/<id>/notes * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET|POST /orders $routes[ $this->base ] = array( array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /orders/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), ); # GET /orders/statuses $routes[ $this->base . '/statuses' ] = array( array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), ); # GET|PUT|DELETE /orders/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_order' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), ); # GET|POST /orders/<id>/notes $routes[ $this->base . '/(?P<order_id>\d+)/notes' ] = array( array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET|PUT|DELETE /orders/<order_id>/notes/<id> $routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array( array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), ); # GET|POST /orders/<order_id>/refunds $routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array( array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET|PUT|DELETE /orders/<order_id>/refunds/<id> $routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array( array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), ); # POST|PUT /orders/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all orders * * @since 2.1 * @param string $fields * @param array $filter * @param string $status * @param int $page * @return array */ public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $filter['page'] = $page; $query = $this->query_orders( $filter ); $orders = array(); foreach ( $query->posts as $order_id ) { if ( ! $this->is_readable( $order_id ) ) { continue; } $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); } $this->server->add_pagination_headers( $query ); return array( 'orders' => $orders ); } /** * Get the order for the given ID * * @since 2.1 * @param int $id the order ID * @param array $fields * @param array $filter * @return array|WP_Error */ public function get_order( $id, $fields = null, $filter = array() ) { // ensure order ID is valid & user has permission to read $id = $this->validate_request( $id, $this->post_type, 'read' ); if ( is_wp_error( $id ) ) { return $id; } // Get the decimal precession $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); $order = wc_get_order( $id ); $order_data = array( 'id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. 'status' => $order->get_status(), 'currency' => $order->get_currency(), 'total' => wc_format_decimal( $order->get_total(), $dp ), 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), 'total_line_items_quantity' => $order->get_item_count(), 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->get_payment_method(), 'method_title' => $order->get_payment_method_title(), 'paid' => ! is_null( $order->get_date_paid() ), ), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), 'note' => $order->get_customer_note(), 'customer_ip' => $order->get_customer_ip_address(), 'customer_user_agent' => $order->get_customer_user_agent(), 'customer_id' => $order->get_user_id(), 'view_order_url' => $order->get_view_order_url(), 'line_items' => array(), 'shipping_lines' => array(), 'tax_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), ); // add line items foreach ( $order->get_items() as $item_id => $item ) { $product = $item->get_product(); $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; $item_meta = $item->get_formatted_meta_data( $hideprefix ); foreach ( $item_meta as $key => $values ) { $item_meta[ $key ]->label = $values->display_key; unset( $item_meta[ $key ]->display_key ); unset( $item_meta[ $key ]->display_value ); } $order_data['line_items'][] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'quantity' => $item->get_quantity(), 'tax_class' => $item->get_tax_class(), 'name' => $item->get_name(), 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => array_values( $item_meta ), ); } // add shipping foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $order_data['shipping_lines'][] = array( 'id' => $shipping_item_id, 'method_id' => $shipping_item->get_method_id(), 'method_title' => $shipping_item->get_name(), 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), ); } // add taxes foreach ( $order->get_tax_totals() as $tax_code => $tax ) { $order_data['tax_lines'][] = array( 'id' => $tax->id, 'rate_id' => $tax->rate_id, 'code' => $tax_code, 'title' => $tax->label, 'total' => wc_format_decimal( $tax->amount, $dp ), 'compound' => (bool) $tax->is_compound, ); } // add fees foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $order_data['fee_lines'][] = array( 'id' => $fee_item_id, 'title' => $fee_item->get_name(), 'tax_class' => $fee_item->get_tax_class(), 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), ); } // add coupons foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $order_data['coupon_lines'][] = array( 'id' => $coupon_item_id, 'code' => $coupon_item->get_code(), 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), ); } return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); } /** * Get the total number of orders * * @since 2.4 * * @param string $status * @param array $filter * * @return array|WP_Error */ public function get_orders_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); } if ( ! empty( $status ) ) { if ( 'any' === $status ) { $order_statuses = array(); foreach ( wc_get_order_statuses() as $slug => $name ) { $filter['status'] = str_replace( 'wc-', '', $slug ); $query = $this->query_orders( $filter ); $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; } return array( 'count' => $order_statuses ); } else { $filter['status'] = $status; } } $query = $this->query_orders( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get a list of valid order statuses * * Note this requires no specific permissions other than being an authenticated * API user. Order statuses (particularly custom statuses) could be considered * private information which is why it's not in the API index. * * @since 2.1 * @return array */ public function get_order_statuses() { $order_statuses = array(); foreach ( wc_get_order_statuses() as $slug => $name ) { $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; } return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); } /** * Create an order * * @since 2.2 * * @param array $data raw order data * * @return array|WP_Error */ public function create_order( $data ) { global $wpdb; try { if ( ! isset( $data['order'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); } $data = $data['order']; // permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); // default order args, note that status is checked for validity in wc_create_order() $default_order_args = array( 'status' => isset( $data['status'] ) ? $data['status'] : '', 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, ); // if creating order for existing customer if ( ! empty( $data['customer_id'] ) ) { // make sure customer exists if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } $default_order_args['customer_id'] = $data['customer_id']; } // create the pending order $order = $this->create_base_order( $default_order_args, $data ); if ( is_wp_error( $order ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); } // billing/shipping addresses $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $set_item = "set_{$line_type}"; foreach ( $data[ $line ] as $item ) { $this->$set_item( $order, $item, 'create' ); } } } // calculate totals and set them $order->calculate_totals(); // payment method (and payment_complete() if `paid` == true) if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // method ID & title are required if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); // mark as paid if set if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // set order currency if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); } // set order meta if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->get_id(), $data['order_meta'] ); } // HTTP 201 Created $this->server->send_status( 201 ); wc_delete_shop_order_transients( $order ); do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); do_action( 'woocommerce_new_order', $order->get_id() ); return $this->get_order( $order->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Creates new WC_Order. * * Requires a separate function for classes that extend WC_API_Orders. * * @since 2.3 * * @param $args array * @param $data * * @return WC_Order */ protected function create_base_order( $args, $data ) { return wc_create_order( $args ); } /** * Edit an order * * @since 2.2 * * @param int $id the order ID * @param array $data * * @return array|WP_Error */ public function edit_order( $id, $data ) { try { if ( ! isset( $data['order'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); } $data = $data['order']; $update_totals = false; $id = $this->validate_request( $id, $this->post_type, 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); $order = wc_get_order( $id ); if ( empty( $order ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); } $order_args = array( 'order_id' => $order->get_id() ); // Customer note. if ( isset( $data['note'] ) ) { $order_args['customer_note'] = $data['note']; } // Customer ID. if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { // Make sure customer exists. if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); } // Billing/shipping address. $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $update_totals = true; foreach ( $data[ $line ] as $item ) { // Item ID is always required. if ( ! array_key_exists( 'id', $item ) ) { $item['id'] = null; } // Create item. if ( is_null( $item['id'] ) ) { $this->set_item( $order, $line_type, $item, 'create' ); } elseif ( $this->item_is_null( $item ) ) { // Delete item. wc_delete_order_item( $item['id'] ); } else { // Update item. $this->set_item( $order, $line_type, $item, 'update' ); } } } } // Payment method (and payment_complete() if `paid` == true and order needs payment). if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // Method ID. if ( isset( $data['payment_details']['method_id'] ) ) { update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); } // Method title. if ( isset( $data['payment_details']['method_title'] ) ) { update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); } // Mark as paid if set. if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // Set order currency. if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); } // If items have changed, recalculate order totals. if ( $update_totals ) { $order->calculate_totals(); } // Update order meta. if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->get_id(), $data['order_meta'] ); } // Update the order post to set customer note/modified date. wc_update_order( $order_args ); // Order status. if ( ! empty( $data['status'] ) ) { // Refresh the order instance. $order = wc_get_order( $order->get_id() ); $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); } wc_delete_shop_order_transients( $order ); do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); do_action( 'woocommerce_update_order', $order->get_id() ); return $this->get_order( $id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete an order * * @param int $id the order ID * @param bool $force true to permanently delete order, false to move to trash * @return array|WP_Error */ public function delete_order( $id, $force = false ) { $id = $this->validate_request( $id, $this->post_type, 'delete' ); if ( is_wp_error( $id ) ) { return $id; } wc_delete_shop_order_transients( $id ); do_action( 'woocommerce_api_delete_order', $id, $this ); return $this->delete( $id, 'order', ( 'true' === $force ) ); } /** * Helper method to get order post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ protected function query_orders( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => $this->post_type, 'post_status' => array_keys( wc_get_order_statuses() ), ); // add status argument if ( ! empty( $args['status'] ) ) { $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); $statuses = explode( ',', $statuses ); $query_args['post_status'] = $statuses; unset( $args['status'] ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Helper method to set/update the billing & shipping addresses for * an order * * @since 2.1 * @param \WC_Order $order * @param array $data */ protected function set_order_addresses( $order, $data ) { $address_fields = array( 'first_name', 'last_name', 'company', 'email', 'phone', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ); $billing_address = $shipping_address = array(); // billing address if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['billing_address'][ $field ] ) ) { $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); } } unset( $address_fields['email'] ); unset( $address_fields['phone'] ); } // shipping address if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['shipping_address'][ $field ] ) ) { $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); } } } $this->update_address( $order, $billing_address, 'billing' ); $this->update_address( $order, $shipping_address, 'shipping' ); // update user meta if ( $order->get_user_id() ) { foreach ( $billing_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); } foreach ( $shipping_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); } } } /** * Update address. * * @param WC_Order $order * @param array $posted * @param string $type */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { $order->{"set_{$type}_{$key}"}( $value ); } } } /** * Helper method to add/update order meta, with two restrictions: * * 1) Only non-protected meta (no leading underscore) can be set * 2) Meta values must be scalar (int, string, bool) * * @since 2.2 * @param int $order_id valid order ID * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format */ protected function set_order_meta( $order_id, $order_meta ) { foreach ( $order_meta as $meta_key => $meta_value ) { if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { update_post_meta( $order_id, $meta_key, $meta_value ); } } } /** * Helper method to check if the resource ID associated with the provided item is null * * Items can be deleted by setting the resource ID to null * * @since 2.2 * @param array $item item provided in the request body * @return bool true if the item resource ID is null, false otherwise */ protected function item_is_null( $item ) { $keys = array( 'product_id', 'method_id', 'title', 'code' ); foreach ( $keys as $key ) { if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { return true; } } return false; } /** * Wrapper method to create/update order items * * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @since 2.2 * @param \WC_Order $order order * @param string $item_type * @param array $item item provided in the request body * @param string $action either 'create' or 'update' * @throws WC_API_Exception if item ID is not associated with order */ protected function set_item( $order, $item_type, $item, $action ) { global $wpdb; $set_method = "set_{$item_type}"; // verify provided line item ID is associated with order if ( 'update' === $action ) { $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", absint( $item['id'] ), absint( $order->get_id() ) ) ); if ( is_null( $result ) ) { throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); } } $this->$set_method( $order, $item, $action ); } /** * Create or update a line item * * @since 2.2 * @param \WC_Order $order * @param array $item line item data * @param string $action 'create' to add line item or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_line_item( $order, $item, $action ) { $creating = ( 'create' === $action ); // product is always required if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); } // when updating, ensure product ID provided matches if ( 'update' === $action ) { $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); } } if ( isset( $item['product_id'] ) ) { $product_id = $item['product_id']; } elseif ( isset( $item['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $item['sku'] ); } // variations must each have a key & value $variation_id = 0; if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { foreach ( $item['variations'] as $key => $value ) { if ( ! $key || ! $value ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); } } $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); } $product = wc_get_product( $variation_id ? $variation_id : $product_id ); // must be a valid WC_Product if ( ! is_object( $product ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); } // quantity must be positive float if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); } // quantity is required when creating if ( $creating && ! isset( $item['quantity'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); } if ( $creating ) { $line_item = new WC_Order_Item_Product(); } else { $line_item = new WC_Order_Item_Product( $item['id'] ); } $line_item->set_product( $product ); $line_item->set_order_id( $order->get_id() ); if ( isset( $item['quantity'] ) ) { $line_item->set_quantity( $item['quantity'] ); } if ( isset( $item['total'] ) ) { $line_item->set_total( floatval( $item['total'] ) ); } elseif ( $creating ) { $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); $line_item->set_total( $total ); $line_item->set_subtotal( $total ); } if ( isset( $item['total_tax'] ) ) { $line_item->set_total_tax( floatval( $item['total_tax'] ) ); } if ( isset( $item['subtotal'] ) ) { $line_item->set_subtotal( floatval( $item['subtotal'] ) ); } if ( isset( $item['subtotal_tax'] ) ) { $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); } if ( $variation_id ) { $line_item->set_variation_id( $variation_id ); $line_item->set_variation( $item['variations'] ); } // Save or add to order. if ( $creating ) { $order->add_item( $line_item ); } else { $item_id = $line_item->save(); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); } } } /** * Given a product ID & API provided variations, find the correct variation ID to use for calculation * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass * the cheapest variation ID but provide other information so we have to look up the variation ID. * * @param WC_Product $product * @param array $variations * * @return int returns an ID if a valid variation was found for this product */ function get_variation_id( $product, $variations = array() ) { $variation_id = null; $variations_normalized = array(); if ( $product->is_type( 'variable' ) && $product->has_child() ) { if ( isset( $variations ) && is_array( $variations ) ) { // start by normalizing the passed variations foreach ( $variations as $key => $value ) { $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php $variations_normalized[ $key ] = strtolower( $value ); } // now search through each product child and see if our passed variations match anything foreach ( $product->get_children() as $variation ) { $meta = array(); foreach ( get_post_meta( $variation ) as $key => $value ) { $value = $value[0]; $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); $meta[ $key ] = strtolower( $value ); } // if the variation array is a part of the $meta array, we found our match if ( $this->array_contains( $variations_normalized, $meta ) ) { $variation_id = $variation; break; } } } } return $variation_id; } /** * Utility function to see if the meta array contains data from variations * * @param array $needles * @param array $haystack * * @return bool */ protected function array_contains( $needles, $haystack ) { foreach ( $needles as $key => $value ) { if ( $haystack[ $key ] !== $value ) { return false; } } return true; } /** * Create or update an order shipping method * * @since 2.2 * @param \WC_Order $order * @param array $shipping item data * @param string $action 'create' to add shipping or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_shipping( $order, $shipping, $action ) { // total must be a positive float if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // method ID is required if ( ! isset( $shipping['method_id'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); } $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); $item = new WC_Order_Item_Shipping(); $item->set_order_id( $order->get_id() ); $item->set_shipping_rate( $rate ); $order->add_item( $item ); } else { $item = new WC_Order_Item_Shipping( $shipping['id'] ); if ( isset( $shipping['method_id'] ) ) { $item->set_method_id( $shipping['method_id'] ); } if ( isset( $shipping['method_title'] ) ) { $item->set_method_title( $shipping['method_title'] ); } if ( isset( $shipping['total'] ) ) { $item->set_total( floatval( $shipping['total'] ) ); } $shipping_id = $item->save(); if ( ! $shipping_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order fee * * @since 2.2 * @param \WC_Order $order * @param array $fee item data * @param string $action 'create' to add fee or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_fee( $order, $fee, $action ) { if ( 'create' === $action ) { // fee title is required if ( ! isset( $fee['title'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); } $item = new WC_Order_Item_Fee(); $item->set_order_id( $order->get_id() ); $item->set_name( wc_clean( $fee['title'] ) ); $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); // if taxable, tax class and total are required if ( ! empty( $fee['taxable'] ) ) { if ( ! isset( $fee['tax_class'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); } $item->set_tax_status( 'taxable' ); $item->set_tax_class( $fee['tax_class'] ); if ( isset( $fee['total_tax'] ) ) { $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); } if ( isset( $fee['tax_data'] ) ) { $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); } } $order->add_item( $item ); } else { $item = new WC_Order_Item_Fee( $fee['id'] ); if ( isset( $fee['title'] ) ) { $item->set_name( wc_clean( $fee['title'] ) ); } if ( isset( $fee['tax_class'] ) ) { $item->set_tax_class( $fee['tax_class'] ); } if ( isset( $fee['total'] ) ) { $item->set_total( floatval( $fee['total'] ) ); } if ( isset( $fee['total_tax'] ) ) { $item->set_total_tax( floatval( $fee['total_tax'] ) ); } $fee_id = $item->save(); if ( ! $fee_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order coupon * * @since 2.2 * @param \WC_Order $order * @param array $coupon item data * @param string $action 'create' to add coupon or 'update' to update it * @throws WC_API_Exception invalid data, server error */ protected function set_coupon( $order, $coupon, $action ) { // coupon amount must be positive float if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // coupon code is required if ( empty( $coupon['code'] ) ) { throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } $item = new WC_Order_Item_Coupon(); $item->set_props( array( 'code' => $coupon['code'], 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, 'discount_tax' => 0, 'order_id' => $order->get_id(), ) ); $order->add_item( $item ); } else { $item = new WC_Order_Item_Coupon( $coupon['id'] ); if ( isset( $coupon['code'] ) ) { $item->set_code( $coupon['code'] ); } if ( isset( $coupon['amount'] ) ) { $item->set_discount( floatval( $coupon['amount'] ) ); } $coupon_id = $item->save(); if ( ! $coupon_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); } } } /** * Get the admin order notes for an order * * @since 2.1 * @param string $order_id order ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_order_notes( $order_id, $fields = null ) { // ensure ID is valid order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $args = array( 'post_id' => $order_id, 'approve' => 'approve', 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $order_notes = array(); foreach ( $notes as $note ) { $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); } return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); } /** * Get an order note for the given order ID and ID * * @since 2.2 * * @param string $order_id order ID * @param string $id order note ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_order_note( $order_id, $id, $fields = null ) { try { // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } $order_note = array( 'id' => $note->comment_ID, 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new order note for the given order * * @since 2.2 * @param string $order_id order ID * @param array $data raw request data * @return WP_Error|array error or created note response data */ public function create_order_note( $order_id, $data ) { try { if ( ! isset( $data['order_note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); } $data = $data['order_note']; // permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); } $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $order = wc_get_order( $order_id ); $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); // note content is required if ( ! isset( $data['note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); } $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); // create the note $note_id = $order->add_order_note( $data['note'], $is_customer_note ); if ( ! $note_id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); } // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); return $this->get_order_note( $order->get_id(), $note_id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit the order note * * @since 2.2 * @param string $order_id order ID * @param string $id note ID * @param array $data parsed request data * @return WP_Error|array error or edited note response data */ public function edit_order_note( $order_id, $id, $data ) { try { if ( ! isset( $data['order_note'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); } $data = $data['order_note']; // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $order = wc_get_order( $order_id ); // Validate note ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } // Ensure note ID is valid $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } // Ensure note ID is associated with given order if ( $note->comment_post_ID != $order->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); // Note content if ( isset( $data['note'] ) ) { wp_update_comment( array( 'comment_ID' => $note->comment_ID, 'comment_content' => $data['note'], ) ); } // Customer note if ( isset( $data['customer_note'] ) ) { update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); } do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); return $this->get_order_note( $order->get_id(), $note->comment_ID ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete order note * * @since 2.2 * @param string $order_id order ID * @param string $id note ID * @return WP_Error|array error or deleted message */ public function delete_order_note( $order_id, $id ) { try { $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate note ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); } // Ensure note ID is valid $note = get_comment( $id ); if ( is_null( $note ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); } // Ensure note ID is associated with given order if ( $note->comment_post_ID != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); } // Force delete since trashed order notes could not be managed through comments list table $result = wc_delete_order_note( $note->comment_ID ); if ( ! $result ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); } do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the order refunds for an order * * @since 2.2 * @param string $order_id order ID * @param string|null $fields fields to include in response * @return array|WP_Error */ public function get_order_refunds( $order_id, $fields = null ) { // Ensure ID is valid order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $refund_items = wc_get_orders( array( 'type' => 'shop_order_refund', 'parent' => $order_id, 'limit' => -1, 'return' => 'ids', ) ); $order_refunds = array(); foreach ( $refund_items as $refund_id ) { $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); } return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); } /** * Get an order refund for the given order ID and ID * * @since 2.2 * * @param string $order_id order ID * @param int $id * @param string|null $fields fields to limit response to * @param array $filter * * @return array|WP_Error */ public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { try { // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); if ( is_wp_error( $order_id ) ) { return $order_id; } $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } $order = wc_get_order( $order_id ); $refund = wc_get_order( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } $line_items = array(); // Add line items foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { $product = $item->get_product(); $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; $item_meta = $item->get_formatted_meta_data( $hideprefix ); foreach ( $item_meta as $key => $values ) { $item_meta[ $key ]->label = $values->display_key; unset( $item_meta[ $key ]->display_key ); unset( $item_meta[ $key ]->display_value ); } $line_items[] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), 'quantity' => $item->get_quantity(), 'tax_class' => $item->get_tax_class(), 'name' => $item->get_name(), 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => array_values( $item_meta ), 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), ); } $order_refund = array( 'id' => $refund->get_id(), 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), 'reason' => $refund->get_reason(), 'line_items' => $line_items, ); return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new order refund for the given order * * @since 2.2 * @param string $order_id order ID * @param array $data raw request data * @param bool $api_refund do refund using a payment gateway API * @return WP_Error|array error or created refund response data */ public function create_order_refund( $order_id, $data, $api_refund = true ) { try { if ( ! isset( $data['order_refund'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); } $data = $data['order_refund']; // Permission check if ( ! current_user_can( 'publish_shop_orders' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); } $order_id = absint( $order_id ); if ( empty( $order_id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); // Refund amount is required if ( ! isset( $data['amount'] ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); } elseif ( 0 > $data['amount'] ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); } $data['order_id'] = $order_id; $data['refund_id'] = 0; // Create the refund $refund = wc_create_refund( $data ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } // Refund via API if ( $api_refund ) { if ( WC()->payment_gateways() ) { $payment_gateways = WC()->payment_gateways->payment_gateways(); } $order = wc_get_order( $order_id ); if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); if ( is_wp_error( $result ) ) { return $result; } elseif ( ! $result ) { throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); } } } // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); return $this->get_order_refund( $order_id, $refund->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit an order refund * * @since 2.2 * @param string $order_id order ID * @param string $id refund ID * @param array $data parsed request data * @return WP_Error|array error or edited refund response data */ public function edit_order_refund( $order_id, $id, $data ) { try { if ( ! isset( $data['order_refund'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); } $data = $data['order_refund']; // Validate order ID $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate refund ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } // Ensure order ID is valid $refund = get_post( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } // Ensure refund ID is associated with given order if ( $refund->post_parent != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); } $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); // Update reason if ( isset( $data['reason'] ) ) { $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); if ( is_wp_error( $updated_refund ) ) { return $updated_refund; } } // Update refund amount if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); } do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); return $this->get_order_refund( $order_id, $refund->ID ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete order refund * * @since 2.2 * @param string $order_id order ID * @param string $id refund ID * @return WP_Error|array error or deleted message */ public function delete_order_refund( $order_id, $id ) { try { $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Validate refund ID $id = absint( $id ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); } // Ensure refund ID is valid $refund = get_post( $id ); if ( ! $refund ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); } // Ensure refund ID is associated with given order if ( $refund->post_parent != $order_id ) { throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); } wc_delete_shop_order_transients( $order_id ); do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); return $this->delete( $refund->ID, 'refund', true ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Bulk update or insert orders * Accepts an array with orders in the formats supported by * WC_API_Orders->create_order() and WC_API_Orders->edit_order() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['orders'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); } $data = $data['orders']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $orders = array(); foreach ( $data as $_order ) { $order_id = 0; // Try to get the order ID if ( isset( $_order['id'] ) ) { $order_id = intval( $_order['id'] ); } // Order exists / edit order if ( $order_id ) { $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); if ( is_wp_error( $edit ) ) { $orders[] = array( 'id' => $order_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $orders[] = $edit['order']; } } else { // Order don't exists / create order $new = $this->create_order( array( 'order' => $_order ) ); if ( is_wp_error( $new ) ) { $orders[] = array( 'id' => $order_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $orders[] = $new['order']; } } } return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v2/class-wc-api-coupons.php 0000644 00000050074 15132754524 0015414 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Coupons extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/coupons'; /** * Register the routes for this class * * GET /coupons * GET /coupons/count * GET /coupons/<id> * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /coupons $routes[ $this->base ] = array( array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /coupons/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), ); # GET/PUT/DELETE /coupons/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ), ); # GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores $routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array( array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), ); # POST|PUT /coupons/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all coupons * * @since 2.1 * @param string $fields * @param array $filter * @param int $page * @return array */ public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_coupons( $filter ); $coupons = array(); foreach ( $query->posts as $coupon_id ) { if ( ! $this->is_readable( $coupon_id ) ) { continue; } $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'coupons' => $coupons ); } /** * Get the coupon for the given ID * * @since 2.1 * @param int $id the coupon ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_coupon( $id, $fields = null ) { try { $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $coupon = new WC_Coupon( $id ); if ( 0 === $coupon->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); } $coupon_data = array( 'id' => $coupon->get_id(), 'code' => $coupon->get_code(), 'type' => $coupon->get_discount_type(), 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), 'individual_use' => $coupon->get_individual_use(), 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), 'usage_count' => (int) $coupon->get_usage_count(), 'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times. 'enable_free_shipping' => $coupon->get_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), 'exclude_sale_items' => $coupon->get_exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), 'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ), 'customer_emails' => $coupon->get_email_restrictions(), 'description' => $coupon->get_description(), ); return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the total number of coupons * * @since 2.1 * * @param array $filter * * @return array|WP_Error */ public function get_coupons_count( $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_coupons' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 ); } $query = $this->query_coupons( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the coupon for the given code * * @since 2.1 * @param string $code the coupon code * @param string $fields fields to include in response * @return int|WP_Error */ public function get_coupon_by_code( $code, $fields = null ) { global $wpdb; try { $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); if ( is_null( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 ); } return $this->get_coupon( $id, $fields ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a coupon * * @since 2.2 * * @param array $data * * @return array|WP_Error */ public function create_coupon( $data ) { global $wpdb; try { if ( ! isset( $data['coupon'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 ); } $data = $data['coupon']; // Check user permission if ( ! current_user_can( 'publish_shop_coupons' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this ); // Check if coupon code is specified if ( ! isset( $data['code'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 ); } $coupon_code = wc_format_coupon_code( $data['code'] ); $id_from_code = wc_get_coupon_id_by_code( $coupon_code ); if ( $id_from_code ) { throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); } $defaults = array( 'type' => 'fixed_cart', 'amount' => 0, 'individual_use' => false, 'product_ids' => array(), 'exclude_product_ids' => array(), 'usage_limit' => '', 'usage_limit_per_user' => '', 'limit_usage_to_x_items' => '', 'usage_count' => '', 'expiry_date' => '', 'enable_free_shipping' => false, 'product_category_ids' => array(), 'exclude_product_category_ids' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'customer_emails' => array(), 'description' => '', ); $coupon_data = wp_parse_args( $data, $defaults ); // Validate coupon types if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); } $new_coupon = array( 'post_title' => $coupon_code, 'post_content' => '', 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_type' => 'shop_coupon', 'post_excerpt' => $coupon_data['description'], ); $id = wp_insert_post( $new_coupon, true ); if ( is_wp_error( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 ); } // Set coupon meta update_post_meta( $id, 'discount_type', $coupon_data['type'] ); update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) ); update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) ); update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' ); update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) ); update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) ); do_action( 'woocommerce_api_create_coupon', $id, $data ); do_action( 'woocommerce_new_coupon', $id ); $this->server->send_status( 201 ); return $this->get_coupon( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a coupon * * @since 2.2 * * @param int $id the coupon ID * @param array $data * * @return array|WP_Error */ public function edit_coupon( $id, $data ) { try { if ( ! isset( $data['coupon'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 ); } $data = $data['coupon']; $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this ); if ( isset( $data['code'] ) ) { global $wpdb; $coupon_code = wc_format_coupon_code( $data['code'] ); $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); } $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) ); if ( 0 === $updated ) { throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); } } if ( isset( $data['description'] ) ) { $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) ); if ( 0 === $updated ) { throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); } } if ( isset( $data['type'] ) ) { // Validate coupon types if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); } update_post_meta( $id, 'discount_type', $data['type'] ); } if ( isset( $data['amount'] ) ) { update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); } if ( isset( $data['individual_use'] ) ) { update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); } if ( isset( $data['product_ids'] ) ) { update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); } if ( isset( $data['exclude_product_ids'] ) ) { update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); } if ( isset( $data['usage_limit'] ) ) { update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); } if ( isset( $data['usage_limit_per_user'] ) ) { update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); } if ( isset( $data['limit_usage_to_x_items'] ) ) { update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); } if ( isset( $data['usage_count'] ) ) { update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); } if ( isset( $data['expiry_date'] ) ) { update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) ); } if ( isset( $data['enable_free_shipping'] ) ) { update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); } if ( isset( $data['product_category_ids'] ) ) { update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); } if ( isset( $data['exclude_product_category_ids'] ) ) { update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); } if ( isset( $data['exclude_sale_items'] ) ) { update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); } if ( isset( $data['minimum_amount'] ) ) { update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); } if ( isset( $data['maximum_amount'] ) ) { update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); } if ( isset( $data['customer_emails'] ) ) { update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); } do_action( 'woocommerce_api_edit_coupon', $id, $data ); do_action( 'woocommerce_update_coupon', $id ); return $this->get_coupon( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a coupon * * @since 2.2 * @param int $id the coupon ID * @param bool $force true to permanently delete coupon, false to move to trash * @return array|WP_Error */ public function delete_coupon( $id, $force = false ) { $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_coupon', $id, $this ); return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); } /** * expiry_date format * * @since 2.3.0 * @param string $expiry_date * @param bool $as_timestamp (default: false) * @return string|int */ protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) { if ( '' != $expiry_date ) { if ( $as_timestamp ) { return strtotime( $expiry_date ); } return date( 'Y-m-d', strtotime( $expiry_date ) ); } return ''; } /** * Helper method to get coupon post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_coupons( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_coupon', 'post_status' => 'publish', ); $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Bulk update or insert coupons * Accepts an array with coupons in the formats supported by * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['coupons'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 ); } $data = $data['coupons']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $coupons = array(); foreach ( $data as $_coupon ) { $coupon_id = 0; // Try to get the coupon ID if ( isset( $_coupon['id'] ) ) { $coupon_id = intval( $_coupon['id'] ); } // Coupon exists / edit coupon if ( $coupon_id ) { $edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) ); if ( is_wp_error( $edit ) ) { $coupons[] = array( 'id' => $coupon_id, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $coupons[] = $edit['coupon']; } } else { // Coupon don't exists / create coupon $new = $this->create_coupon( array( 'coupon' => $_coupon ) ); if ( is_wp_error( $new ) ) { $coupons[] = array( 'id' => $coupon_id, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $coupons[] = $new['coupon']; } } } return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v2/class-wc-api-products.php 0000644 00000220632 15132754524 0015570 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Products extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/products'; /** * Register the routes for this class * * GET/POST /products * GET /products/count * GET/PUT/DELETE /products/<id> * GET /products/<id>/reviews * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET/POST /products $routes[ $this->base ] = array( array( array( $this, 'get_products' ), WC_API_Server::READABLE ), array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /products/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), ); # GET/PUT/DELETE /products/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_product' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), ); # GET /products/<id>/reviews $routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array( array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), ); # GET /products/<id>/orders $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array( array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), ); # GET /products/categories $routes[ $this->base . '/categories' ] = array( array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), ); # GET /products/categories/<id> $routes[ $this->base . '/categories/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), ); # GET/POST /products/attributes $routes[ $this->base . '/attributes' ] = array( array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), ); # GET/PUT/DELETE /attributes/<id> $routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array( array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), ); # GET /products/sku/<product sku> $routes[ $this->base . '/sku/(?P<sku>\w[\w\s\-]*)' ] = array( array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ), ); # POST|PUT /products/bulk $routes[ $this->base . '/bulk' ] = array( array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); return $routes; } /** * Get all products * * @since 2.1 * @param string $fields * @param string $type * @param array $filter * @param int $page * @return array */ public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { if ( ! empty( $type ) ) { $filter['type'] = $type; } $filter['page'] = $page; $query = $this->query_products( $filter ); $products = array(); foreach ( $query->posts as $product_id ) { if ( ! $this->is_readable( $product_id ) ) { continue; } $products[] = current( $this->get_product( $product_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'products' => $products ); } /** * Get the product for the given ID * * @since 2.1 * @param int $id the product ID * @param string $fields * @return array|WP_Error */ public function get_product( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); // add data that applies to every product type $product_data = $this->get_product_data( $product ); // add variations to variable products if ( $product->is_type( 'variable' ) && $product->has_child() ) { $product_data['variations'] = $this->get_variation_data( $product ); } // add the parent product data to an individual variation if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { $_product = wc_get_product( $product->get_parent_id() ); $product_data['parent'] = $this->get_product_data( $_product ); } return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); } /** * Get the total number of products * * @since 2.1 * * @param string $type * @param array $filter * * @return array|WP_Error */ public function get_products_count( $type = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_products' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); } if ( ! empty( $type ) ) { $filter['type'] = $type; } $query = $this->query_products( $filter ); return array( 'count' => (int) $query->found_posts ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Create a new product * * @since 2.2 * * @param array $data posted data * * @return array|WP_Error */ public function create_product( $data ) { $id = 0; try { if ( ! isset( $data['product'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); } $data = $data['product']; // Check permissions if ( ! current_user_can( 'publish_products' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); // Check if product title is specified if ( ! isset( $data['title'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); } // Check product type if ( ! isset( $data['type'] ) ) { $data['type'] = 'simple'; } // Set visible visibility when not sent if ( ! isset( $data['catalog_visibility'] ) ) { $data['catalog_visibility'] = 'visible'; } // Validate the product type if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); } // Enable description html tags. $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { $post_content = wp_filter_post_kses( $data['description'] ); } // Enable short description html tags. $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { $post_excerpt = wp_filter_post_kses( $data['short_description'] ); } $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); if ( ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } $product = new $classname(); $product->set_name( wc_clean( $data['title'] ) ); $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); $product->set_description( isset( $data['description'] ) ? $post_content : '' ); // Attempts to create the new product. $product->save(); $id = $product->get_id(); // Checks for an error in the product creation if ( 0 >= $id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); } // Check for featured/gallery images, upload it and set it if ( isset( $data['images'] ) ) { $product = $this->save_product_images( $product, $data['images'] ); } // Save product meta fields $product = $this->save_product_meta( $product, $data ); $product->save(); // Save variations if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $product, $data ); } do_action( 'woocommerce_api_create_product', $id, $data ); // Clear cache/transients wc_delete_product_transients( $id ); $this->server->send_status( 201 ); return $this->get_product( $id ); } catch ( WC_Data_Exception $e ) { $this->clear_product( $id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } catch ( WC_API_Exception $e ) { $this->clear_product( $id ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product * * @since 2.2 * * @param int $id the product ID * @param array $data * * @return array|WP_Error */ public function edit_product( $id, $data ) { try { if ( ! isset( $data['product'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); } $data = $data['product']; $id = $this->validate_request( $id, 'product', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); // Product title. if ( isset( $data['title'] ) ) { $product->set_name( wc_clean( $data['title'] ) ); } // Product name (slug). if ( isset( $data['name'] ) ) { $product->set_slug( wc_clean( $data['name'] ) ); } // Product status. if ( isset( $data['status'] ) ) { $product->set_status( wc_clean( $data['status'] ) ); } // Product short description. if ( isset( $data['short_description'] ) ) { // Enable short description html tags. $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); $product->set_short_description( $post_excerpt ); } // Product description. if ( isset( $data['description'] ) ) { // Enable description html tags. $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); $product->set_description( $post_content ); } // Validate the product type. if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); } // Check for featured/gallery images, upload it and set it. if ( isset( $data['images'] ) ) { $product = $this->save_product_images( $product, $data['images'] ); } // Save product meta fields. $product = $this->save_product_meta( $product, $data ); // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $product, $data ); } else { // Just sync variations. $product = WC_Product_Variable::sync( $product, false ); } } $product->save(); do_action( 'woocommerce_api_edit_product', $id, $data ); // Clear cache/transients. wc_delete_product_transients( $id ); return $this->get_product( $id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product. * * @since 2.2 * * @param int $id the product ID. * @param bool $force true to permanently delete order, false to move to trash. * * @return array|WP_Error */ public function delete_product( $id, $force = false ) { $id = $this->validate_request( $id, 'product', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); do_action( 'woocommerce_api_delete_product', $id, $this ); // If we're forcing, then delete permanently. if ( $force ) { if ( $product->is_type( 'variable' ) ) { foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->delete( true ); } } } else { // For other product types, if the product has children, remove the relationship. foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->set_parent_id( 0 ); $child->save(); } } } $product->delete( true ); $result = ! ( $product->get_id() > 0 ); } else { $product->delete(); $result = 'trash' === $product->get_status(); } if ( ! $result ) { return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); } // Delete parent product transients. if ( $parent_id = wp_get_post_parent_id( $id ) ) { wc_delete_product_transients( $parent_id ); } if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); } } /** * Get the reviews for a product * * @since 2.1 * @param int $id the product ID to get reviews for * @param string $fields fields to include in response * @return array|WP_Error */ public function get_product_reviews( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $comments = get_approved_comments( $id ); $reviews = array(); foreach ( $comments as $comment ) { $reviews[] = array( 'id' => intval( $comment->comment_ID ), 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), 'review' => $comment->comment_content, 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'reviewer_name' => $comment->comment_author, 'reviewer_email' => $comment->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), ); } return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); } /** * Get the orders for a product * * @since 2.4.0 * @param int $id the product ID to get orders for * @param string fields fields to retrieve * @param array $filter filters to include in response * @param string $status the order status to retrieve * @param $page $page page to retrieve * @return array|WP_Error */ public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { global $wpdb; $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $order_ids = $wpdb->get_col( $wpdb->prepare( " SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) AND order_item_type = 'line_item' ", $id ) ); if ( empty( $order_ids ) ) { return array( 'orders' => array() ); } $filter = array_merge( $filter, array( 'in' => implode( ',', $order_ids ), ) ); $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); } /** * Get a listing of product categories * * @since 2.2 * * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_categories( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); } $product_categories = array(); $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); foreach ( $terms as $term_id ) { $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); } return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product category for the given ID * * @since 2.2 * * @param string $id product category term ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_category( $id, $fields = null ) { try { $id = absint( $id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); } $term = get_term( $id, 'product_cat' ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); } $term_id = intval( $term->term_id ); // Get category display type $display_type = get_term_meta( $term_id, 'display_type', true ); // Get category image $image = ''; if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { $image = wp_get_attachment_url( $image_id ); } $product_category = array( 'id' => $term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent, 'description' => $term->description, 'display' => $display_type ? $display_type : 'default', 'image' => $image ? esc_url( $image ) : '', 'count' => intval( $term->count ), ); return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Helper method to get product post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_products( $args ) { // Set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'product', 'post_status' => 'publish', 'meta_query' => array(), ); if ( ! empty( $args['type'] ) ) { $types = explode( ',', $args['type'] ); $query_args['tax_query'] = array( array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $types, ), ); unset( $args['type'] ); } // Filter products by category if ( ! empty( $args['category'] ) ) { $query_args['product_cat'] = $args['category']; } // Filter by specific sku if ( ! empty( $args['sku'] ) ) { if ( ! is_array( $query_args['meta_query'] ) ) { $query_args['meta_query'] = array(); } $query_args['meta_query'][] = array( 'key' => '_sku', 'value' => $args['sku'], 'compare' => '=', ); $query_args['post_type'] = array( 'product', 'product_variation' ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Get standard product data that applies to every product type * * @since 2.1 * @param WC_Product|int $product * @return array */ private function get_product_data( $product ) { if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } if ( ! is_a( $product, 'WC_Product' ) ) { return array(); } $prices_precision = wc_get_price_decimals(); return array( 'title' => $product->get_name(), 'id' => $product->get_id(), 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), 'type' => $product->get_type(), 'status' => $product->get_status(), 'downloadable' => $product->is_downloadable(), 'virtual' => $product->is_virtual(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'price' => wc_format_decimal( $product->get_price(), $prices_precision ), 'regular_price' => wc_format_decimal( $product->get_regular_price(), $prices_precision ), 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : null, 'price_html' => $product->get_price_html(), 'taxable' => $product->is_taxable(), 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'managing_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'purchaseable' => $product->is_purchasable(), 'featured' => $product->is_featured(), 'visible' => $product->is_visible(), 'catalog_visibility' => $product->get_catalog_visibility(), 'on_sale' => $product->is_on_sale(), 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, 'dimensions' => array( 'length' => $product->get_length(), 'width' => $product->get_width(), 'height' => $product->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, 'description' => wpautop( do_shortcode( $product->get_description() ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), 'reviews_allowed' => $product->get_reviews_allowed(), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), 'parent_id' => $product->get_parent_id(), 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), 'images' => $this->get_images( $product ), 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => $product->get_download_limit(), 'download_expiry' => $product->get_download_expiry(), 'download_type' => 'standard', 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), 'total_sales' => $product->get_total_sales(), 'variations' => array(), 'parent' => array(), ); } /** * Get an individual variation's data * * @since 2.1 * @param WC_Product $product * @return array */ private function get_variation_data( $product ) { $prices_precision = wc_get_price_decimals(); $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = wc_get_product( $child_id ); if ( ! $variation || ! $variation->exists() ) { continue; } $variations[] = array( 'id' => $variation->get_id(), 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), 'downloadable' => $variation->is_downloadable(), 'virtual' => $variation->is_virtual(), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => wc_format_decimal( $variation->get_price(), $prices_precision ), 'regular_price' => wc_format_decimal( $variation->get_regular_price(), $prices_precision ), 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null, 'taxable' => $variation->is_taxable(), 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'managing_stock' => $variation->managing_stock(), 'stock_quantity' => (int) $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backordered' => $variation->is_on_backorder(), 'purchaseable' => $variation->is_purchasable(), 'visible' => $variation->variation_is_visible(), 'on_sale' => $variation->is_on_sale(), 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null, 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => (int) $product->get_download_limit(), 'download_expiry' => (int) $product->get_download_expiry(), ); } return $variations; } /** * Save default attributes. * * @since 3.0.0 * @param WC_Product $product * @param array $request * @return WC_Product */ protected function save_default_attributes( $product, $request ) { // Update default attributes options setting. if ( isset( $request['default_attribute'] ) ) { $request['default_attributes'] = $request['default_attribute']; } if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { $attributes = $product->get_attributes(); $default_attributes = array(); foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { if ( ! isset( $default_attr['name'] ) ) { continue; } $taxonomy = sanitize_title( $default_attr['name'] ); if ( isset( $default_attr['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; if ( $_attribute['is_variation'] ) { $value = ''; if ( isset( $default_attr['option'] ) ) { if ( $_attribute['is_taxonomy'] ) { // Don't use wc_clean as it destroys sanitized characters. $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); } else { $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); } } if ( $value ) { $default_attributes[ $taxonomy ] = $value; } } } } $product->set_default_attributes( $default_attributes ); } return $product; } /** * Save product meta * * @since 2.2 * @param WC_Product $product * @param array $data * @return WC_Product * @throws WC_API_Exception */ protected function save_product_meta( $product, $data ) { global $wpdb; // Virtual if ( isset( $data['virtual'] ) ) { $product->set_virtual( $data['virtual'] ); } // Tax status if ( isset( $data['tax_status'] ) ) { $product->set_tax_status( wc_clean( $data['tax_status'] ) ); } // Tax Class if ( isset( $data['tax_class'] ) ) { $product->set_tax_class( wc_clean( $data['tax_class'] ) ); } // Catalog Visibility if ( isset( $data['catalog_visibility'] ) ) { $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); } // Purchase Note if ( isset( $data['purchase_note'] ) ) { $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); } // Featured Product if ( isset( $data['featured'] ) ) { $product->set_featured( $data['featured'] ); } // Shipping data $product = $this->save_product_shipping_data( $product, $data ); // SKU if ( isset( $data['sku'] ) ) { $sku = $product->get_sku(); $new_sku = wc_clean( $data['sku'] ); if ( '' == $new_sku ) { $product->set_sku( '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); if ( ! $unique_sku ) { throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); } else { $product->set_sku( $new_sku ); } } else { $product->set_sku( '' ); } } } // Attributes if ( isset( $data['attributes'] ) ) { $attributes = array(); foreach ( $data['attributes'] as $attribute ) { $is_taxonomy = 0; $taxonomy = 0; if ( ! isset( $attribute['name'] ) ) { continue; } $attribute_slug = sanitize_title( $attribute['name'] ); if ( isset( $attribute['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); $attribute_slug = sanitize_title( $attribute['slug'] ); } if ( $taxonomy ) { $is_taxonomy = 1; } if ( $is_taxonomy ) { $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } // Update post terms if ( taxonomy_exists( $taxonomy ) ) { wp_set_object_terms( $product->get_id(), $values, $taxonomy ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $taxonomy ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['options'] ) ) { // Array based if ( is_array( $attribute['options'] ) ) { $values = $attribute['options']; // Text based, separate by pipe } else { $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); } // Custom attribute - Add attribute to array and set the values. $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute['name'] ); $attribute_object->set_options( $values ); $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); $attributes[] = $attribute_object; } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); $product->set_attributes( $attributes ); } // Sales and prices. if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices. $product->set_regular_price( '' ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->set_price( '' ); } else { // Regular Price. if ( isset( $data['regular_price'] ) ) { $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; $product->set_regular_price( $regular_price ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; $product->set_sale_price( $sale_price ); } if ( isset( $data['sale_price_dates_from'] ) ) { $date_from = $data['sale_price_dates_from']; } else { $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; } if ( isset( $data['sale_price_dates_to'] ) ) { $date_to = $data['sale_price_dates_to']; } else { $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; } if ( $date_to && ! $date_from ) { $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); } $product->set_date_on_sale_to( $date_to ); $product->set_date_on_sale_from( $date_from ); if ( $product->is_on_sale( 'edit' ) ) { $product->set_price( $product->get_sale_price( 'edit' ) ); } else { $product->set_price( $product->get_regular_price( 'edit' ) ); } } // Product parent ID for groups if ( isset( $data['parent_id'] ) ) { $product->set_parent_id( absint( $data['parent_id'] ) ); } // Sold Individually if ( isset( $data['sold_individually'] ) ) { $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); } // Stock status if ( isset( $data['in_stock'] ) ) { $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; } else { $stock_status = $product->get_stock_status(); if ( '' === $stock_status ) { $stock_status = 'instock'; } } // Stock Data if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { // Manage stock if ( isset( $data['managing_stock'] ) ) { $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; $product->set_manage_stock( $managing_stock ); } else { $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; } // Backorders if ( isset( $data['backorders'] ) ) { if ( 'notify' == $data['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; } $product->set_backorders( $backorders ); } else { $backorders = $product->get_backorders(); } if ( $product->is_type( 'grouped' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } elseif ( $product->is_type( 'external' ) ) { $product->set_manage_stock( 'no' ); $product->set_backorders( 'no' ); $product->set_stock_quantity( '' ); $product->set_stock_status( 'instock' ); } elseif ( 'yes' == $managing_stock ) { $product->set_backorders( $backorders ); // Stock status is always determined by children so sync later. if ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Stock quantity if ( isset( $data['stock_quantity'] ) ) { $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); } } else { // Don't manage stock. $product->set_manage_stock( 'no' ); $product->set_backorders( $backorders ); $product->set_stock_quantity( '' ); $product->set_stock_status( $stock_status ); } } elseif ( ! $product->is_type( 'variable' ) ) { $product->set_stock_status( $stock_status ); } // Upsells if ( isset( $data['upsell_ids'] ) ) { $upsells = array(); $ids = $data['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } $product->set_upsell_ids( $upsells ); } else { $product->set_upsell_ids( array() ); } } // Cross sells if ( isset( $data['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $data['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } $product->set_cross_sell_ids( $crosssells ); } else { $product->set_cross_sell_ids( array() ); } } // Product categories if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { $product->set_category_ids( $data['categories'] ); } // Product tags if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { $product->set_tag_ids( $data['tags'] ); } // Downloadable if ( isset( $data['downloadable'] ) ) { $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; $product->set_downloadable( $is_downloadable ); } else { $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; } // Downloadable options if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $product = $this->save_downloadable_files( $product, $data['downloads'] ); } // Download limit if ( isset( $data['download_limit'] ) ) { $product->set_download_limit( $data['download_limit'] ); } // Download expiry if ( isset( $data['download_expiry'] ) ) { $product->set_download_expiry( $data['download_expiry'] ); } } // Product url if ( $product->is_type( 'external' ) ) { if ( isset( $data['product_url'] ) ) { $product->set_product_url( $data['product_url'] ); } if ( isset( $data['button_text'] ) ) { $product->set_button_text( $data['button_text'] ); } } // Reviews allowed if ( isset( $data['reviews_allowed'] ) ) { $product->set_reviews_allowed( $data['reviews_allowed'] ); } // Save default attributes for variable products. if ( $product->is_type( 'variable' ) ) { $product = $this->save_default_attributes( $product, $data ); } // Do action for product type do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); return $product; } /** * Save variations * * @since 2.2 * @param WC_Product $product * @param array $request * * @return true * * @throws WC_API_Exception */ protected function save_variations( $product, $request ) { global $wpdb; $id = $product->get_id(); $attributes = $product->get_attributes(); foreach ( $request['variations'] as $menu_order => $data ) { $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; $variation = new WC_Product_Variation( $variation_id ); // Create initial name and status. if ( ! $variation->get_slug() ) { /* translators: 1: variation id 2: product name */ $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); } // Parent ID. $variation->set_parent_id( $product->get_id() ); // Menu order. $variation->set_menu_order( $menu_order ); // Status. if ( isset( $data['visible'] ) ) { $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); } // SKU. if ( isset( $data['sku'] ) ) { $variation->set_sku( wc_clean( $data['sku'] ) ); } // Thumbnail. if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { $image = current( $data['image'] ); if ( is_array( $image ) ) { $image['position'] = 0; } $variation = $this->save_product_images( $variation, array( $image ) ); } // Virtual variation. if ( isset( $data['virtual'] ) ) { $variation->set_virtual( $data['virtual'] ); } // Downloadable variation. if ( isset( $data['downloadable'] ) ) { $is_downloadable = $data['downloadable']; $variation->set_downloadable( $is_downloadable ); } else { $is_downloadable = $variation->get_downloadable(); } // Downloads. if ( $is_downloadable ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { $variation->set_download_limit( $data['download_limit'] ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { $variation->set_download_expiry( $data['download_expiry'] ); } } // Shipping data. $variation = $this->save_product_shipping_data( $variation, $data ); // Stock handling. $manage_stock = (bool) $variation->get_manage_stock(); if ( isset( $data['managing_stock'] ) ) { $manage_stock = $data['managing_stock']; } $variation->set_manage_stock( $manage_stock ); $stock_status = $variation->get_stock_status(); if ( isset( $data['in_stock'] ) ) { $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; } $variation->set_stock_status( $stock_status ); $backorders = $variation->get_backorders(); if ( isset( $data['backorders'] ) ) { $backorders = $data['backorders']; } $variation->set_backorders( $backorders ); if ( $manage_stock ) { if ( isset( $data['stock_quantity'] ) ) { $variation->set_stock_quantity( $data['stock_quantity'] ); } } else { $variation->set_backorders( 'no' ); $variation->set_stock_quantity( '' ); } // Regular Price. if ( isset( $data['regular_price'] ) ) { $variation->set_regular_price( $data['regular_price'] ); } // Sale Price. if ( isset( $data['sale_price'] ) ) { $variation->set_sale_price( $data['sale_price'] ); } if ( isset( $data['sale_price_dates_from'] ) ) { $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); } if ( isset( $data['sale_price_dates_to'] ) ) { $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); } // Tax class. if ( isset( $data['tax_class'] ) ) { $variation->set_tax_class( $data['tax_class'] ); } // Update taxonomies. if ( isset( $data['attributes'] ) ) { $_attributes = array(); foreach ( $data['attributes'] as $attribute_key => $attribute ) { if ( ! isset( $attribute['name'] ) ) { continue; } $taxonomy = 0; $_attribute = array(); if ( isset( $attribute['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); } if ( ! $taxonomy ) { $taxonomy = sanitize_title( $attribute['name'] ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; } if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { $_attribute_key = sanitize_title( $_attribute['name'] ); if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { // Don't use wc_clean as it destroys sanitized characters $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; } else { $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; } $_attributes[ $_attribute_key ] = $_attribute_value; } } $variation->set_attributes( $_attributes ); } $variation->save(); do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); } return true; } /** * Save product shipping data * * @since 2.2 * @param WC_Product $product * @param array $data * @return WC_Product */ private function save_product_shipping_data( $product, $data ) { if ( isset( $data['weight'] ) ) { $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); } // Product dimensions if ( isset( $data['dimensions'] ) ) { // Height if ( isset( $data['dimensions']['height'] ) ) { $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width if ( isset( $data['dimensions']['width'] ) ) { $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); } // Length if ( isset( $data['dimensions']['length'] ) ) { $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Virtual if ( isset( $data['virtual'] ) ) { $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; if ( 'yes' == $virtual ) { $product->set_weight( '' ); $product->set_height( '' ); $product->set_length( '' ); $product->set_width( '' ); } } // Shipping class if ( isset( $data['shipping_class'] ) ) { $data_store = $product->get_data_store(); $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } return $product; } /** * Save downloadable files * * @since 2.2 * @param WC_Product $product * @param array $downloads * @param int $deprecated Deprecated since 3.0. * @return WC_Product */ private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { if ( $deprecated ) { wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); } $files = array(); foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( empty( $file['file'] ) ) { continue; } $download = new WC_Product_Download(); $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); $files[] = $download; } $product->set_downloads( $files ); return $product; } /** * Get attribute taxonomy by slug. * * @since 2.2 * @param string $slug * @return string|null */ private function get_attribute_taxonomy_by_slug( $slug ) { $taxonomy = null; $attribute_taxonomies = wc_get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $key => $tax ) { if ( $slug == $tax->attribute_name ) { $taxonomy = 'pa_' . $tax->attribute_name; break; } } return $taxonomy; } /** * Get the images for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_images( $product ) { $images = $attachment_ids = array(); $product_image = $product->get_image_id(); // Add featured image. if ( ! empty( $product_image ) ) { $attachment_ids[] = $product_image; } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => (int) $position, ); } // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'created_at' => $this->server->format_datetime( time() ), // Default to now. 'updated_at' => $this->server->format_datetime( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Save product images * * @since 2.2 * * @param WC_Product $product * @param array $images * * @return WC_Product * @throws WC_API_Exception */ protected function save_product_images( $product, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { if ( isset( $image['position'] ) && 0 == $image['position'] ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); } $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); } $product->set_image_id( $attachment_id ); } else { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); } $gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() ); } else { $gallery[] = $attachment_id; } } } if ( ! empty( $gallery ) ) { $product->set_gallery_image_ids( $gallery ); } } else { $product->set_image_id( '' ); $product->set_gallery_image_ids( array() ); } return $product; } /** * Upload image from URL * * @since 2.2 * * @param string $image_url * * @return array * * @throws WC_API_Exception */ public function upload_product_image( $image_url ) { $upload = wc_rest_upload_image_from_url( $image_url ); if ( is_wp_error( $upload ) ) { throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 ); } return $upload; } /** * Sets product image as attachment and returns the attachment ID. * * @since 2.2 * @param array $upload * @param int $id * @return int */ protected function set_product_image_as_attachment( $upload, $id ) { $info = wp_check_filetype( $upload['file'] ); $title = ''; $content = ''; if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = wc_clean( $image_meta['title'] ); } if ( trim( $image_meta['caption'] ) ) { $content = wc_clean( $image_meta['caption'] ); } } $attachment = array( 'post_mime_type' => $info['type'], 'guid' => $upload['url'], 'post_parent' => $id, 'post_title' => $title, 'post_content' => $content, ); $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); if ( ! is_wp_error( $attachment_id ) ) { wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); } return $attachment_id; } /** * Get attribute options. * * @param int $product_id * @param array $attribute * @return array */ protected function get_attribute_options( $product_id, $attribute ) { if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); } elseif ( isset( $attribute['value'] ) ) { return array_map( 'trim', explode( '|', $attribute['value'] ) ); } return array(); } /** * Get the attributes for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // variation attributes foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` $attributes[] = array( 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => wc_attribute_label( $attribute['name'] ), 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } } return $attributes; } /** * Get the downloads for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_downloads() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // do not cast as int as this is a hash 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get a listing of product attributes * * @since 2.4.0 * * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_attributes( $fields = null ) { try { // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); } $product_attributes = array(); $attribute_taxonomies = wc_get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $attribute ) { $product_attributes[] = array( 'id' => intval( $attribute->attribute_id ), 'name' => $attribute->attribute_label, 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), 'type' => $attribute->attribute_type, 'order_by' => $attribute->attribute_orderby, 'has_archives' => (bool) $attribute->attribute_public, ); } return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get the product attribute for the given ID * * @since 2.4.0 * * @param string $id product attribute term ID * @param string|null $fields fields to limit response to * * @return array|WP_Error */ public function get_product_attribute( $id, $fields = null ) { global $wpdb; try { $id = absint( $id ); // Validate ID if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); } // Permissions check if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); } $attribute = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $product_attribute = array( 'id' => intval( $attribute->attribute_id ), 'name' => $attribute->attribute_label, 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), 'type' => $attribute->attribute_type, 'order_by' => $attribute->attribute_orderby, 'has_archives' => (bool) $attribute->attribute_public, ); return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Validate attribute data. * * @since 2.4.0 * @param string $name * @param string $slug * @param string $type * @param string $order_by * @param bool $new_data * @return bool * @throws WC_API_Exception */ protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { if ( empty( $name ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); } if ( strlen( $slug ) >= 28 ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); } // Validate the attribute type if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); } // Validate the attribute order by if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); } return true; } /** * Create a new product attribute * * @since 2.4.0 * * @param array $data posted data * * @return array|WP_Error */ public function create_product_attribute( $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); } $data = $data['product_attribute']; // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); if ( ! isset( $data['name'] ) ) { $data['name'] = ''; } // Set the attribute slug if ( ! isset( $data['slug'] ) ) { $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); } else { $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); } // Set attribute type when not sent if ( ! isset( $data['type'] ) ) { $data['type'] = 'select'; } // Set order by when not sent if ( ! isset( $data['order_by'] ) ) { $data['order_by'] = 'menu_order'; } // Validate the attribute data $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); $insert = $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_label' => $data['name'], 'attribute_name' => $data['slug'], 'attribute_type' => $data['type'], 'attribute_orderby' => $data['order_by'], 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, ), array( '%s', '%s', '%s', '%s', '%d' ) ); // Checks for an error in the product creation if ( is_wp_error( $insert ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); } $id = $wpdb->insert_id; do_action( 'woocommerce_api_create_product_attribute', $id, $data ); // Clear transients delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); $this->server->send_status( 201 ); return $this->get_product_attribute( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Edit a product attribute * * @since 2.4.0 * * @param int $id the attribute ID * @param array $data * * @return array|WP_Error */ public function edit_product_attribute( $id, $data ) { global $wpdb; try { if ( ! isset( $data['product_attribute'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); } $id = absint( $id ); $data = $data['product_attribute']; // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); } $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); $attribute = $this->get_product_attribute( $id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; if ( isset( $data['slug'] ) ) { $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); } else { $attribute_slug = $attribute['product_attribute']['slug']; } $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); if ( isset( $data['has_archives'] ) ) { $attribute_public = true === $data['has_archives'] ? 1 : 0; } else { $attribute_public = $attribute['product_attribute']['has_archives']; } // Validate the attribute data $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); $update = $wpdb->update( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_label' => $attribute_name, 'attribute_name' => $attribute_slug, 'attribute_type' => $attribute_type, 'attribute_orderby' => $attribute_order_by, 'attribute_public' => $attribute_public, ), array( 'attribute_id' => $id ), array( '%s', '%s', '%s', '%s', '%d' ), array( '%d' ) ); // Checks for an error in the product creation if ( false === $update ) { throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); } do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); // Clear transients delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return $this->get_product_attribute( $id ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Delete a product attribute * * @since 2.4.0 * * @param int $id the product attribute ID * * @return array|WP_Error */ public function delete_product_attribute( $id ) { global $wpdb; try { // Check permissions if ( ! current_user_can( 'manage_product_terms' ) ) { throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); } $id = absint( $id ); $attribute_name = $wpdb->get_var( $wpdb->prepare( " SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $id ) ); if ( is_null( $attribute_name ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); } $deleted = $wpdb->delete( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_id' => $id ), array( '%d' ) ); if ( false === $deleted ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); } $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); if ( taxonomy_exists( $taxonomy ) ) { $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); foreach ( $terms as $term ) { wp_delete_term( $term->term_id, $taxonomy ); } } do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); // Clear transients delete_transient( 'wc_attribute_taxonomies' ); WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Get product by SKU * * @deprecated 2.4.0 * * @since 2.3.0 * * @param int $sku the product SKU * @param string $fields * * @return array|WP_Error */ public function get_product_by_sku( $sku, $fields = null ) { try { $id = wc_get_product_id_by_sku( $sku ); if ( empty( $id ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 ); } return $this->get_product( $id, $fields ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Clear product * * @param int $product_id */ protected function clear_product( $product_id ) { if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { return; } // Delete product attachments $attachments = get_children( array( 'post_parent' => $product_id, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product $product = wc_get_product( $product_id ); $product->delete(); } /** * Bulk update or insert products * Accepts an array with products in the formats supported by * WC_API_Products->create_product() and WC_API_Products->edit_product() * * @since 2.4.0 * * @param array $data * * @return array|WP_Error */ public function bulk( $data ) { try { if ( ! isset( $data['products'] ) ) { throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); } $data = $data['products']; $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); // Limit bulk operation if ( count( $data ) > $limit ) { throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); } $products = array(); foreach ( $data as $_product ) { $product_id = 0; $product_sku = ''; // Try to get the product ID if ( isset( $_product['id'] ) ) { $product_id = intval( $_product['id'] ); } if ( ! $product_id && isset( $_product['sku'] ) ) { $product_sku = wc_clean( $_product['sku'] ); $product_id = wc_get_product_id_by_sku( $product_sku ); } if ( $product_id ) { // Product exists / edit product $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); if ( is_wp_error( $edit ) ) { $products[] = array( 'id' => $product_id, 'sku' => $product_sku, 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), ); } else { $products[] = $edit['product']; } } else { // Product don't exists / create product $new = $this->create_product( array( 'product' => $_product ) ); if ( is_wp_error( $new ) ) { $products[] = array( 'id' => $product_id, 'sku' => $product_sku, 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), ); } else { $products[] = $new['product']; } } } return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } includes/legacy/api/v2/interface-wc-api-handler.php 0000644 00000001515 15132754524 0016172 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } interface WC_API_Handler { /** * Get the content type for the response * * This should return the proper HTTP content-type for the response * * @since 2.1 * @return string */ public function get_content_type(); /** * Parse the raw request body entity into an array * * @since 2.1 * @param string $data * @return array */ public function parse_body( $data ); /** * Generate a response from an array of data * * @since 2.1 * @param array $data * @return string */ public function generate_response( $data ); } includes/legacy/api/v2/class-wc-api-authentication.php 0000644 00000027436 15132754524 0016753 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1.0 * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Authentication { /** * Setup class * * @since 2.1 */ public function __construct() { // To disable authentication, hook into this filter at a later priority and return a valid WP_User add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 ); } /** * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not. * * @since 2.1 * @param WP_User $user * @return null|WP_Error|WP_User */ public function authenticate( $user ) { // Allow access to the index by default if ( '/' === WC()->api->server->path ) { return new WP_User( 0 ); } try { if ( is_ssl() ) { $keys = $this->perform_ssl_authentication(); } else { $keys = $this->perform_oauth_authentication(); } // Check API key-specific permission $this->check_api_key_permissions( $keys['permissions'] ); $user = $this->get_user_by_id( $keys['user_id'] ); $this->update_api_key_last_access( $keys['key_id'] ); } catch ( Exception $e ) { $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); } return $user; } /** * SSL-encrypted requests are not subject to sniffing or man-in-the-middle * attacks, so the request can be authenticated by simply looking up the user * associated with the given consumer key and confirming the consumer secret * provided is valid * * @since 2.1 * @return array * @throws Exception */ private function perform_ssl_authentication() { $params = WC()->api->server->params['GET']; // Get consumer key if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) { // Should be in HTTP Auth header by default $consumer_key = $_SERVER['PHP_AUTH_USER']; } elseif ( ! empty( $params['consumer_key'] ) ) { // Allow a query string parameter as a fallback $consumer_key = $params['consumer_key']; } else { throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 ); } // Get consumer secret if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { // Should be in HTTP Auth header by default $consumer_secret = $_SERVER['PHP_AUTH_PW']; } elseif ( ! empty( $params['consumer_secret'] ) ) { // Allow a query string parameter as a fallback $consumer_secret = $params['consumer_secret']; } else { throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 ); } $keys = $this->get_keys_by_consumer_key( $consumer_key ); if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) { throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); } return $keys; } /** * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests * * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP * * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: * * 1) There is no token associated with request/responses, only consumer keys/secrets are used * * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, * This is because there is no cross-OS function within PHP to get the raw Authorization header * * @link http://tools.ietf.org/html/rfc5849 for the full spec * @since 2.1 * @return array * @throws Exception */ private function perform_oauth_authentication() { $params = WC()->api->server->params['GET']; $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); // Check for required OAuth parameters foreach ( $param_names as $param_name ) { if ( empty( $params[ $param_name ] ) ) { throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); } } // Fetch WP user by consumer key $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); // Perform OAuth validation $this->check_oauth_signature( $keys, $params ); $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); // Authentication successful, return user return $keys; } /** * Return the keys for the given consumer key * * @since 2.4.0 * @param string $consumer_key * @return array * @throws Exception */ private function get_keys_by_consumer_key( $consumer_key ) { global $wpdb; $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); $keys = $wpdb->get_row( $wpdb->prepare( " SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = '%s' ", $consumer_key ), ARRAY_A ); if ( empty( $keys ) ) { throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); } return $keys; } /** * Get user by ID * * @since 2.4.0 * @param int $user_id * @return WP_User * @throws Exception */ private function get_user_by_id( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user ) { throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); } return $user; } /** * Check if the consumer secret provided for the given user is valid * * @since 2.1 * @param string $keys_consumer_secret * @param string $consumer_secret * @return bool */ private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { return hash_equals( $keys_consumer_secret, $consumer_secret ); } /** * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer * has a valid key/secret * * @param array $keys * @param array $params the request parameters * @throws Exception */ private function check_oauth_signature( $keys, $params ) { $http_method = strtoupper( WC()->api->server->method ); $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path ); // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); unset( $params['oauth_signature'] ); // Remove filters and convert them from array to strings to void normalize issues if ( isset( $params['filter'] ) ) { $filters = $params['filter']; unset( $params['filter'] ); foreach ( $filters as $filter => $filter_value ) { $params[ 'filter[' . $filter . ']' ] = $filter_value; } } // Normalize parameter key/values $params = $this->normalize_parameters( $params ); // Sort parameters if ( ! uksort( $params, 'strcmp' ) ) { throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); } // Form query string $query_params = array(); foreach ( $params as $param_key => $param_value ) { $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign } $query_string = implode( '%26', $query_params ); // join with ampersand $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); } } /** * Normalize each parameter by assuming each parameter may have already been * encoded, so attempt to decode, and then re-encode according to RFC 3986 * * Note both the key and value is normalized so a filter param like: * * 'filter[period]' => 'week' * * is encoded to: * * 'filter%5Bperiod%5D' => 'week' * * This conforms to the OAuth 1.0a spec which indicates the entire query string * should be URL encoded * * @since 2.1 * @see rawurlencode() * @param array $parameters un-normalized parameters * @return array normalized parameters */ private function normalize_parameters( $parameters ) { $normalized_parameters = array(); foreach ( $parameters as $key => $value ) { // Percent symbols (%) must be double-encoded $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) ); $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); $normalized_parameters[ $key ] = $value; } return $normalized_parameters; } /** * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where * an attacker could attempt to re-send an intercepted request at a later time. * * - A timestamp is valid if it is within 15 minutes of now * - A nonce is valid if it has not been used within the last 15 minutes * * @param array $keys * @param int $timestamp the unix timestamp for when the request was made * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated * @throws Exception */ private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { global $wpdb; $valid_window = 15 * 60; // 15 minute window if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 ); } $used_nonces = maybe_unserialize( $keys['nonces'] ); if ( empty( $used_nonces ) ) { $used_nonces = array(); } if ( in_array( $nonce, $used_nonces ) ) { throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); } $used_nonces[ $timestamp ] = $nonce; // Remove expired nonces foreach ( $used_nonces as $nonce_timestamp => $nonce ) { if ( $nonce_timestamp < ( time() - $valid_window ) ) { unset( $used_nonces[ $nonce_timestamp ] ); } } $used_nonces = maybe_serialize( $used_nonces ); $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'nonces' => $used_nonces ), array( 'key_id' => $keys['key_id'] ), array( '%s' ), array( '%d' ) ); } /** * Check that the API keys provided have the proper key-specific permissions to either read or write API resources * * @param string $key_permissions * @throws Exception if the permission check fails */ public function check_api_key_permissions( $key_permissions ) { switch ( WC()->api->server->method ) { case 'HEAD': case 'GET': if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); } break; case 'POST': case 'PUT': case 'PATCH': case 'DELETE': if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); } break; } } /** * Updated API Key last access datetime * * @since 2.4.0 * * @param int $key_id */ private function update_api_key_last_access( $key_id ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'last_access' => current_time( 'mysql' ) ), array( 'key_id' => $key_id ), array( '%s' ), array( '%d' ) ); } } includes/legacy/api/v2/class-wc-api-resource.php 0000644 00000033103 15132754524 0015547 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Resource { /** @var WC_API_Server the API server */ protected $server; /** @var string sub-classes override this to set a resource-specific base route */ protected $base; /** * Setup class * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { $this->server = $server; // automatically register routes for sub-classes add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); // maybe add meta to top-level resource responses foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); } $response_names = array( 'order', 'coupon', 'customer', 'product', 'report', 'customer_orders', 'customer_downloads', 'order_note', 'order_refund', 'product_reviews', 'product_category', ); foreach ( $response_names as $name ) { /** * Remove fields from responses when requests specify certain fields * note these are hooked at a later priority so data added via * filters (e.g. customer data to the order response) still has the * fields filtered properly */ add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid post object and matches the provided post type * 3) the current user has the proper permissions to read/edit/delete the post * * @since 2.1 * @param string|int $id the post ID * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid post ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } $id = absint( $id ); // Validate ID if ( empty( $id ) ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); } // Only custom post types have per-post type/permission checks if ( 'customer' !== $type ) { $post = get_post( $id ); if ( null === $post ) { return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); } // For checking permissions, product variations are the same as the product post type $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; // Validate post type if ( $type !== $post_type ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); } // Validate permissions switch ( $context ) { case 'read': if ( ! $this->is_readable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! $this->is_editable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! $this->is_deletable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; } } return $id; } /** * Add common request arguments to argument list before WP_Query is run * * @since 2.1 * @param array $base_args required arguments for the query (e.g. `post_type`, etc) * @param array $request_args arguments provided in the request * @return array */ protected function merge_query_args( $base_args, $request_args ) { $args = array(); // date if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'] = array(); // resources created after specified date if ( ! empty( $request_args['created_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $request_args['created_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); } // resources updated after specified date if ( ! empty( $request_args['updated_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); } // resources updated before specified date if ( ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); } } // search if ( ! empty( $request_args['q'] ) ) { $args['s'] = $request_args['q']; } // resources per response if ( ! empty( $request_args['limit'] ) ) { $args['posts_per_page'] = $request_args['limit']; } // resource offset if ( ! empty( $request_args['offset'] ) ) { $args['offset'] = $request_args['offset']; } // order (ASC or DESC, ASC by default) if ( ! empty( $request_args['order'] ) ) { $args['order'] = $request_args['order']; } // orderby if ( ! empty( $request_args['orderby'] ) ) { $args['orderby'] = $request_args['orderby']; // allow sorting by meta value if ( ! empty( $request_args['orderby_meta_key'] ) ) { $args['meta_key'] = $request_args['orderby_meta_key']; } } // allow post status change if ( ! empty( $request_args['post_status'] ) ) { $args['post_status'] = $request_args['post_status']; unset( $request_args['post_status'] ); } // filter by a list of post id if ( ! empty( $request_args['in'] ) ) { $args['post__in'] = explode( ',', $request_args['in'] ); unset( $request_args['in'] ); } // filter by a list of post id if ( ! empty( $request_args['in'] ) ) { $args['post__in'] = explode( ',', $request_args['in'] ); unset( $request_args['in'] ); } // resource page $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); return array_merge( $base_args, $args ); } /** * Add meta to resources when requested by the client. Meta is added as a top-level * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs * * @since 2.1 * @param array $data the resource data * @param object $resource the resource object (e.g WC_Order) * @return mixed */ public function maybe_add_meta( $data, $resource ) { if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { // don't attempt to add meta more than once if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { return $data; } // define the top-level property name for the meta switch ( get_class( $resource ) ) { case 'WC_Order': $meta_name = 'order_meta'; break; case 'WC_Coupon': $meta_name = 'coupon_meta'; break; case 'WP_User': $meta_name = 'customer_meta'; break; default: $meta_name = 'product_meta'; break; } if ( is_a( $resource, 'WP_User' ) ) { // customer meta $meta = (array) get_user_meta( $resource->ID ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->get_id() ); } foreach ( $meta as $meta_key => $meta_value ) { // don't add hidden meta by default if ( ! is_protected_meta( $meta_key ) ) { $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); } } } return $data; } /** * Restrict the fields included in the response if the request specified certain only certain fields should be returned * * @since 2.1 * @param array $data the response data * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order * @param array|string the requested list of fields to include in the response * @return array response data */ public function filter_response_fields( $data, $resource, $fields ) { if ( ! is_array( $data ) || empty( $fields ) ) { return $data; } $fields = explode( ',', $fields ); $sub_fields = array(); // get sub fields foreach ( $fields as $field ) { if ( false !== strpos( $field, '.' ) ) { list( $name, $value ) = explode( '.', $field ); $sub_fields[ $name ] = $value; } } // iterate through top-level fields foreach ( $data as $data_field => $data_value ) { // if a field has sub-fields and the top-level field has sub-fields to filter if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { // iterate through each sub-field foreach ( $data_value as $sub_field => $sub_field_value ) { // remove non-matching sub-fields if ( ! in_array( $sub_field, $sub_fields ) ) { unset( $data[ $data_field ][ $sub_field ] ); } } } else { // remove non-matching top-level fields if ( ! in_array( $data_field, $fields ) ) { unset( $data[ $data_field ] ); } } } return $data; } /** * Delete a given resource * * @since 2.1 * @param int $id the resource ID * @param string $type the resource post type, or `customer` * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) * @return array|WP_Error */ protected function delete( $id, $type, $force = false ) { if ( 'shop_order' === $type || 'shop_coupon' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } if ( 'customer' === $type ) { $result = wp_delete_user( $id ); if ( $result ) { return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); } else { return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); } } else { // delete order/coupon/webhook $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); if ( ! $result ) { return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); } if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); } } } /** * Checks if the given post is readable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_readable( $post ) { return $this->check_permission( $post, 'read' ); } /** * Checks if the given post is editable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_editable( $post ) { return $this->check_permission( $post, 'edit' ); } /** * Checks if the given post is deletable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_deletable( $post ) { return $this->check_permission( $post, 'delete' ); } /** * Checks the permissions for the current user given a post and context * * @since 2.1 * @param WP_Post|int $post * @param string $context the type of permission to check, either `read`, `write`, or `delete` * @return bool true if the current user has the permissions to perform the context on the post */ private function check_permission( $post, $context ) { if ( ! is_a( $post, 'WP_Post' ) ) { $post = get_post( $post ); } if ( is_null( $post ) ) { return false; } $post_type = get_post_type_object( $post->post_type ); if ( 'read' === $context ) { return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ) ); } elseif ( 'edit' === $context ) { return current_user_can( $post_type->cap->edit_post, $post->ID ); } elseif ( 'delete' === $context ) { return current_user_can( $post_type->cap->delete_post, $post->ID ); } else { return false; } } } includes/legacy/api/class-wc-rest-legacy-coupons-controller.php 0000644 00000011710 15132754524 0020706 0 ustar 00 <?php /** * REST API Legacy Coupons controller * * Handles requests to the /coupons endpoint. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Legacy Coupons controller class. * * @package WooCommerce\RestApi * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller { /** * Query args. * * @deprecated 3.0.0 * * @param array $args Query args * @param WP_REST_Request $request Request data. * @return array */ public function query_args( $args, $request ) { if ( ! empty( $request['code'] ) ) { $id = wc_get_coupon_id_by_code( $request['code'] ); $args['post__in'] = array( $id ); } return $args; } /** * Prepare a single coupon output for response. * * @deprecated 3.0.0 * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $data */ public function prepare_item_for_response( $post, $request ) { $coupon = new WC_Coupon( (int) $post->ID ); $data = $coupon->get_data(); $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); $format_date = array( 'date_created', 'date_modified', 'date_expires' ); $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); // Format decimal values. foreach ( $format_decimal as $key ) { $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); } // Format date values. foreach ( $format_date as $key ) { $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null; } // Format null values. foreach ( $format_null as $key ) { $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $post, $request ) ); /** * Filter the data for a response. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for the response. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** * Prepare a single coupon for create or update. * * @deprecated 3.0.0 * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { global $wpdb; $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $coupon = new WC_Coupon( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Validate required POST fields. if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { if ( empty( $request['code'] ) ) { return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); } } // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; if ( ! is_null( $value ) ) { switch ( $key ) { case 'code' : $coupon_code = wc_format_coupon_code( $value ); $id = $coupon->get_id() ? $coupon->get_id() : 0; $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); } $coupon->set_code( $coupon_code ); break; case 'meta_data' : if ( is_array( $value ) ) { foreach ( $value as $meta ) { $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; case 'description' : $coupon->set_description( wp_filter_post_kses( $value ) ); break; default : if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { $coupon->{"set_{$key}"}( $value ); } break; } } } /** * Filter the query_vars used in `get_items` for the constructed query. * * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being * prepared for insertion. * * @param WC_Coupon $coupon The coupon object. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); } } includes/legacy/api/v1/class-wc-api-reports.php 0000644 00000032372 15132754524 0015424 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Reports extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/reports'; /** @var WC_Admin_Report instance */ private $report; /** * Register the routes for this class * * GET /reports * GET /reports/sales * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /reports $routes[ $this->base ] = array( array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), ); # GET /reports/sales $routes[ $this->base . '/sales' ] = array( array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), ); # GET /reports/sales/top_sellers $routes[ $this->base . '/sales/top_sellers' ] = array( array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get a simple listing of available reports * * @since 2.1 * @return array */ public function get_reports() { return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); } /** * Get the sales report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_sales_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); // total sales, taxes, shipping, and order count $totals = $this->report->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'sales', ), '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'tax', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'shipping_tax', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'shipping', ), 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'order_count', ), ), 'filter_range' => true, ) ); // total items ordered $total_items = absint( $this->report->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'query_type' => 'get_var', 'filter_range' => true, ) ) ); // total discount used $total_discount = $this->report->get_order_report_data( array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, ) ); // new customers $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), 'role' => 'customer', ) ); $customers = $users_query->get_results(); foreach ( $customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { unset( $customers[ $key ] ); } } $total_customers = count( $customers ); // get order totals grouped by period $orders = $this->report->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping', ), '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_tax', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping_tax', ), 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', 'distinct' => true, ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => $this->report->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); // get order item totals grouped by period $order_items = $this->report->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'line_item', 'operator' => '=', ), ), 'group_by' => $this->report->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); // get discount totals grouped by period $discounts = $this->report->get_order_report_data( array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->report->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); $period_totals = array(); // setup period totals by ensuring each period in the interval has data for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { switch ( $this->report->chart_groupby ) { case 'day' : $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); break; case 'month' : $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); break; } // set the customer signups for each period $customer_count = 0; foreach ( $customers as $customer ) { if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { $customer_count++; } } $period_totals[ $time ] = array( 'sales' => wc_format_decimal( 0.00, 2 ), 'orders' => 0, 'items' => 0, 'tax' => wc_format_decimal( 0.00, 2 ), 'shipping' => wc_format_decimal( 0.00, 2 ), 'discount' => wc_format_decimal( 0.00, 2 ), 'customers' => $customer_count, ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $orders as $order ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); $period_totals[ $time ]['orders'] = (int) $order->total_orders; $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); } // add total order items for each period foreach ( $order_items as $order_item ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // add total discount for each period foreach ( $discounts as $discount ) { $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } $sales_data = array( 'total_sales' => wc_format_decimal( $totals->sales, 2 ), 'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ), 'total_orders' => (int) $totals->order_count, 'total_items' => $total_items, 'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ), 'total_shipping' => wc_format_decimal( $totals->shipping, 2 ), 'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ), 'totals_grouped_by' => $this->report->chart_groupby, 'totals' => $period_totals, 'total_customers' => $total_customers, ); return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); } /** * Get the top sellers report * * @since 2.1 * @param string $fields fields to include in response * @param array $filter date filtering * @return array|WP_Error */ public function get_top_sellers_report( $fields = null, $filter = array() ) { // check user permissions $check = $this->validate_request(); if ( is_wp_error( $check ) ) { return $check; } // set date filtering $this->setup_report( $filter ); $top_sellers = $this->report->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); $top_sellers_data = array(); foreach ( $top_sellers as $top_seller ) { $product = wc_get_product( $top_seller->product_id ); $top_sellers_data[] = array( 'title' => $product->get_name(), 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->order_item_qty, ); } return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); } /** * Setup the report object and parse any date filtering * * @since 2.1 * @param array $filter date filtering */ private function setup_report( $filter ) { include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); $this->report = new WC_Admin_Report(); if ( empty( $filter['period'] ) ) { // custom date range $filter['period'] = 'custom'; if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; } else { // default custom range to today $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { // ensure period is valid if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { $filter['period'] = 'week'; } // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods // allow "week" for period instead of "7day" if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; } } $this->report->calculate_current_range( $filter['period'] ); } /** * Verify that the current user has permission to view reports * * @since 2.1 * @see WC_API_Resource::validate_request() * @param null $id unused * @param null $type unused * @param null $context unused * @return true|WP_Error */ protected function validate_request( $id = null, $type = null, $context = null ) { if ( ! current_user_can( 'view_woocommerce_reports' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); } else { return true; } } } includes/legacy/api/v1/interface-wc-api-handler.php 0000644 00000001541 15132754524 0016170 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } interface WC_API_Handler { /** * Get the content type for the response * * This should return the proper HTTP content-type for the response * * @since 2.1 * @return string */ public function get_content_type(); /** * Parse the raw request body entity into an array * * @since 2.1 * @param string $data * @return array */ public function parse_body( $data ); /** * Generate a response from an array of data * * @since 2.1 * @param array $data * @return string */ public function generate_response( $data ); } includes/legacy/api/v1/class-wc-api-customers.php 0000644 00000033130 15132754524 0015743 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Customers extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/customers'; /** @var string $created_at_min for date filtering */ private $created_at_min = null; /** @var string $created_at_max for date filtering */ private $created_at_max = null; /** * Setup class, overridden to provide customer data to order response * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { parent::__construct( $server ); // add customer data to order responses add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 ); // modify WP_User_Query to support created_at date filtering add_action( 'pre_user_query', array( $this, 'modify_user_query' ) ); } /** * Register the routes for this class * * GET /customers * GET /customers/count * GET /customers/<id> * GET /customers/<id>/orders * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /customers $routes[ $this->base ] = array( array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), ); # GET /customers/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), ); # GET /customers/<id>/orders $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array( array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), ); return $routes; } /** * Get all customers * * @since 2.1 * @param array $fields * @param array $filter * @param int $page * @return array */ public function get_customers( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_customers( $filter ); $customers = array(); foreach ( $query->get_results() as $user_id ) { if ( ! $this->is_readable( $user_id ) ) { continue; } $customers[] = current( $this->get_customer( $user_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'customers' => $customers ); } /** * Get the customer for the given ID * * @since 2.1 * @param int $id the customer ID * @param string $fields * @return array|WP_Error */ public function get_customer( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $customer = new WC_Customer( $id ); $last_order = $customer->get_last_order(); $customer_data = array( 'id' => $customer->get_id(), 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'email' => $customer->get_email(), 'first_name' => $customer->get_first_name(), 'last_name' => $customer->get_last_name(), 'username' => $customer->get_username(), 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. 'orders_count' => $customer->get_order_count(), 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), 'avatar_url' => $customer->get_avatar_url(), 'billing_address' => array( 'first_name' => $customer->get_billing_first_name(), 'last_name' => $customer->get_billing_last_name(), 'company' => $customer->get_billing_company(), 'address_1' => $customer->get_billing_address_1(), 'address_2' => $customer->get_billing_address_2(), 'city' => $customer->get_billing_city(), 'state' => $customer->get_billing_state(), 'postcode' => $customer->get_billing_postcode(), 'country' => $customer->get_billing_country(), 'email' => $customer->get_billing_email(), 'phone' => $customer->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $customer->get_shipping_first_name(), 'last_name' => $customer->get_shipping_last_name(), 'company' => $customer->get_shipping_company(), 'address_1' => $customer->get_shipping_address_1(), 'address_2' => $customer->get_shipping_address_2(), 'city' => $customer->get_shipping_city(), 'state' => $customer->get_shipping_state(), 'postcode' => $customer->get_shipping_postcode(), 'country' => $customer->get_shipping_country(), ), ); return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); } /** * Get the total number of customers * * @since 2.1 * @param array $filter * @return array|WP_Error */ public function get_customers_count( $filter = array() ) { $query = $this->query_customers( $filter ); if ( ! current_user_can( 'list_users' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) ); } return array( 'count' => count( $query->get_results() ) ); } /** * Create a customer * * @param array $data * @return array|WP_Error */ public function create_customer( $data ) { if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) ); } return array(); } /** * Edit a customer * * @param int $id the customer ID * @param array $data * @return array|WP_Error */ public function edit_customer( $id, $data ) { $id = $this->validate_request( $id, 'customer', 'edit' ); if ( ! is_wp_error( $id ) ) { return $id; } return $this->get_customer( $id ); } /** * Delete a customer * * @param int $id the customer ID * @return array|WP_Error */ public function delete_customer( $id ) { $id = $this->validate_request( $id, 'customer', 'delete' ); if ( ! is_wp_error( $id ) ) { return $id; } return $this->delete( $id, 'customer' ); } /** * Get the orders for a customer * * @since 2.1 * @param int $id the customer ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_customer_orders( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'customer', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $order_ids = wc_get_orders( array( 'customer' => $id, 'limit' => -1, 'orderby' => 'date', 'order' => 'ASC', 'return' => 'ids', ) ); if ( empty( $order_ids ) ) { return array( 'orders' => array() ); } $orders = array(); foreach ( $order_ids as $order_id ) { $orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) ); } return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) ); } /** * Helper method to get customer user objects * * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited * pagination support * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_User_Query */ private function query_customers( $args = array() ) { // default users per page $users_per_page = get_option( 'posts_per_page' ); // set base query arguments $query_args = array( 'fields' => 'ID', 'role' => 'customer', 'orderby' => 'registered', 'number' => $users_per_page, ); // search if ( ! empty( $args['q'] ) ) { $query_args['search'] = $args['q']; } // limit number of users returned if ( ! empty( $args['limit'] ) ) { $query_args['number'] = absint( $args['limit'] ); $users_per_page = absint( $args['limit'] ); } // page $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; // offset if ( ! empty( $args['offset'] ) ) { $query_args['offset'] = absint( $args['offset'] ); } else { $query_args['offset'] = $users_per_page * ( $page - 1 ); } // created date if ( ! empty( $args['created_at_min'] ) ) { $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); } if ( ! empty( $args['created_at_max'] ) ) { $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); } $query = new WP_User_Query( $query_args ); // helper members for pagination headers $query->total_pages = ceil( $query->get_total() / $users_per_page ); $query->page = $page; return $query; } /** * Add customer data to orders * * @since 2.1 * @param $order_data * @param $order * @return array */ public function add_customer_data( $order_data, $order ) { if ( 0 == $order->get_user_id() ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->get_billing_email(), 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); } return $order_data; } /** * Modify the WP_User_Query to support filtering on the date the customer was created * * @since 2.1 * @param WP_User_Query $query */ public function modify_user_query( $query ) { if ( $this->created_at_min ) { $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) ); } if ( $this->created_at_max ) { $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid WP_User * 3) the current user has the proper permissions * * @since 2.1 * @see WC_API_Resource::validate_request() * @param string|int $id the customer ID * @param string $type the request type, unused because this method overrides the parent class * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid user ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { $id = absint( $id ); // validate ID if ( empty( $id ) ) { return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) ); } // non-existent IDs return a valid WP_User object with the user ID = 0 $customer = new WP_User( $id ); if ( 0 === $customer->ID ) { return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) ); } // validate permissions switch ( $context ) { case 'read': if ( ! current_user_can( 'list_users' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! current_user_can( 'delete_users' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) ); } break; } return $id; } /** * Check if the current user can read users * * @since 2.1 * @see WC_API_Resource::is_readable() * @param int|WP_Post $post unused * @return bool true if the current user can read users, false otherwise */ protected function is_readable( $post ) { return current_user_can( 'list_users' ); } } includes/legacy/api/v1/class-wc-api-resource.php 0000644 00000027744 15132754524 0015564 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Resource { /** @var WC_API_Server the API server */ protected $server; /** @var string sub-classes override this to set a resource-specific base route */ protected $base; /** * Setup class * * @since 2.1 * @param WC_API_Server $server */ public function __construct( WC_API_Server $server ) { $this->server = $server; // automatically register routes for sub-classes add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); // remove fields from responses when requests specify certain fields // note these are hooked at a later priority so data added via filters (e.g. customer data to the order response) // still has the fields filtered properly foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 ); } } /** * Validate the request by checking: * * 1) the ID is a valid integer * 2) the ID returns a valid post object and matches the provided post type * 3) the current user has the proper permissions to read/edit/delete the post * * @since 2.1 * @param string|int $id the post ID * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` * @param string $context the context of the request, either `read`, `edit` or `delete` * @return int|WP_Error valid post ID or WP_Error if any of the checks fails */ protected function validate_request( $id, $type, $context ) { if ( 'shop_order' === $type || 'shop_coupon' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } $id = absint( $id ); // validate ID if ( empty( $id ) ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); } // only custom post types have per-post type/permission checks if ( 'customer' !== $type ) { $post = get_post( $id ); // for checking permissions, product variations are the same as the product post type $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; // validate post type if ( $type !== $post_type ) { return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); } // validate permissions switch ( $context ) { case 'read': if ( ! $this->is_readable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'edit': if ( ! $this->is_editable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; case 'delete': if ( ! $this->is_deletable( $post ) ) { return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); } break; } } return $id; } /** * Add common request arguments to argument list before WP_Query is run * * @since 2.1 * @param array $base_args required arguments for the query (e.g. `post_type`, etc) * @param array $request_args arguments provided in the request * @return array */ protected function merge_query_args( $base_args, $request_args ) { $args = array(); // date if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'] = array(); // resources created after specified date if ( ! empty( $request_args['created_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $request_args['created_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); } // resources updated after specified date if ( ! empty( $request_args['updated_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); } // resources updated before specified date if ( ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); } } // search if ( ! empty( $request_args['q'] ) ) { $args['s'] = $request_args['q']; } // resources per response if ( ! empty( $request_args['limit'] ) ) { $args['posts_per_page'] = $request_args['limit']; } // resource offset if ( ! empty( $request_args['offset'] ) ) { $args['offset'] = $request_args['offset']; } // resource page $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; return array_merge( $base_args, $args ); } /** * Add meta to resources when requested by the client. Meta is added as a top-level * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs * * @since 2.1 * @param array $data the resource data * @param object $resource the resource object (e.g WC_Order) * @return mixed */ public function maybe_add_meta( $data, $resource ) { if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { // don't attempt to add meta more than once if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { return $data; } // define the top-level property name for the meta switch ( get_class( $resource ) ) { case 'WC_Order': $meta_name = 'order_meta'; break; case 'WC_Coupon': $meta_name = 'coupon_meta'; break; case 'WP_User': $meta_name = 'customer_meta'; break; default: $meta_name = 'product_meta'; break; } if ( is_a( $resource, 'WP_User' ) ) { // customer meta $meta = (array) get_user_meta( $resource->ID ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->get_id() ); } foreach ( $meta as $meta_key => $meta_value ) { // don't add hidden meta by default if ( ! is_protected_meta( $meta_key ) ) { $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); } } } return $data; } /** * Restrict the fields included in the response if the request specified certain only certain fields should be returned * * @since 2.1 * @param array $data the response data * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order * @param array|string the requested list of fields to include in the response * @return array response data */ public function filter_response_fields( $data, $resource, $fields ) { if ( ! is_array( $data ) || empty( $fields ) ) { return $data; } $fields = explode( ',', $fields ); $sub_fields = array(); // get sub fields foreach ( $fields as $field ) { if ( false !== strpos( $field, '.' ) ) { list( $name, $value ) = explode( '.', $field ); $sub_fields[ $name ] = $value; } } // iterate through top-level fields foreach ( $data as $data_field => $data_value ) { // if a field has sub-fields and the top-level field has sub-fields to filter if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { // iterate through each sub-field foreach ( $data_value as $sub_field => $sub_field_value ) { // remove non-matching sub-fields if ( ! in_array( $sub_field, $sub_fields ) ) { unset( $data[ $data_field ][ $sub_field ] ); } } } else { // remove non-matching top-level fields if ( ! in_array( $data_field, $fields ) ) { unset( $data[ $data_field ] ); } } } return $data; } /** * Delete a given resource * * @since 2.1 * @param int $id the resource ID * @param string $type the resource post type, or `customer` * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) * @return array|WP_Error */ protected function delete( $id, $type, $force = false ) { if ( 'shop_order' === $type || 'shop_coupon' === $type ) { $resource_name = str_replace( 'shop_', '', $type ); } else { $resource_name = $type; } if ( 'customer' === $type ) { $result = wp_delete_user( $id ); if ( $result ) { return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); } else { return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); } } else { // delete order/coupon/product $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); if ( ! $result ) { return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); } if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); } } } /** * Checks if the given post is readable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_readable( $post ) { return $this->check_permission( $post, 'read' ); } /** * Checks if the given post is editable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_editable( $post ) { return $this->check_permission( $post, 'edit' ); } /** * Checks if the given post is deletable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_deletable( $post ) { return $this->check_permission( $post, 'delete' ); } /** * Checks the permissions for the current user given a post and context * * @since 2.1 * @param WP_Post|int $post * @param string $context the type of permission to check, either `read`, `write`, or `delete` * @return bool true if the current user has the permissions to perform the context on the post */ private function check_permission( $post, $context ) { if ( ! is_a( $post, 'WP_Post' ) ) { $post = get_post( $post ); } if ( is_null( $post ) ) { return false; } $post_type = get_post_type_object( $post->post_type ); if ( 'read' === $context ) { return current_user_can( $post_type->cap->read_private_posts, $post->ID ); } elseif ( 'edit' === $context ) { return current_user_can( $post_type->cap->edit_post, $post->ID ); } elseif ( 'delete' === $context ) { return current_user_can( $post_type->cap->delete_post, $post->ID ); } else { return false; } } } includes/legacy/api/v1/class-wc-api-json-handler.php 0000644 00000003702 15132754524 0016305 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_JSON_Handler implements WC_API_Handler { /** * Get the content type for the response * * @since 2.1 * @return string */ public function get_content_type() { return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) ); } /** * Parse the raw request body entity * * @since 2.1 * @param string $body the raw request body * @return array|mixed */ public function parse_body( $body ) { return json_decode( $body, true ); } /** * Generate a JSON response given an array of data * * @since 2.1 * @param array $data the response data * @return string */ public function generate_response( $data ) { if ( isset( $_GET['_jsonp'] ) ) { if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); } $jsonp_callback = $_GET['_jsonp']; if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { WC()->api->server->send_status( 400 ); return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); } WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks. // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; } return wp_json_encode( $data ); } } includes/legacy/api/v1/class-wc-api-authentication.php 0000644 00000027507 15132754524 0016751 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1.0 * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Authentication { /** * Setup class * * @since 2.1 */ public function __construct() { // To disable authentication, hook into this filter at a later priority and return a valid WP_User add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 ); } /** * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not. * * @since 2.1 * @param WP_User $user * @return null|WP_Error|WP_User */ public function authenticate( $user ) { // Allow access to the index by default if ( '/' === WC()->api->server->path ) { return new WP_User( 0 ); } try { if ( is_ssl() ) { $keys = $this->perform_ssl_authentication(); } else { $keys = $this->perform_oauth_authentication(); } // Check API key-specific permission $this->check_api_key_permissions( $keys['permissions'] ); $user = $this->get_user_by_id( $keys['user_id'] ); $this->update_api_key_last_access( $keys['key_id'] ); } catch ( Exception $e ) { $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); } return $user; } /** * SSL-encrypted requests are not subject to sniffing or man-in-the-middle * attacks, so the request can be authenticated by simply looking up the user * associated with the given consumer key and confirming the consumer secret * provided is valid * * @since 2.1 * @return array * @throws Exception */ private function perform_ssl_authentication() { $params = WC()->api->server->params['GET']; // Get consumer key if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) { // Should be in HTTP Auth header by default $consumer_key = $_SERVER['PHP_AUTH_USER']; } elseif ( ! empty( $params['consumer_key'] ) ) { // Allow a query string parameter as a fallback $consumer_key = $params['consumer_key']; } else { throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 ); } // Get consumer secret if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { // Should be in HTTP Auth header by default $consumer_secret = $_SERVER['PHP_AUTH_PW']; } elseif ( ! empty( $params['consumer_secret'] ) ) { // Allow a query string parameter as a fallback $consumer_secret = $params['consumer_secret']; } else { throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 ); } $keys = $this->get_keys_by_consumer_key( $consumer_key ); if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) { throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); } return $keys; } /** * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests * * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP * * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: * * 1) There is no token associated with request/responses, only consumer keys/secrets are used * * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, * This is because there is no cross-OS function within PHP to get the raw Authorization header * * @link http://tools.ietf.org/html/rfc5849 for the full spec * @since 2.1 * @return array * @throws Exception */ private function perform_oauth_authentication() { $params = WC()->api->server->params['GET']; $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); // Check for required OAuth parameters foreach ( $param_names as $param_name ) { if ( empty( $params[ $param_name ] ) ) { /* translators: %s: parameter name */ throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); } } // Fetch WP user by consumer key $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); // Perform OAuth validation $this->check_oauth_signature( $keys, $params ); $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); // Authentication successful, return user return $keys; } /** * Return the keys for the given consumer key * * @since 2.4.0 * @param string $consumer_key * @return array * @throws Exception */ private function get_keys_by_consumer_key( $consumer_key ) { global $wpdb; $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); $keys = $wpdb->get_row( $wpdb->prepare( " SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = '%s' ", $consumer_key ), ARRAY_A ); if ( empty( $keys ) ) { throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); } return $keys; } /** * Get user by ID * * @since 2.4.0 * @param int $user_id * @return WP_User * * @throws Exception */ private function get_user_by_id( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user ) { throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); } return $user; } /** * Check if the consumer secret provided for the given user is valid * * @since 2.1 * @param string $keys_consumer_secret * @param string $consumer_secret * @return bool */ private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { return hash_equals( $keys_consumer_secret, $consumer_secret ); } /** * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer * has a valid key/secret * * @param array $keys * @param array $params the request parameters * @throws Exception */ private function check_oauth_signature( $keys, $params ) { $http_method = strtoupper( WC()->api->server->method ); $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path ); // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); unset( $params['oauth_signature'] ); // Remove filters and convert them from array to strings to void normalize issues if ( isset( $params['filter'] ) ) { $filters = $params['filter']; unset( $params['filter'] ); foreach ( $filters as $filter => $filter_value ) { $params[ 'filter[' . $filter . ']' ] = $filter_value; } } // Normalize parameter key/values $params = $this->normalize_parameters( $params ); // Sort parameters if ( ! uksort( $params, 'strcmp' ) ) { throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); } // Form query string $query_params = array(); foreach ( $params as $param_key => $param_value ) { $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign } $query_string = implode( '%26', $query_params ); // join with ampersand $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); } } /** * Normalize each parameter by assuming each parameter may have already been * encoded, so attempt to decode, and then re-encode according to RFC 3986 * * Note both the key and value is normalized so a filter param like: * * 'filter[period]' => 'week' * * is encoded to: * * 'filter%5Bperiod%5D' => 'week' * * This conforms to the OAuth 1.0a spec which indicates the entire query string * should be URL encoded * * @since 2.1 * @see rawurlencode() * @param array $parameters un-normalized parameters * @return array normalized parameters */ private function normalize_parameters( $parameters ) { $normalized_parameters = array(); foreach ( $parameters as $key => $value ) { // Percent symbols (%) must be double-encoded $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) ); $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); $normalized_parameters[ $key ] = $value; } return $normalized_parameters; } /** * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where * an attacker could attempt to re-send an intercepted request at a later time. * * - A timestamp is valid if it is within 15 minutes of now * - A nonce is valid if it has not been used within the last 15 minutes * * @param array $keys * @param int $timestamp the unix timestamp for when the request was made * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated * @throws Exception */ private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { global $wpdb; $valid_window = 15 * 60; // 15 minute window if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) ); } $used_nonces = maybe_unserialize( $keys['nonces'] ); if ( empty( $used_nonces ) ) { $used_nonces = array(); } if ( in_array( $nonce, $used_nonces ) ) { throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); } $used_nonces[ $timestamp ] = $nonce; // Remove expired nonces foreach ( $used_nonces as $nonce_timestamp => $nonce ) { if ( $nonce_timestamp < ( time() - $valid_window ) ) { unset( $used_nonces[ $nonce_timestamp ] ); } } $used_nonces = maybe_serialize( $used_nonces ); $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'nonces' => $used_nonces ), array( 'key_id' => $keys['key_id'] ), array( '%s' ), array( '%d' ) ); } /** * Check that the API keys provided have the proper key-specific permissions to either read or write API resources * * @param string $key_permissions * @throws Exception if the permission check fails */ public function check_api_key_permissions( $key_permissions ) { switch ( WC()->api->server->method ) { case 'HEAD': case 'GET': if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); } break; case 'POST': case 'PUT': case 'PATCH': case 'DELETE': if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); } break; } } /** * Updated API Key last access datetime * * @since 2.4.0 * * @param int $key_id */ private function update_api_key_last_access( $key_id ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'last_access' => current_time( 'mysql' ) ), array( 'key_id' => $key_id ), array( '%s' ), array( '%d' ) ); } } includes/legacy/api/v1/class-wc-api-xml-handler.php 0000644 00000016707 15132754524 0016145 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing XML request bodies and generating XML responses * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_XML_Handler implements WC_API_Handler { /** @var XMLWriter instance */ private $xml; /** * Add some response filters * * @since 2.1 */ public function __construct() { // tweak sales report response data add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 ); // tweak product response data add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 ); } /** * Get the content type for the response * * @since 2.1 * @return string */ public function get_content_type() { return 'application/xml; charset=' . get_option( 'blog_charset' ); } /** * Parse the raw request body entity * * @since 2.1 * @param string $data the raw request body * @return array */ public function parse_body( $data ) { // TODO: implement simpleXML parsing } /** * Generate an XML response given an array of data * * @since 2.1 * @param array $data the response data * @return string */ public function generate_response( $data ) { $this->xml = new XMLWriter(); $this->xml->openMemory(); $this->xml->setIndent( true ); $this->xml->startDocument( '1.0', 'UTF-8' ); $root_element = key( $data ); $data = $data[ $root_element ]; switch ( $root_element ) { case 'orders': $data = array( 'order' => $data ); break; case 'order_notes': $data = array( 'order_note' => $data ); break; case 'customers': $data = array( 'customer' => $data ); break; case 'coupons': $data = array( 'coupon' => $data ); break; case 'products': $data = array( 'product' => $data ); break; case 'product_reviews': $data = array( 'product_review' => $data ); break; default: $data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element ); break; } // generate xml starting with the root element and recursively generating child elements $this->array_to_xml( $root_element, $data ); $this->xml->endDocument(); return $this->xml->outputMemory(); } /** * Convert array into XML by recursively generating child elements * * @since 2.1 * @param string|array $element_key - name for element, e.g. <OrderID> * @param string|array $element_value - value for element, e.g. 1234 * @return string - generated XML */ private function array_to_xml( $element_key, $element_value = array() ) { if ( is_array( $element_value ) ) { // handle attributes if ( '@attributes' === $element_key ) { foreach ( $element_value as $attribute_key => $attribute_value ) { $this->xml->startAttribute( $attribute_key ); $this->xml->text( $attribute_value ); $this->xml->endAttribute(); } return; } // handle multi-elements (e.g. multiple <Order> elements) if ( is_numeric( key( $element_value ) ) ) { // recursively generate child elements foreach ( $element_value as $child_element_key => $child_element_value ) { $this->xml->startElement( $element_key ); foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) { $this->array_to_xml( $sibling_element_key, $sibling_element_value ); } $this->xml->endElement(); } } else { // start root element $this->xml->startElement( $element_key ); // recursively generate child elements foreach ( $element_value as $child_element_key => $child_element_value ) { $this->array_to_xml( $child_element_key, $child_element_value ); } // end root element $this->xml->endElement(); } } else { // handle single elements if ( '@value' == $element_key ) { $this->xml->text( $element_value ); } else { // wrap element in CDATA tags if it contains illegal characters if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) { $this->xml->startElement( $element_key ); $this->xml->writeCdata( $element_value ); $this->xml->endElement(); } else { $this->xml->writeElement( $element_key, $element_value ); } } return; } } /** * Adjust the sales report array format to change totals keyed with the sales date to become an * attribute for the totals element instead * * @since 2.1 * @param array $data * @return array */ public function format_sales_report_data( $data ) { if ( ! empty( $data['totals'] ) ) { foreach ( $data['totals'] as $date => $totals ) { unset( $data['totals'][ $date ] ); $data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals ); } } return $data; } /** * Adjust the product data to handle options for attributes without a named child element and other * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) ) * * Note that the parent product data for variations is also adjusted in the same manner as needed * * @since 2.1 * @param array $data * @return array */ public function format_product_data( $data ) { // handle attribute values if ( ! empty( $data['attributes'] ) ) { foreach ( $data['attributes'] as $attribute_key => $attribute ) { if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { foreach ( $attribute['options'] as $option_key => $option ) { unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] ); $data['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); } } } } // simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data // array to define a child element name for each field $fields_to_fix = array( 'related_ids' => 'related_id', 'upsell_ids' => 'upsell_id', 'cross_sell_ids' => 'cross_sell_id', 'categories' => 'category', 'tags' => 'tag', ); foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { if ( ! empty( $data[ $parent_field_name ] ) ) { foreach ( $data[ $parent_field_name ] as $field_key => $field ) { unset( $data[ $parent_field_name ][ $field_key ] ); $data[ $parent_field_name ][ $child_field_name ][] = array( $field ); } } } // handle adjusting the parent product for variations if ( ! empty( $data['parent'] ) ) { // attributes if ( ! empty( $data['parent']['attributes'] ) ) { foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) { if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { foreach ( $attribute['options'] as $option_key => $option ) { unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] ); $data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); } } } } // fields foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { if ( ! empty( $data['parent'][ $parent_field_name ] ) ) { foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) { unset( $data['parent'][ $parent_field_name ][ $field_key ] ); $data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field ); } } } } return $data; } } includes/legacy/api/v1/class-wc-api-products.php 0000644 00000041105 15132754524 0015563 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Products extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/products'; /** * Register the routes for this class * * GET /products * GET /products/count * GET /products/<id> * GET /products/<id>/reviews * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /products $routes[ $this->base ] = array( array( array( $this, 'get_products' ), WC_API_Server::READABLE ), ); # GET /products/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), ); # GET /products/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_product' ), WC_API_Server::READABLE ), ); # GET /products/<id>/reviews $routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array( array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get all products * * @since 2.1 * @param string $fields * @param string $type * @param array $filter * @param int $page * @return array */ public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { if ( ! empty( $type ) ) { $filter['type'] = $type; } $filter['page'] = $page; $query = $this->query_products( $filter ); $products = array(); foreach ( $query->posts as $product_id ) { if ( ! $this->is_readable( $product_id ) ) { continue; } $products[] = current( $this->get_product( $product_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'products' => $products ); } /** * Get the product for the given ID * * @since 2.1 * @param int $id the product ID * @param string $fields * @return array|WP_Error */ public function get_product( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $product = wc_get_product( $id ); // add data that applies to every product type $product_data = $this->get_product_data( $product ); // add variations to variable products if ( $product->is_type( 'variable' ) && $product->has_child() ) { $product_data['variations'] = $this->get_variation_data( $product ); } // add the parent product data to an individual variation if ( $product->is_type( 'variation' ) ) { $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); } return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); } /** * Get the total number of orders * * @since 2.1 * * @param string $type * @param array $filter * * @return array|WP_Error */ public function get_products_count( $type = null, $filter = array() ) { if ( ! empty( $type ) ) { $filter['type'] = $type; } if ( ! current_user_can( 'read_private_products' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) ); } $query = $this->query_products( $filter ); return array( 'count' => (int) $query->found_posts ); } /** * Edit a product * * @param int $id the product ID * @param array $data * @return array|WP_Error */ public function edit_product( $id, $data ) { $id = $this->validate_request( $id, 'product', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } return $this->get_product( $id ); } /** * Delete a product * * @param int $id the product ID * @param bool $force true to permanently delete order, false to move to trash * @return array|WP_Error */ public function delete_product( $id, $force = false ) { $id = $this->validate_request( $id, 'product', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } return $this->delete( $id, 'product', ( 'true' === $force ) ); } /** * Get the reviews for a product * * @since 2.1 * @param int $id the product ID to get reviews for * @param string $fields fields to include in response * @return array|WP_Error */ public function get_product_reviews( $id, $fields = null ) { $id = $this->validate_request( $id, 'product', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $args = array( 'post_id' => $id, 'approve' => 'approve', ); $comments = get_comments( $args ); $reviews = array(); foreach ( $comments as $comment ) { $reviews[] = array( 'id' => $comment->comment_ID, 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), 'review' => $comment->comment_content, 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'reviewer_name' => $comment->comment_author, 'reviewer_email' => $comment->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), ); } return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); } /** * Helper method to get product post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_products( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'product', 'post_status' => 'publish', 'meta_query' => array(), ); if ( ! empty( $args['type'] ) ) { $types = explode( ',', $args['type'] ); $query_args['tax_query'] = array( array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $types, ), ); unset( $args['type'] ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Get standard product data that applies to every product type * * @since 2.1 * @param WC_Product|int $product * @return array */ private function get_product_data( $product ) { if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } if ( ! is_a( $product, 'WC_Product' ) ) { return array(); } return array( 'title' => $product->get_name(), 'id' => $product->get_id(), 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), 'type' => $product->get_type(), 'status' => $product->get_status(), 'downloadable' => $product->is_downloadable(), 'virtual' => $product->is_virtual(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'price' => wc_format_decimal( $product->get_price(), 2 ), 'regular_price' => wc_format_decimal( $product->get_regular_price(), 2 ), 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null, 'price_html' => $product->get_price_html(), 'taxable' => $product->is_taxable(), 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'managing_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'purchaseable' => $product->is_purchasable(), 'featured' => $product->is_featured(), 'visible' => $product->is_visible(), 'catalog_visibility' => $product->get_catalog_visibility(), 'on_sale' => $product->is_on_sale(), 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, 'dimensions' => array( 'length' => $product->get_length(), 'width' => $product->get_width(), 'height' => $product->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, 'description' => apply_filters( 'the_content', $product->get_description() ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), 'reviews_allowed' => $product->get_reviews_allowed(), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), 'images' => $this->get_images( $product ), 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => $product->get_download_limit(), 'download_expiry' => $product->get_download_expiry(), 'download_type' => 'standard', 'purchase_note' => apply_filters( 'the_content', $product->get_purchase_note() ), 'total_sales' => $product->get_total_sales(), 'variations' => array(), 'parent' => array(), ); } /** * Get an individual variation's data * * @since 2.1 * @param WC_Product $product * @return array */ private function get_variation_data( $product ) { $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = wc_get_product( $child_id ); if ( ! $variation || ! $variation->exists() ) { continue; } $variations[] = array( 'id' => $variation->get_id(), 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), 'downloadable' => $variation->is_downloadable(), 'virtual' => $variation->is_virtual(), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => wc_format_decimal( $variation->get_price(), 2 ), 'regular_price' => wc_format_decimal( $variation->get_regular_price(), 2 ), 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null, 'taxable' => $variation->is_taxable(), 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'stock_quantity' => (int) $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backordered' => $variation->is_on_backorder(), 'purchaseable' => $variation->is_purchasable(), 'visible' => $variation->variation_is_visible(), 'on_sale' => $variation->is_on_sale(), 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null, 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => (int) $product->get_download_limit(), 'download_expiry' => (int) $product->get_download_expiry(), ); } return $variations; } /** * Get the images for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_images( $product ) { $images = $attachment_ids = array(); $product_image = $product->get_image_id(); // Add featured image. if ( ! empty( $product_image ) ) { $attachment_ids[] = $product_image; } // add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); // Build image data. foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => $position, ); } // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'created_at' => $this->server->format_datetime( time() ), // default to now 'updated_at' => $this->server->format_datetime( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Get attribute options. * * @param int $product_id * @param array $attribute * @return array */ protected function get_attribute_options( $product_id, $attribute ) { if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); } elseif ( isset( $attribute['value'] ) ) { return array_map( 'trim', explode( '|', $attribute['value'] ) ); } return array(); } /** * Get the attributes for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // variation attributes foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` $attributes[] = array( 'name' => ucwords( str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => ucwords( wc_attribute_taxonomy_slug( $attribute['name'] ) ), 'position' => $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), ); } } return $attributes; } /** * Get the downloads for a product or product variation * * @since 2.1 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_downloads() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // do not cast as int as this is a hash 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } } includes/legacy/api/v1/class-wc-api-server.php 0000644 00000051246 15132754524 0015235 0 ustar 00 <?php /** * WooCommerce API * * Handles REST API requests * * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API) * Many thanks to Ryan McCue and any other contributors! * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } require_once ABSPATH . 'wp-admin/includes/admin.php'; class WC_API_Server { const METHOD_GET = 1; const METHOD_POST = 2; const METHOD_PUT = 4; const METHOD_PATCH = 8; const METHOD_DELETE = 16; const READABLE = 1; // GET const CREATABLE = 2; // POST const EDITABLE = 14; // POST | PUT | PATCH const DELETABLE = 16; // DELETE const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE /** * Does the endpoint accept a raw request body? */ const ACCEPT_RAW_DATA = 64; /** Does the endpoint accept a request body? (either JSON or XML) */ const ACCEPT_DATA = 128; /** * Should we hide this endpoint from the index? */ const HIDDEN_ENDPOINT = 256; /** * Map of HTTP verbs to constants * @var array */ public static $method_map = array( 'HEAD' => self::METHOD_GET, 'GET' => self::METHOD_GET, 'POST' => self::METHOD_POST, 'PUT' => self::METHOD_PUT, 'PATCH' => self::METHOD_PATCH, 'DELETE' => self::METHOD_DELETE, ); /** * Requested path (relative to the API root, wp-json.php) * * @var string */ public $path = ''; /** * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) * * @var string */ public $method = 'HEAD'; /** * Request parameters * * This acts as an abstraction of the superglobals * (GET => $_GET, POST => $_POST) * * @var array */ public $params = array( 'GET' => array(), 'POST' => array() ); /** * Request headers * * @var array */ public $headers = array(); /** * Request files (matches $_FILES) * * @var array */ public $files = array(); /** * Request/Response handler, either JSON by default * or XML if requested by client * * @var WC_API_Handler */ public $handler; /** * Setup class and set request/response handler * * @since 2.1 * @param $path */ public function __construct( $path ) { if ( empty( $path ) ) { if ( isset( $_SERVER['PATH_INFO'] ) ) { $path = $_SERVER['PATH_INFO']; } else { $path = '/'; } } $this->path = $path; $this->method = $_SERVER['REQUEST_METHOD']; $this->params['GET'] = $_GET; $this->params['POST'] = $_POST; $this->headers = $this->get_headers( $_SERVER ); $this->files = $_FILES; // Compatibility for clients that can't use PUT/PATCH/DELETE if ( isset( $_GET['_method'] ) ) { $this->method = strtoupper( $_GET['_method'] ); } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } // determine type of request/response and load handler, JSON by default if ( $this->is_json_request() ) { $handler_class = 'WC_API_JSON_Handler'; } elseif ( $this->is_xml_request() ) { $handler_class = 'WC_API_XML_Handler'; } else { $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); } $this->handler = new $handler_class(); } /** * Check authentication for the request * * @since 2.1 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login */ public function check_authentication() { // allow plugins to remove default authentication or add their own authentication $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); // API requests run under the context of the authenticated user if ( is_a( $user, 'WP_User' ) ) { wp_set_current_user( $user->ID ); } elseif ( ! is_wp_error( $user ) ) { // WP_Errors are handled in serve_request() $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); } return $user; } /** * Convert an error to an array * * This iterates over all error codes and messages to change it into a flat * array. This enables simpler client behaviour, as it is represented as a * list in JSON rather than an object/map * * @since 2.1 * @param WP_Error $error * @return array List of associative arrays with code and message keys */ protected function error_to_array( $error ) { $errors = array(); foreach ( (array) $error->errors as $code => $messages ) { foreach ( (array) $messages as $message ) { $errors[] = array( 'code' => $code, 'message' => $message ); } } return array( 'errors' => $errors ); } /** * Handle serving an API request * * Matches the current server URI to a route and runs the first matching * callback then outputs a JSON representation of the returned value. * * @since 2.1 * @uses WC_API_Server::dispatch() */ public function serve_request() { do_action( 'woocommerce_api_server_before_serve', $this ); $this->header( 'Content-Type', $this->handler->get_content_type(), true ); // the API is enabled by default if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { $this->send_status( 404 ); echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); return; } $result = $this->check_authentication(); // if authorization check was successful, dispatch the request if ( ! is_wp_error( $result ) ) { $result = $this->dispatch(); } // handle any dispatch errors if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); if ( is_array( $data ) && isset( $data['status'] ) ) { $this->send_status( $data['status'] ); } $result = $this->error_to_array( $result ); } // This is a filter rather than an action, since this is designed to be // re-entrant if needed $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); if ( ! $served ) { if ( 'HEAD' === $this->method ) { return; } echo $this->handler->generate_response( $result ); } } /** * Retrieve the route map * * The route map is an associative array with path regexes as the keys. The * value is an indexed array with the callback function/method as the first * item, and a bitmask of HTTP methods as the second item (see the class * constants). * * Each route can be mapped to more than one callback by using an array of * the indexed arrays. This allows mapping e.g. GET requests to one callback * and POST requests to another. * * Note that the path regexes (array keys) must have @ escaped, as this is * used as the delimiter with preg_match() * * @since 2.1 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` */ public function get_routes() { // index added by default $endpoints = array( '/' => array( array( $this, 'get_index' ), self::READABLE ), ); $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); // Normalise the endpoints foreach ( $endpoints as $route => &$handlers ) { if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { $handlers = array( $handlers ); } } return $endpoints; } /** * Match the request to a callback and call it * * @since 2.1 * @return mixed The value returned by the callback, or a WP_Error instance */ public function dispatch() { switch ( $this->method ) { case 'HEAD': case 'GET': $method = self::METHOD_GET; break; case 'POST': $method = self::METHOD_POST; break; case 'PUT': $method = self::METHOD_PUT; break; case 'PATCH': $method = self::METHOD_PATCH; break; case 'DELETE': $method = self::METHOD_DELETE; break; default: return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); } foreach ( $this->get_routes() as $route => $handlers ) { foreach ( $handlers as $handler ) { $callback = $handler[0]; $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; if ( ! ( $supported & $method ) ) { continue; } $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); if ( ! $match ) { continue; } if ( ! is_callable( $callback ) ) { return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); } $args = array_merge( $args, $this->params['GET'] ); if ( $method & self::METHOD_POST ) { $args = array_merge( $args, $this->params['POST'] ); } if ( $supported & self::ACCEPT_DATA ) { $data = $this->handler->parse_body( $this->get_raw_data() ); $args = array_merge( $args, array( 'data' => $data ) ); } elseif ( $supported & self::ACCEPT_RAW_DATA ) { $data = $this->get_raw_data(); $args = array_merge( $args, array( 'data' => $data ) ); } $args['_method'] = $method; $args['_route'] = $route; $args['_path'] = $this->path; $args['_headers'] = $this->headers; $args['_files'] = $this->files; $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); // Allow plugins to halt the request via this filter if ( is_wp_error( $args ) ) { return $args; } $params = $this->sort_callback_params( $callback, $args ); if ( is_wp_error( $params ) ) { return $params; } return call_user_func_array( $callback, $params ); } } return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); } /** * Sort parameters by order specified in method declaration * * Takes a callback and a list of available params, then filters and sorts * by the parameters the method actually needs, using the Reflection API * * @since 2.1 * * @param callable|array $callback the endpoint callback * @param array $provided the provided request parameters * * @return array|WP_Error */ protected function sort_callback_params( $callback, $provided ) { if ( is_array( $callback ) ) { $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); } else { $ref_func = new ReflectionFunction( $callback ); } $wanted = $ref_func->getParameters(); $ordered_parameters = array(); foreach ( $wanted as $param ) { if ( isset( $provided[ $param->getName() ] ) ) { // We have this parameters in the list to choose from $ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] ); } elseif ( $param->isDefaultValueAvailable() ) { // We don't have this parameter, but it's optional $ordered_parameters[] = $param->getDefaultValue(); } else { // We don't have this parameter and it wasn't optional, abort! return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); } } return $ordered_parameters; } /** * Get the site index. * * This endpoint describes the capabilities of the site. * * @since 2.1 * @return array Index entity */ public function get_index() { // General site data $available = array( 'store' => array( 'name' => get_option( 'blogname' ), 'description' => get_option( 'blogdescription' ), 'URL' => get_option( 'siteurl' ), 'wc_version' => WC()->version, 'routes' => array(), 'meta' => array( 'timezone' => wc_timezone_string(), 'currency' => get_woocommerce_currency(), 'currency_format' => get_woocommerce_currency_symbol(), 'tax_included' => wc_prices_include_tax(), 'weight_unit' => get_option( 'woocommerce_weight_unit' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), 'links' => array( 'help' => 'https://woocommerce.github.io/woocommerce/rest-api/', ), ), ), ); // Find the available routes foreach ( $this->get_routes() as $route => $callbacks ) { $data = array(); $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); $methods = array(); foreach ( self::$method_map as $name => $bitmask ) { foreach ( $callbacks as $callback ) { // Skip to the next route if any callback is hidden if ( $callback[1] & self::HIDDEN_ENDPOINT ) { continue 3; } if ( $callback[1] & $bitmask ) { $data['supports'][] = $name; } if ( $callback[1] & self::ACCEPT_DATA ) { $data['accepts_data'] = true; } // For non-variable routes, generate links if ( strpos( $route, '<' ) === false ) { $data['meta'] = array( 'self' => get_woocommerce_api_url( $route ), ); } } } $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); } return apply_filters( 'woocommerce_api_index', $available ); } /** * Send a HTTP status code * * @since 2.1 * @param int $code HTTP status */ public function send_status( $code ) { status_header( $code ); } /** * Send a HTTP header * * @since 2.1 * @param string $key Header key * @param string $value Header value * @param boolean $replace Should we replace the existing header? */ public function header( $key, $value, $replace = true ) { header( sprintf( '%s: %s', $key, $value ), $replace ); } /** * Send a Link header * * @internal The $rel parameter is first, as this looks nicer when sending multiple * * @link http://tools.ietf.org/html/rfc5988 * @link http://www.iana.org/assignments/link-relations/link-relations.xml * * @since 2.1 * @param string $rel Link relation. Either a registered type, or an absolute URL * @param string $link Target IRI for the link * @param array $other Other parameters to send, as an associative array */ public function link_header( $rel, $link, $other = array() ) { $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); foreach ( $other as $key => $value ) { if ( 'title' == $key ) { $value = '"' . $value . '"'; } $header .= '; ' . $key . '=' . $value; } $this->header( 'Link', $header, false ); } /** * Send pagination headers for resources * * @since 2.1 * @param WP_Query|WP_User_Query $query */ public function add_pagination_headers( $query ) { // WP_User_Query if ( is_a( $query, 'WP_User_Query' ) ) { $page = $query->page; $single = count( $query->get_results() ) == 1; $total = $query->get_total(); $total_pages = $query->total_pages; // WP_Query } else { $page = $query->get( 'paged' ); $single = $query->is_single(); $total = $query->found_posts; $total_pages = $query->max_num_pages; } if ( ! $page ) { $page = 1; } $next_page = absint( $page ) + 1; if ( ! $single ) { // first/prev if ( $page > 1 ) { $this->link_header( 'first', $this->get_paginated_url( 1 ) ); $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); } // next if ( $next_page <= $total_pages ) { $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); } // last if ( $page != $total_pages ) { $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); } } $this->header( 'X-WC-Total', $total ); $this->header( 'X-WC-TotalPages', $total_pages ); do_action( 'woocommerce_api_pagination_headers', $this, $query ); } /** * Returns the request URL with the page query parameter set to the specified page * * @since 2.1 * @param int $page * @return string */ private function get_paginated_url( $page ) { // remove existing page query param $request = remove_query_arg( 'page' ); // add provided page query param $request = urldecode( add_query_arg( 'page', $page, $request ) ); // get the home host $host = parse_url( get_home_url(), PHP_URL_HOST ); return set_url_scheme( "http://{$host}{$request}" ); } /** * Retrieve the raw request entity (body) * * @since 2.1 * @return string */ public function get_raw_data() { // @codingStandardsIgnoreStart // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6. if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; // @codingStandardsIgnoreEnd } /** * Parse an RFC3339 datetime into a MySQl datetime * * Invalid dates default to unix epoch * * @since 2.1 * @param string $datetime RFC3339 datetime * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) */ public function parse_datetime( $datetime ) { // Strip millisecond precision (a full stop followed by one or more digits) if ( strpos( $datetime, '.' ) !== false ) { $datetime = preg_replace( '/\.\d+/', '', $datetime ); } // default timezone to UTC $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); try { $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { $datetime = new DateTime( '@0' ); } return $datetime->format( 'Y-m-d H:i:s' ); } /** * Format a unix timestamp or MySQL datetime into an RFC3339 datetime * * @since 2.1 * @param int|string $timestamp unix timestamp or MySQL datetime * @param bool $convert_to_utc * @param bool $convert_to_gmt Use GMT timezone. * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { if ( $convert_to_gmt ) { if ( is_numeric( $timestamp ) ) { $timestamp = date( 'Y-m-d H:i:s', $timestamp ); } $timestamp = get_gmt_from_date( $timestamp ); } if ( $convert_to_utc ) { $timezone = new DateTimeZone( wc_timezone_string() ); } else { $timezone = new DateTimeZone( 'UTC' ); } try { if ( is_numeric( $timestamp ) ) { $date = new DateTime( "@{$timestamp}" ); } else { $date = new DateTime( $timestamp, $timezone ); } // convert to UTC by adjusting the time based on the offset of the site's timezone if ( $convert_to_utc ) { $date->modify( -1 * $date->getOffset() . ' seconds' ); } } catch ( Exception $e ) { $date = new DateTime( '@0' ); } return $date->format( 'Y-m-d\TH:i:s\Z' ); } /** * Extract headers from a PHP-style $_SERVER array * * @since 2.1 * @param array $server Associative array similar to $_SERVER * @return array Headers extracted from the input */ public function get_headers( $server ) { $headers = array(); // CONTENT_* headers are not prefixed with HTTP_ $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); foreach ( $server as $key => $value ) { if ( strpos( $key, 'HTTP_' ) === 0 ) { $headers[ substr( $key, 5 ) ] = $value; } elseif ( isset( $additional[ $key ] ) ) { $headers[ $key ] = $value; } } return $headers; } /** * Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or * the HTTP ACCEPT header * * @since 2.1 * @return bool */ private function is_json_request() { // check path if ( false !== stripos( $this->path, '.json' ) ) { return true; } // check ACCEPT header, only 'application/json' is acceptable, see RFC 4627 if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) { return true; } return false; } /** * Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or * the HTTP ACCEPT header * * @since 2.1 * @return bool */ private function is_xml_request() { // check path if ( false !== stripos( $this->path, '.xml' ) ) { return true; } // check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376 if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) { return true; } return false; } } includes/legacy/api/v1/class-wc-api-orders.php 0000644 00000027600 15132754524 0015222 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Orders extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/orders'; /** * Register the routes for this class * * GET /orders * GET /orders/count * GET|PUT /orders/<id> * GET /orders/<id>/notes * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /orders $routes[ $this->base ] = array( array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), ); # GET /orders/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), ); # GET|PUT /orders/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_order' ), WC_API_Server::READABLE ), array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), ); # GET /orders/<id>/notes $routes[ $this->base . '/(?P<id>\d+)/notes' ] = array( array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get all orders * * @since 2.1 * @param string $fields * @param array $filter * @param string $status * @param int $page * @return array */ public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $filter['page'] = $page; $query = $this->query_orders( $filter ); $orders = array(); foreach ( $query->posts as $order_id ) { if ( ! $this->is_readable( $order_id ) ) { continue; } $orders[] = current( $this->get_order( $order_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'orders' => $orders ); } /** * Get the order for the given ID * * @since 2.1 * @param int $id the order ID * @param array $fields * @return array|WP_Error */ public function get_order( $id, $fields = null ) { // ensure order ID is valid & user has permission to read $id = $this->validate_request( $id, 'shop_order', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $order = wc_get_order( $id ); $order_data = array( 'id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. 'status' => $order->get_status(), 'currency' => $order->get_currency(), 'total' => wc_format_decimal( $order->get_total(), 2 ), 'subtotal' => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ), 'total_line_items_quantity' => $order->get_item_count(), 'total_tax' => wc_format_decimal( $order->get_total_tax(), 2 ), 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), 2 ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), 2 ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), 2 ), 'total_discount' => wc_format_decimal( $order->get_total_discount(), 2 ), 'cart_discount' => wc_format_decimal( 0, 2 ), 'order_discount' => wc_format_decimal( 0, 2 ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->get_payment_method(), 'method_title' => $order->get_payment_method_title(), 'paid' => ! is_null( $order->get_date_paid() ), ), 'billing_address' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone(), ), 'shipping_address' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'company' => $order->get_shipping_company(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), ), 'note' => $order->get_customer_note(), 'customer_ip' => $order->get_customer_ip_address(), 'customer_user_agent' => $order->get_customer_user_agent(), 'customer_id' => $order->get_user_id(), 'view_order_url' => $order->get_view_order_url(), 'line_items' => array(), 'shipping_lines' => array(), 'tax_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), ); // add line items foreach ( $order->get_items() as $item_id => $item ) { $product = $item->get_product(); $order_data['line_items'][] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), 'quantity' => $item->get_quantity(), 'tax_class' => $item->get_tax_class(), 'name' => $item->get_name(), 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), 'sku' => is_object( $product ) ? $product->get_sku() : null, ); } // add shipping foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $order_data['shipping_lines'][] = array( 'id' => $shipping_item_id, 'method_id' => $shipping_item->get_method_id(), 'method_title' => $shipping_item->get_name(), 'total' => wc_format_decimal( $shipping_item->get_total(), 2 ), ); } // add taxes foreach ( $order->get_tax_totals() as $tax_code => $tax ) { $order_data['tax_lines'][] = array( 'code' => $tax_code, 'title' => $tax->label, 'total' => wc_format_decimal( $tax->amount, 2 ), 'compound' => (bool) $tax->is_compound, ); } // add fees foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $order_data['fee_lines'][] = array( 'id' => $fee_item_id, 'title' => $fee_item->get_name(), 'tax_class' => $fee_item->get_tax_class(), 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ), ); } // add coupons foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $order_data['coupon_lines'][] = array( 'id' => $coupon_item_id, 'code' => $coupon_item->get_code(), 'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ), ); } return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); } /** * Get the total number of orders * * @since 2.1 * * @param string $status * @param array $filter * * @return array|WP_Error */ public function get_orders_count( $status = null, $filter = array() ) { if ( ! empty( $status ) ) { $filter['status'] = $status; } $query = $this->query_orders( $filter ); if ( ! current_user_can( 'read_private_shop_orders' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) ); } return array( 'count' => (int) $query->found_posts ); } /** * Edit an order * * API v1 only allows updating the status of an order * * @since 2.1 * @param int $id the order ID * @param array $data * @return array|WP_Error */ public function edit_order( $id, $data ) { $id = $this->validate_request( $id, 'shop_order', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } $order = wc_get_order( $id ); if ( ! empty( $data['status'] ) ) { $order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' ); } return $this->get_order( $id ); } /** * Delete an order * * @param int $id the order ID * @param bool $force true to permanently delete order, false to move to trash * @return array */ public function delete_order( $id, $force = false ) { $id = $this->validate_request( $id, 'shop_order', 'delete' ); return $this->delete( $id, 'order', ( 'true' === $force ) ); } /** * Get the admin order notes for an order * * @since 2.1 * @param int $id the order ID * @param string $fields fields to include in response * @return array|WP_Error */ public function get_order_notes( $id, $fields = null ) { // ensure ID is valid order ID $id = $this->validate_request( $id, 'shop_order', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $args = array( 'post_id' => $id, 'approve' => 'approve', 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $order_notes = array(); foreach ( $notes as $note ) { $order_notes[] = array( 'id' => $note->comment_ID, 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); } return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) ); } /** * Helper method to get order post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_orders( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_order', 'post_status' => array_keys( wc_get_order_statuses() ), ); // add status argument if ( ! empty( $args['status'] ) ) { $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); $statuses = explode( ',', $statuses ); $query_args['post_status'] = $statuses; unset( $args['status'] ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Helper method to get the order subtotal * * @since 2.1 * @param WC_Order $order * @return float */ private function get_order_subtotal( $order ) { $subtotal = 0; // subtotal foreach ( $order->get_items() as $item ) { $subtotal += $item->get_subtotal(); } return $subtotal; } } includes/legacy/api/v1/class-wc-api-coupons.php 0000644 00000015767 15132754524 0015425 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.1 * @version 2.1 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class WC_API_Coupons extends WC_API_Resource { /** @var string $base the route base */ protected $base = '/coupons'; /** * Register the routes for this class * * GET /coupons * GET /coupons/count * GET /coupons/<id> * * @since 2.1 * @param array $routes * @return array */ public function register_routes( $routes ) { # GET /coupons $routes[ $this->base ] = array( array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), ); # GET /coupons/count $routes[ $this->base . '/count' ] = array( array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), ); # GET /coupons/<id> $routes[ $this->base . '/(?P<id>\d+)' ] = array( array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), ); # GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores $routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array( array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), ); return $routes; } /** * Get all coupons * * @since 2.1 * @param string $fields * @param array $filter * @param int $page * @return array */ public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { $filter['page'] = $page; $query = $this->query_coupons( $filter ); $coupons = array(); foreach ( $query->posts as $coupon_id ) { if ( ! $this->is_readable( $coupon_id ) ) { continue; } $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'coupons' => $coupons ); } /** * Get the coupon for the given ID * * @since 2.1 * * @param int $id the coupon ID * @param string $fields fields to include in response * * @return array|WP_Error * @throws WC_API_Exception */ public function get_coupon( $id, $fields = null ) { $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) { return $id; } $coupon = new WC_Coupon( $id ); if ( 0 === $coupon->get_id() ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); } $coupon_data = array( 'id' => $coupon->get_id(), 'code' => $coupon->get_code(), 'type' => $coupon->get_discount_type(), 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), 'individual_use' => $coupon->get_individual_use(), 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), 'usage_count' => (int) $coupon->get_usage_count(), 'expiry_date' => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times. 'enable_free_shipping' => $coupon->get_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), 'exclude_sale_items' => $coupon->get_exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), 'customer_emails' => $coupon->get_email_restrictions(), ); return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); } /** * Get the total number of coupons * * @since 2.1 * * @param array $filter * * @return array|WP_Error */ public function get_coupons_count( $filter = array() ) { $query = $this->query_coupons( $filter ); if ( ! current_user_can( 'read_private_shop_coupons' ) ) { return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) ); } return array( 'count' => (int) $query->found_posts ); } /** * Get the coupon for the given code * * @since 2.1 * @param string $code the coupon code * @param string $fields fields to include in response * @return int|WP_Error */ public function get_coupon_by_code( $code, $fields = null ) { global $wpdb; $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); if ( is_null( $id ) ) { return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) ); } return $this->get_coupon( $id, $fields ); } /** * Create a coupon * * @param array $data * @return array */ public function create_coupon( $data ) { return array(); } /** * Edit a coupon * * @param int $id the coupon ID * @param array $data * @return array|WP_Error */ public function edit_coupon( $id, $data ) { $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); if ( is_wp_error( $id ) ) { return $id; } return $this->get_coupon( $id ); } /** * Delete a coupon * * @param int $id the coupon ID * @param bool $force true to permanently delete coupon, false to move to trash * @return array|WP_Error */ public function delete_coupon( $id, $force = false ) { $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); } /** * Helper method to get coupon post objects * * @since 2.1 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_coupons( $args ) { // set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_coupon', 'post_status' => 'publish', ); $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } } includes/legacy/abstract-wc-legacy-product.php 0000644 00000052544 15132754524 0015503 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Abstract Product * * Legacy and deprecated functions are here to keep the WC_Abstract_Product * clean. * This class will be removed in future versions. * * @version 3.0.0 * @package WooCommerce\Abstracts * @category Abstract Class * @author WooThemes */ abstract class WC_Abstract_Legacy_Product extends WC_Data { /** * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. * * @param string $key Key name. * @return bool */ public function __isset( $key ) { $valid = array( 'id', 'product_attributes', 'visibility', 'sale_price_dates_from', 'sale_price_dates_to', 'post', 'download_type', 'product_image_gallery', 'variation_shipping_class', 'shipping_class', 'total_stock', 'crosssell_ids', 'parent', ); if ( $this->is_type( 'variation' ) ) { $valid = array_merge( $valid, array( 'variation_id', 'variation_data', 'variation_has_stock', 'variation_shipping_class_id', 'variation_has_sku', 'variation_has_length', 'variation_has_width', 'variation_has_height', 'variation_has_weight', 'variation_has_tax_class', 'variation_has_downloadable_files', ) ); } return in_array( $key, array_merge( $valid, array_keys( $this->data ) ) ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) || metadata_exists( 'post', $this->get_parent_id(), '_' . $key ); } /** * Magic __get method for backwards compatibility. Maps legacy vars to new getters. * * @param string $key Key name. * @return mixed */ public function __get( $key ) { if ( 'post_type' === $key ) { return $this->post_type; } wc_doing_it_wrong( $key, __( 'Product properties should not be accessed directly.', 'woocommerce' ), '3.0' ); switch ( $key ) { case 'id' : $value = $this->is_type( 'variation' ) ? $this->get_parent_id() : $this->get_id(); break; case 'product_type' : $value = $this->get_type(); break; case 'product_attributes' : $value = isset( $this->data['attributes'] ) ? $this->data['attributes'] : ''; break; case 'visibility' : $value = $this->get_catalog_visibility(); break; case 'sale_price_dates_from' : return $this->get_date_on_sale_from() ? $this->get_date_on_sale_from()->getTimestamp() : ''; break; case 'sale_price_dates_to' : return $this->get_date_on_sale_to() ? $this->get_date_on_sale_to()->getTimestamp() : ''; break; case 'post' : $value = get_post( $this->get_id() ); break; case 'download_type' : return 'standard'; break; case 'product_image_gallery' : $value = $this->get_gallery_image_ids(); break; case 'variation_shipping_class' : case 'shipping_class' : $value = $this->get_shipping_class(); break; case 'total_stock' : $value = $this->get_total_stock(); break; case 'downloadable' : case 'virtual' : case 'manage_stock' : case 'featured' : case 'sold_individually' : $value = $this->{"get_$key"}() ? 'yes' : 'no'; break; case 'crosssell_ids' : $value = $this->get_cross_sell_ids(); break; case 'upsell_ids' : $value = $this->get_upsell_ids(); break; case 'parent' : $value = wc_get_product( $this->get_parent_id() ); break; case 'variation_id' : $value = $this->is_type( 'variation' ) ? $this->get_id() : ''; break; case 'variation_data' : $value = $this->is_type( 'variation' ) ? wc_get_product_variation_attributes( $this->get_id() ) : ''; break; case 'variation_has_stock' : $value = $this->is_type( 'variation' ) ? $this->managing_stock() : ''; break; case 'variation_shipping_class_id' : $value = $this->is_type( 'variation' ) ? $this->get_shipping_class_id() : ''; break; case 'variation_has_sku' : case 'variation_has_length' : case 'variation_has_width' : case 'variation_has_height' : case 'variation_has_weight' : case 'variation_has_tax_class' : case 'variation_has_downloadable_files' : $value = true; // These were deprecated in 2.2 and simply returned true in 2.6.x. break; default : if ( in_array( $key, array_keys( $this->data ) ) ) { $value = $this->{"get_$key"}(); } else { $value = get_post_meta( $this->id, '_' . $key, true ); } break; } return $value; } /** * If set, get the default attributes for a variable product. * * @deprecated 3.0.0 * @return array */ public function get_variation_default_attributes() { wc_deprecated_function( 'WC_Product_Variable::get_variation_default_attributes', '3.0', 'WC_Product::get_default_attributes' ); return apply_filters( 'woocommerce_product_default_attributes', $this->get_default_attributes(), $this ); } /** * Returns the gallery attachment ids. * * @deprecated 3.0.0 * @return array */ public function get_gallery_attachment_ids() { wc_deprecated_function( 'WC_Product::get_gallery_attachment_ids', '3.0', 'WC_Product::get_gallery_image_ids' ); return $this->get_gallery_image_ids(); } /** * Set stock level of the product. * * @deprecated 3.0.0 * * @param int $amount * @param string $mode * * @return int */ public function set_stock( $amount = null, $mode = 'set' ) { wc_deprecated_function( 'WC_Product::set_stock', '3.0', 'wc_update_product_stock' ); return wc_update_product_stock( $this, $amount, $mode ); } /** * Reduce stock level of the product. * * @deprecated 3.0.0 * @param int $amount Amount to reduce by. Default: 1 * @return int new stock level */ public function reduce_stock( $amount = 1 ) { wc_deprecated_function( 'WC_Product::reduce_stock', '3.0', 'wc_update_product_stock' ); return wc_update_product_stock( $this, $amount, 'decrease' ); } /** * Increase stock level of the product. * * @deprecated 3.0.0 * @param int $amount Amount to increase by. Default 1. * @return int new stock level */ public function increase_stock( $amount = 1 ) { wc_deprecated_function( 'WC_Product::increase_stock', '3.0', 'wc_update_product_stock' ); return wc_update_product_stock( $this, $amount, 'increase' ); } /** * Check if the stock status needs changing. * * @deprecated 3.0.0 Sync is done automatically on read/save, so calling this should not be needed any more. */ public function check_stock_status() { wc_deprecated_function( 'WC_Product::check_stock_status', '3.0' ); } /** * Get and return related products. * @deprecated 3.0.0 Use wc_get_related_products instead. * * @param int $limit * * @return array */ public function get_related( $limit = 5 ) { wc_deprecated_function( 'WC_Product::get_related', '3.0', 'wc_get_related_products' ); return wc_get_related_products( $this->get_id(), $limit ); } /** * Retrieves related product terms. * @deprecated 3.0.0 Use wc_get_product_term_ids instead. * * @param $term * * @return array */ protected function get_related_terms( $term ) { wc_deprecated_function( 'WC_Product::get_related_terms', '3.0', 'wc_get_product_term_ids' ); return array_merge( array( 0 ), wc_get_product_term_ids( $this->get_id(), $term ) ); } /** * Builds the related posts query. * @deprecated 3.0.0 Use Product Data Store get_related_products_query instead. * * @param $cats_array * @param $tags_array * @param $exclude_ids * @param $limit */ protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) { wc_deprecated_function( 'WC_Product::build_related_query', '3.0', 'Product Data Store get_related_products_query' ); $data_store = WC_Data_Store::load( 'product' ); return $data_store->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ); } /** * Returns the child product. * @deprecated 3.0.0 Use wc_get_product instead. * @param mixed $child_id * @return WC_Product|WC_Product|WC_Product_variation */ public function get_child( $child_id ) { wc_deprecated_function( 'WC_Product::get_child', '3.0', 'wc_get_product' ); return wc_get_product( $child_id ); } /** * Functions for getting parts of a price, in html, used by get_price_html. * * @deprecated 3.0.0 * @return string */ public function get_price_html_from_text() { wc_deprecated_function( 'WC_Product::get_price_html_from_text', '3.0', 'wc_get_price_html_from_text' ); return wc_get_price_html_from_text(); } /** * Functions for getting parts of a price, in html, used by get_price_html. * * @deprecated 3.0.0 Use wc_format_sale_price instead. * @param string $from String or float to wrap with 'from' text * @param mixed $to String or float to wrap with 'to' text * @return string */ public function get_price_html_from_to( $from, $to ) { wc_deprecated_function( 'WC_Product::get_price_html_from_to', '3.0', 'wc_format_sale_price' ); return apply_filters( 'woocommerce_get_price_html_from_to', wc_format_sale_price( $from, $to ), $from, $to, $this ); } /** * Lists a table of attributes for the product page. * @deprecated 3.0.0 Use wc_display_product_attributes instead. */ public function list_attributes() { wc_deprecated_function( 'WC_Product::list_attributes', '3.0', 'wc_display_product_attributes' ); wc_display_product_attributes( $this ); } /** * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes. * * @deprecated 3.0.0 Use wc_get_price_including_tax instead. * @param int $qty * @param string $price to calculate, left blank to just use get_price() * @return string */ public function get_price_including_tax( $qty = 1, $price = '' ) { wc_deprecated_function( 'WC_Product::get_price_including_tax', '3.0', 'wc_get_price_including_tax' ); return wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ); } /** * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. * * @deprecated 3.0.0 Use wc_get_price_to_display instead. * @param string $price to calculate, left blank to just use get_price() * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax() * @return string */ public function get_display_price( $price = '', $qty = 1 ) { wc_deprecated_function( 'WC_Product::get_display_price', '3.0', 'wc_get_price_to_display' ); return wc_get_price_to_display( $this, array( 'qty' => $qty, 'price' => $price ) ); } /** * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting. * Uses store base tax rates. Can work for a specific $qty for more accurate taxes. * * @deprecated 3.0.0 Use wc_get_price_excluding_tax instead. * @param int $qty * @param string $price to calculate, left blank to just use get_price() * @return string */ public function get_price_excluding_tax( $qty = 1, $price = '' ) { wc_deprecated_function( 'WC_Product::get_price_excluding_tax', '3.0', 'wc_get_price_excluding_tax' ); return wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ); } /** * Adjust a products price dynamically. * * @deprecated 3.0.0 * @param mixed $price */ public function adjust_price( $price ) { wc_deprecated_function( 'WC_Product::adjust_price', '3.0', 'WC_Product::set_price / WC_Product::get_price' ); $this->data['price'] = $this->data['price'] + $price; } /** * Returns the product categories. * * @deprecated 3.0.0 * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ public function get_categories( $sep = ', ', $before = '', $after = '' ) { wc_deprecated_function( 'WC_Product::get_categories', '3.0', 'wc_get_product_category_list' ); return wc_get_product_category_list( $this->get_id(), $sep, $before, $after ); } /** * Returns the product tags. * * @deprecated 3.0.0 * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return array */ public function get_tags( $sep = ', ', $before = '', $after = '' ) { wc_deprecated_function( 'WC_Product::get_tags', '3.0', 'wc_get_product_tag_list' ); return wc_get_product_tag_list( $this->get_id(), $sep, $before, $after ); } /** * Get the product's post data. * * @deprecated 3.0.0 * @return WP_Post */ public function get_post_data() { wc_deprecated_function( 'WC_Product::get_post_data', '3.0', 'get_post' ); // In order to keep backwards compatibility it's required to use the parent data for variations. if ( $this->is_type( 'variation' ) ) { $post_data = get_post( $this->get_parent_id() ); } else { $post_data = get_post( $this->get_id() ); } return $post_data; } /** * Get the parent of the post. * * @deprecated 3.0.0 * @return int */ public function get_parent() { wc_deprecated_function( 'WC_Product::get_parent', '3.0', 'WC_Product::get_parent_id' ); return apply_filters( 'woocommerce_product_parent', absint( $this->get_post_data()->post_parent ), $this ); } /** * Returns the upsell product ids. * * @deprecated 3.0.0 * @return array */ public function get_upsells() { wc_deprecated_function( 'WC_Product::get_upsells', '3.0', 'WC_Product::get_upsell_ids' ); return apply_filters( 'woocommerce_product_upsell_ids', $this->get_upsell_ids(), $this ); } /** * Returns the cross sell product ids. * * @deprecated 3.0.0 * @return array */ public function get_cross_sells() { wc_deprecated_function( 'WC_Product::get_cross_sells', '3.0', 'WC_Product::get_cross_sell_ids' ); return apply_filters( 'woocommerce_product_crosssell_ids', $this->get_cross_sell_ids(), $this ); } /** * Check if variable product has default attributes set. * * @deprecated 3.0.0 * @return bool */ public function has_default_attributes() { wc_deprecated_function( 'WC_Product_Variable::has_default_attributes', '3.0', 'a check against WC_Product::get_default_attributes directly' ); if ( ! $this->get_default_attributes() ) { return true; } return false; } /** * Get variation ID. * * @deprecated 3.0.0 * @return int */ public function get_variation_id() { wc_deprecated_function( 'WC_Product::get_variation_id', '3.0', 'WC_Product::get_id(). It will always be the variation ID if this is a variation.' ); return $this->get_id(); } /** * Get product variation description. * * @deprecated 3.0.0 * @return string */ public function get_variation_description() { wc_deprecated_function( 'WC_Product::get_variation_description', '3.0', 'WC_Product::get_description()' ); return $this->get_description(); } /** * Check if all variation's attributes are set. * * @deprecated 3.0.0 * @return boolean */ public function has_all_attributes_set() { wc_deprecated_function( 'WC_Product::has_all_attributes_set', '3.0', 'an array filter on get_variation_attributes for a quick solution.' ); $set = true; // undefined attributes have null strings as array values foreach ( $this->get_variation_attributes() as $att ) { if ( ! $att ) { $set = false; break; } } return $set; } /** * Returns whether or not the variations parent is visible. * * @deprecated 3.0.0 * @return bool */ public function parent_is_visible() { wc_deprecated_function( 'WC_Product::parent_is_visible', '3.0' ); return $this->is_visible(); } /** * Get total stock - This is the stock of parent and children combined. * * @deprecated 3.0.0 * @return int */ public function get_total_stock() { wc_deprecated_function( 'WC_Product::get_total_stock', '3.0', 'get_stock_quantity on each child. Beware of performance issues in doing so.' ); if ( sizeof( $this->get_children() ) > 0 ) { $total_stock = max( 0, $this->get_stock_quantity() ); foreach ( $this->get_children() as $child_id ) { if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) { $stock = get_post_meta( $child_id, '_stock', true ); $total_stock += max( 0, wc_stock_amount( $stock ) ); } } } else { $total_stock = $this->get_stock_quantity(); } return wc_stock_amount( $total_stock ); } /** * Get formatted variation data with WC < 2.4 back compat and proper formatting of text-based attribute names. * * @deprecated 3.0.0 * * @param bool $flat * * @return string */ public function get_formatted_variation_attributes( $flat = false ) { wc_deprecated_function( 'WC_Product::get_formatted_variation_attributes', '3.0', 'wc_get_formatted_variation' ); return wc_get_formatted_variation( $this, $flat ); } /** * Sync variable product prices with the children lowest/highest prices. * * @deprecated 3.0.0 not used in core. * * @param int $product_id */ public function variable_product_sync( $product_id = 0 ) { wc_deprecated_function( 'WC_Product::variable_product_sync', '3.0' ); if ( empty( $product_id ) ) { $product_id = $this->get_id(); } // Sync prices with children if ( is_callable( array( __CLASS__, 'sync' ) ) ) { self::sync( $product_id ); } } /** * Sync the variable product's attributes with the variations. * * @param $product * @param bool $children */ public static function sync_attributes( $product, $children = false ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = wc_get_product( $product ); } /** * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. * Attempt to get full version of the text attribute from the parent and UPDATE meta. */ if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { $parent_attributes = array_filter( (array) get_post_meta( $product->get_id(), '_product_attributes', true ) ); if ( ! $children ) { $children = $product->get_children( 'edit' ); } foreach ( $children as $child_id ) { $all_meta = get_post_meta( $child_id ); foreach ( $all_meta as $name => $value ) { if ( 0 !== strpos( $name, 'attribute_' ) ) { continue; } if ( sanitize_title( $value[0] ) === $value[0] ) { foreach ( $parent_attributes as $attribute ) { if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { continue; } $text_attributes = wc_get_text_attributes( $attribute['value'] ); foreach ( $text_attributes as $text_attribute ) { if ( sanitize_title( $text_attribute ) === $value[0] ) { update_post_meta( $child_id, $name, $text_attribute ); break; } } } } } } } } /** * Match a variation to a given set of attributes using a WP_Query. * @deprecated 3.0.0 in favour of Product data store's find_matching_product_variation. * * @param array $match_attributes */ public function get_matching_variation( $match_attributes = array() ) { wc_deprecated_function( 'WC_Product::get_matching_variation', '3.0', 'Product data store find_matching_product_variation' ); $data_store = WC_Data_Store::load( 'product' ); return $data_store->find_matching_product_variation( $this, $match_attributes ); } /** * Returns whether or not we are showing dimensions on the product page. * @deprecated 3.0.0 Unused. * @return bool */ public function enable_dimensions_display() { wc_deprecated_function( 'WC_Product::enable_dimensions_display', '3.0' ); return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() ); } /** * Returns the product rating in html format. * * @deprecated 3.0.0 * @param string $rating (default: '') * @return string */ public function get_rating_html( $rating = null ) { wc_deprecated_function( 'WC_Product::get_rating_html', '3.0', 'wc_get_rating_html' ); return wc_get_rating_html( $rating ); } /** * Sync product rating. Can be called statically. * * @deprecated 3.0.0 * @param int $post_id */ public static function sync_average_rating( $post_id ) { wc_deprecated_function( 'WC_Product::sync_average_rating', '3.0', 'WC_Comments::get_average_rating_for_product or leave to CRUD.' ); // See notes in https://github.com/woocommerce/woocommerce/pull/22909#discussion_r262393401. // Sync count first like in the original method https://github.com/woocommerce/woocommerce/blob/2.6.0/includes/abstracts/abstract-wc-product.php#L1101-L1128. self::sync_rating_count( $post_id ); $average = WC_Comments::get_average_rating_for_product( wc_get_product( $post_id ) ); update_post_meta( $post_id, '_wc_average_rating', $average ); } /** * Sync product rating count. Can be called statically. * * @deprecated 3.0.0 * @param int $post_id */ public static function sync_rating_count( $post_id ) { wc_deprecated_function( 'WC_Product::sync_rating_count', '3.0', 'WC_Comments::get_rating_counts_for_product or leave to CRUD.' ); $counts = WC_Comments::get_rating_counts_for_product( wc_get_product( $post_id ) ); update_post_meta( $post_id, '_wc_rating_count', $counts ); } /** * Same as get_downloads in CRUD. * * @deprecated 3.0.0 * @return array */ public function get_files() { wc_deprecated_function( 'WC_Product::get_files', '3.0', 'WC_Product::get_downloads' ); return $this->get_downloads(); } /** * @deprecated 3.0.0 Sync is taken care of during save - no need to call this directly. */ public function grouped_product_sync() { wc_deprecated_function( 'WC_Product::grouped_product_sync', '3.0' ); } } includes/legacy/class-wc-legacy-api.php 0000644 00000020512 15132754524 0014064 0 ustar 00 <?php /** * WooCommerce Legacy API. Was deprecated with 2.6.0. * * @author WooThemes * @category API * @package WooCommerce\RestApi * @since 2.6 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy API. */ class WC_Legacy_API { /** * This is the major version for the REST API and takes * first-order position in endpoint URLs. * * @deprecated 2.6.0 * @var string */ const VERSION = '3.1.0'; /** * The REST API server. * * @deprecated 2.6.0 * @var WC_API_Server */ public $server; /** * REST API authentication class instance. * * @deprecated 2.6.0 * @var WC_API_Authentication */ public $authentication; /** * Init the legacy API. */ public function init() { add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 ); } /** * Add new query vars. * * @since 2.0 * @param array $vars Vars. * @return string[] */ public function add_query_vars( $vars ) { $vars[] = 'wc-api-version'; // Deprecated since 2.6.0. $vars[] = 'wc-api-route'; // Deprecated since 2.6.0. return $vars; } /** * Add new endpoints. * * @since 2.0 */ public static function add_endpoint() { // REST API, deprecated since 2.6.0. add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' ); add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' ); } /** * Handle REST API requests. * * @since 2.2 * @deprecated 2.6.0 */ public function handle_rest_api_requests() { global $wp; if ( ! empty( $_GET['wc-api-version'] ) ) { $wp->query_vars['wc-api-version'] = $_GET['wc-api-version']; } if ( ! empty( $_GET['wc-api-route'] ) ) { $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; } // REST API request. if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { wc_maybe_define_constant( 'WC_API_REQUEST', true ); wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); // Legacy v1 API request. if ( 1 === WC_API_REQUEST_VERSION ) { $this->handle_v1_rest_api_request(); } elseif ( 2 === WC_API_REQUEST_VERSION ) { $this->handle_v2_rest_api_request(); } else { $this->includes(); $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); // load API resource classes. $this->register_resources( $this->server ); // Fire off the request. $this->server->serve_request(); } exit; } } /** * Include required files for REST API request. * * @since 2.1 * @deprecated 2.6.0 */ public function includes() { // API server / response handlers. include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' ); include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' ); // Authentication. include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' ); include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' ); // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); } /** * Register available API resources. * * @since 2.1 * @deprecated 2.6.0 * @param WC_API_Server $server the REST server. */ public function register_resources( $server ) { $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Coupons', 'WC_API_Customers', 'WC_API_Orders', 'WC_API_Products', 'WC_API_Reports', 'WC_API_Taxes', 'WC_API_Webhooks', ) ); foreach ( $api_classes as $api_class ) { $this->$api_class = new $api_class( $server ); } } /** * Handle legacy v1 REST API requests. * * @since 2.2 * @deprecated 2.6.0 */ private function handle_v1_rest_api_request() { // Include legacy required files for v1 REST API request. include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' ); include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' ); // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); // Register available resources for legacy v1 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', 'WC_API_Orders', 'WC_API_Products', 'WC_API_Coupons', 'WC_API_Reports', ) ); foreach ( $api_classes as $api_class ) { $this->$api_class = new $api_class( $this->server ); } // Fire off the request. $this->server->serve_request(); } /** * Handle legacy v2 REST API requests. * * @since 2.4 * @deprecated 2.6.0 */ private function handle_v2_rest_api_request() { include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' ); include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' ); include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' ); // allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); // Register available resources for legacy v2 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', 'WC_API_Orders', 'WC_API_Products', 'WC_API_Coupons', 'WC_API_Reports', 'WC_API_Webhooks', ) ); foreach ( $api_classes as $api_class ) { $this->$api_class = new $api_class( $this->server ); } // Fire off the request. $this->server->serve_request(); } /** * Rest API Init. * * @deprecated 3.7.0 - REST API clases autoload. */ public function rest_api_init() {} /** * Include REST API classes. * * @deprecated 3.7.0 - REST API clases autoload. */ public function rest_api_includes() { $this->rest_api_init(); } /** * Register REST API routes. * * @deprecated 3.7.0 */ public function register_rest_routes() { wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' ); $this->register_wp_admin_settings(); } } includes/legacy/abstract-wc-legacy-order.php 0000644 00000061220 15132754524 0015125 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Abstract Order * * Legacy and deprecated functions are here to keep the WC_Abstract_Order clean. * This class will be removed in future versions. * * @version 3.0.0 * @package WooCommerce\Abstracts * @category Abstract Class * @author WooThemes */ abstract class WC_Abstract_Legacy_Order extends WC_Data { /** * Add coupon code to the order. * @param string|array $code * @param int $discount tax amount. * @param int $discount_tax amount. * @return int order item ID * @throws WC_Data_Exception */ public function add_coupon( $code = array(), $discount = 0, $discount_tax = 0 ) { wc_deprecated_function( 'WC_Order::add_coupon', '3.0', 'a new WC_Order_Item_Coupon object and add to order with WC_Order::add_item()' ); $item = new WC_Order_Item_Coupon(); $item->set_props( array( 'code' => $code, 'discount' => $discount, 'discount_tax' => $discount_tax, 'order_id' => $this->get_id(), ) ); $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_coupon', array( $this->get_id(), $item->get_id(), $code, $discount, $discount_tax ), '3.0', 'woocommerce_new_order_item action instead.' ); return $item->get_id(); } /** * Add a tax row to the order. * @param int $tax_rate_id * @param int $tax_amount amount of tax. * @param int $shipping_tax_amount shipping amount. * @return int order item ID * @throws WC_Data_Exception */ public function add_tax( $tax_rate_id, $tax_amount = 0, $shipping_tax_amount = 0 ) { wc_deprecated_function( 'WC_Order::add_tax', '3.0', 'a new WC_Order_Item_Tax object and add to order with WC_Order::add_item()' ); $item = new WC_Order_Item_Tax(); $item->set_props( array( 'rate_id' => $tax_rate_id, 'tax_total' => $tax_amount, 'shipping_tax_total' => $shipping_tax_amount, ) ); $item->set_rate( $tax_rate_id ); $item->set_order_id( $this->get_id() ); $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_tax', array( $this->get_id(), $item->get_id(), $tax_rate_id, $tax_amount, $shipping_tax_amount ), '3.0', 'woocommerce_new_order_item action instead.' ); return $item->get_id(); } /** * Add a shipping row to the order. * @param WC_Shipping_Rate shipping_rate * @return int order item ID * @throws WC_Data_Exception */ public function add_shipping( $shipping_rate ) { wc_deprecated_function( 'WC_Order::add_shipping', '3.0', 'a new WC_Order_Item_Shipping object and add to order with WC_Order::add_item()' ); $item = new WC_Order_Item_Shipping(); $item->set_props( array( 'method_title' => $shipping_rate->label, 'method_id' => $shipping_rate->id, 'total' => wc_format_decimal( $shipping_rate->cost ), 'taxes' => $shipping_rate->taxes, 'order_id' => $this->get_id(), ) ); foreach ( $shipping_rate->get_meta_data() as $key => $value ) { $item->add_meta_data( $key, $value, true ); } $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_shipping', array( $this->get_id(), $item->get_id(), $shipping_rate ), '3.0', 'woocommerce_new_order_item action instead.' ); return $item->get_id(); } /** * Add a fee to the order. * Order must be saved prior to adding items. * * Fee is an amount of money charged for a particular piece of work * or for a particular right or service, and not supposed to be negative. * * @throws WC_Data_Exception * @param object $fee Fee data. * @return int Updated order item ID. */ public function add_fee( $fee ) { wc_deprecated_function( 'WC_Order::add_fee', '3.0', 'a new WC_Order_Item_Fee object and add to order with WC_Order::add_item()' ); $item = new WC_Order_Item_Fee(); $item->set_props( array( 'name' => $fee->name, 'tax_class' => $fee->taxable ? $fee->tax_class : 0, 'total' => $fee->amount, 'total_tax' => $fee->tax, 'taxes' => array( 'total' => $fee->tax_data, ), 'order_id' => $this->get_id(), ) ); $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_fee', array( $this->get_id(), $item->get_id(), $fee ), '3.0', 'woocommerce_new_order_item action instead.' ); return $item->get_id(); } /** * Update a line item for the order. * * Note this does not update order totals. * * @param object|int $item order item ID or item object. * @param WC_Product $product * @param array $args data to update. * @return int updated order item ID * @throws WC_Data_Exception */ public function update_product( $item, $product, $args ) { wc_deprecated_function( 'WC_Order::update_product', '3.0', 'an interaction with the WC_Order_Item_Product class' ); if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } if ( ! is_object( $item ) || ! $item->is_type( 'line_item' ) ) { return false; } if ( ! $this->get_id() ) { $this->save(); // Order must exist } // BW compatibility with old args if ( isset( $args['totals'] ) ) { foreach ( $args['totals'] as $key => $value ) { if ( 'tax' === $key ) { $args['total_tax'] = $value; } elseif ( 'tax_data' === $key ) { $args['taxes'] = $value; } else { $args[ $key ] = $value; } } } // Handle qty if set. if ( isset( $args['qty'] ) ) { if ( $product->backorders_require_notification() && $product->is_on_backorder( $args['qty'] ) ) { $item->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $item ), $args['qty'] - max( 0, $product->get_stock_quantity() ), true ); } $args['subtotal'] = $args['subtotal'] ? $args['subtotal'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) ); $args['total'] = $args['total'] ? $args['total'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) ); } $item->set_order_id( $this->get_id() ); $item->set_props( $args ); $item->save(); do_action( 'woocommerce_order_edit_product', $this->get_id(), $item->get_id(), $args, $product ); return $item->get_id(); } /** * Update coupon for order. Note this does not update order totals. * @param object|int $item * @param array $args * @return int updated order item ID * @throws WC_Data_Exception */ public function update_coupon( $item, $args ) { wc_deprecated_function( 'WC_Order::update_coupon', '3.0', 'an interaction with the WC_Order_Item_Coupon class' ); if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } if ( ! is_object( $item ) || ! $item->is_type( 'coupon' ) ) { return false; } if ( ! $this->get_id() ) { $this->save(); // Order must exist } // BW compatibility for old args if ( isset( $args['discount_amount'] ) ) { $args['discount'] = $args['discount_amount']; } if ( isset( $args['discount_amount_tax'] ) ) { $args['discount_tax'] = $args['discount_amount_tax']; } $item->set_order_id( $this->get_id() ); $item->set_props( $args ); $item->save(); do_action( 'woocommerce_order_update_coupon', $this->get_id(), $item->get_id(), $args ); return $item->get_id(); } /** * Update shipping method for order. * * Note this does not update the order total. * * @param object|int $item * @param array $args * @return int updated order item ID * @throws WC_Data_Exception */ public function update_shipping( $item, $args ) { wc_deprecated_function( 'WC_Order::update_shipping', '3.0', 'an interaction with the WC_Order_Item_Shipping class' ); if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } if ( ! is_object( $item ) || ! $item->is_type( 'shipping' ) ) { return false; } if ( ! $this->get_id() ) { $this->save(); // Order must exist } // BW compatibility for old args if ( isset( $args['cost'] ) ) { $args['total'] = $args['cost']; } $item->set_order_id( $this->get_id() ); $item->set_props( $args ); $item->save(); $this->calculate_shipping(); do_action( 'woocommerce_order_update_shipping', $this->get_id(), $item->get_id(), $args ); return $item->get_id(); } /** * Update fee for order. * * Note this does not update order totals. * * @param object|int $item * @param array $args * @return int updated order item ID * @throws WC_Data_Exception */ public function update_fee( $item, $args ) { wc_deprecated_function( 'WC_Order::update_fee', '3.0', 'an interaction with the WC_Order_Item_Fee class' ); if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } if ( ! is_object( $item ) || ! $item->is_type( 'fee' ) ) { return false; } if ( ! $this->get_id() ) { $this->save(); // Order must exist } $item->set_order_id( $this->get_id() ); $item->set_props( $args ); $item->save(); do_action( 'woocommerce_order_update_fee', $this->get_id(), $item->get_id(), $args ); return $item->get_id(); } /** * Update tax line on order. * Note this does not update order totals. * * @since 3.0 * @param object|int $item * @param array $args * @return int updated order item ID * @throws WC_Data_Exception */ public function update_tax( $item, $args ) { wc_deprecated_function( 'WC_Order::update_tax', '3.0', 'an interaction with the WC_Order_Item_Tax class' ); if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } if ( ! is_object( $item ) || ! $item->is_type( 'tax' ) ) { return false; } if ( ! $this->get_id() ) { $this->save(); // Order must exist } $item->set_order_id( $this->get_id() ); $item->set_props( $args ); $item->save(); do_action( 'woocommerce_order_update_tax', $this->get_id(), $item->get_id(), $args ); return $item->get_id(); } /** * Get a product (either product or variation). * @deprecated 4.4.0 * @param object $item * @return WC_Product|bool */ public function get_product_from_item( $item ) { wc_deprecated_function( 'WC_Abstract_Legacy_Order::get_product_from_item', '4.4.0', '$item->get_product()' ); if ( is_callable( array( $item, 'get_product' ) ) ) { $product = $item->get_product(); } else { $product = false; } return apply_filters( 'woocommerce_get_product_from_item', $product, $item, $this ); } /** * Set the customer address. * @param array $address Address data. * @param string $type billing or shipping. */ public function set_address( $address, $type = 'billing' ) { foreach ( $address as $key => $value ) { update_post_meta( $this->get_id(), "_{$type}_" . $key, $value ); if ( is_callable( array( $this, "set_{$type}_{$key}" ) ) ) { $this->{"set_{$type}_{$key}"}( $value ); } } } /** * Set an order total. * @param float $amount * @param string $total_type * @return bool */ public function legacy_set_total( $amount, $total_type = 'total' ) { if ( ! in_array( $total_type, array( 'shipping', 'tax', 'shipping_tax', 'total', 'cart_discount', 'cart_discount_tax' ) ) ) { return false; } switch ( $total_type ) { case 'total' : $amount = wc_format_decimal( $amount, wc_get_price_decimals() ); $this->set_total( $amount ); update_post_meta( $this->get_id(), '_order_total', $amount ); break; case 'cart_discount' : $amount = wc_format_decimal( $amount ); $this->set_discount_total( $amount ); update_post_meta( $this->get_id(), '_cart_discount', $amount ); break; case 'cart_discount_tax' : $amount = wc_format_decimal( $amount ); $this->set_discount_tax( $amount ); update_post_meta( $this->get_id(), '_cart_discount_tax', $amount ); break; case 'shipping' : $amount = wc_format_decimal( $amount ); $this->set_shipping_total( $amount ); update_post_meta( $this->get_id(), '_order_shipping', $amount ); break; case 'shipping_tax' : $amount = wc_format_decimal( $amount ); $this->set_shipping_tax( $amount ); update_post_meta( $this->get_id(), '_order_shipping_tax', $amount ); break; case 'tax' : $amount = wc_format_decimal( $amount ); $this->set_cart_tax( $amount ); update_post_meta( $this->get_id(), '_order_tax', $amount ); break; } return true; } /** * Magic __isset method for backwards compatibility. Handles legacy properties which could be accessed directly in the past. * * @param string $key * @return bool */ public function __isset( $key ) { $legacy_props = array( 'completed_date', 'id', 'order_type', 'post', 'status', 'post_status', 'customer_note', 'customer_message', 'user_id', 'customer_user', 'prices_include_tax', 'tax_display_cart', 'display_totals_ex_tax', 'display_cart_ex_tax', 'order_date', 'modified_date', 'cart_discount', 'cart_discount_tax', 'order_shipping', 'order_shipping_tax', 'order_total', 'order_tax', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country', 'customer_ip_address', 'customer_user_agent', 'payment_method_title', 'payment_method', 'order_currency' ); return $this->get_id() ? ( in_array( $key, $legacy_props ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) ) : false; } /** * Magic __get method for backwards compatibility. * * @param string $key * @return mixed */ public function __get( $key ) { wc_doing_it_wrong( $key, 'Order properties should not be accessed directly.', '3.0' ); if ( 'completed_date' === $key ) { return $this->get_date_completed() ? gmdate( 'Y-m-d H:i:s', $this->get_date_completed()->getOffsetTimestamp() ) : ''; } elseif ( 'paid_date' === $key ) { return $this->get_date_paid() ? gmdate( 'Y-m-d H:i:s', $this->get_date_paid()->getOffsetTimestamp() ) : ''; } elseif ( 'modified_date' === $key ) { return $this->get_date_modified() ? gmdate( 'Y-m-d H:i:s', $this->get_date_modified()->getOffsetTimestamp() ) : ''; } elseif ( 'order_date' === $key ) { return $this->get_date_created() ? gmdate( 'Y-m-d H:i:s', $this->get_date_created()->getOffsetTimestamp() ) : ''; } elseif ( 'id' === $key ) { return $this->get_id(); } elseif ( 'post' === $key ) { return get_post( $this->get_id() ); } elseif ( 'status' === $key ) { return $this->get_status(); } elseif ( 'post_status' === $key ) { return get_post_status( $this->get_id() ); } elseif ( 'customer_message' === $key || 'customer_note' === $key ) { return $this->get_customer_note(); } elseif ( in_array( $key, array( 'user_id', 'customer_user' ) ) ) { return $this->get_customer_id(); } elseif ( 'tax_display_cart' === $key ) { return get_option( 'woocommerce_tax_display_cart' ); } elseif ( 'display_totals_ex_tax' === $key ) { return 'excl' === get_option( 'woocommerce_tax_display_cart' ); } elseif ( 'display_cart_ex_tax' === $key ) { return 'excl' === get_option( 'woocommerce_tax_display_cart' ); } elseif ( 'cart_discount' === $key ) { return $this->get_total_discount(); } elseif ( 'cart_discount_tax' === $key ) { return $this->get_discount_tax(); } elseif ( 'order_tax' === $key ) { return $this->get_cart_tax(); } elseif ( 'order_shipping_tax' === $key ) { return $this->get_shipping_tax(); } elseif ( 'order_shipping' === $key ) { return $this->get_shipping_total(); } elseif ( 'order_total' === $key ) { return $this->get_total(); } elseif ( 'order_type' === $key ) { return $this->get_type(); } elseif ( 'order_currency' === $key ) { return $this->get_currency(); } elseif ( 'order_version' === $key ) { return $this->get_version(); } elseif ( is_callable( array( $this, "get_{$key}" ) ) ) { return $this->{"get_{$key}"}(); } else { return get_post_meta( $this->get_id(), '_' . $key, true ); } } /** * has_meta function for order items. This is different to the WC_Data * version and should be removed in future versions. * * @deprecated 3.0 * * @param int $order_item_id * * @return array of meta data. */ public function has_meta( $order_item_id ) { global $wpdb; wc_deprecated_function( 'WC_Order::has_meta( $order_item_id )', '3.0', 'WC_Order_item::get_meta_data' ); return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d ORDER BY meta_id", absint( $order_item_id ) ), ARRAY_A ); } /** * Display meta data belonging to an item. * @param array $item */ public function display_item_meta( $item ) { wc_deprecated_function( 'WC_Order::display_item_meta', '3.0', 'wc_display_item_meta' ); $product = $item->get_product(); $item_meta = new WC_Order_Item_Meta( $item, $product ); $item_meta->display(); } /** * Display download links for an order item. * @param array $item */ public function display_item_downloads( $item ) { wc_deprecated_function( 'WC_Order::display_item_downloads', '3.0', 'wc_display_item_downloads' ); $product = $item->get_product(); if ( $product && $product->exists() && $product->is_downloadable() && $this->is_download_permitted() ) { $download_files = $this->get_item_downloads( $item ); $i = 0; $links = array(); foreach ( $download_files as $download_id => $file ) { $i++; /* translators: 1: current item count */ $prefix = count( $download_files ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' ); $links[] = '<small class="download-url">' . esc_html( $prefix ) . ': <a href="' . esc_url( $file['download_url'] ) . '" target="_blank">' . esc_html( $file['name'] ) . '</a></small>' . "\n"; } echo '<br/>' . implode( '<br/>', $links ); } } /** * Get the Download URL. * * @param int $product_id * @param int $download_id * @return string */ public function get_download_url( $product_id, $download_id ) { wc_deprecated_function( 'WC_Order::get_download_url', '3.0', 'WC_Order_Item_Product::get_item_download_url' ); return add_query_arg( array( 'download_file' => $product_id, 'order' => $this->get_order_key(), 'email' => urlencode( $this->get_billing_email() ), 'key' => $download_id, ), trailingslashit( home_url() ) ); } /** * Get the downloadable files for an item in this order. * * @param array $item * @return array */ public function get_item_downloads( $item ) { wc_deprecated_function( 'WC_Order::get_item_downloads', '3.0', 'WC_Order_Item_Product::get_item_downloads' ); if ( ! $item instanceof WC_Order_Item ) { if ( ! empty( $item['variation_id'] ) ) { $product_id = $item['variation_id']; } elseif ( ! empty( $item['product_id'] ) ) { $product_id = $item['product_id']; } else { return array(); } // Create a 'virtual' order item to allow retrieving item downloads when // an array of product_id is passed instead of actual order item. $item = new WC_Order_Item_Product(); $item->set_product( wc_get_product( $product_id ) ); $item->set_order_id( $this->get_id() ); } return $item->get_item_downloads(); } /** * Gets shipping total. Alias of WC_Order::get_shipping_total(). * @deprecated 3.0.0 since this is an alias only. * @return float */ public function get_total_shipping() { return $this->get_shipping_total(); } /** * Get order item meta. * @deprecated 3.0.0 * @param mixed $order_item_id * @param string $key (default: '') * @param bool $single (default: false) * @return array|string */ public function get_item_meta( $order_item_id, $key = '', $single = false ) { wc_deprecated_function( 'WC_Order::get_item_meta', '3.0', 'wc_get_order_item_meta' ); return get_metadata( 'order_item', $order_item_id, $key, $single ); } /** * Get all item meta data in array format in the order it was saved. Does not group meta by key like get_item_meta(). * * @param mixed $order_item_id * @return array of objects */ public function get_item_meta_array( $order_item_id ) { wc_deprecated_function( 'WC_Order::get_item_meta_array', '3.0', 'WC_Order_Item::get_meta_data() (note the format has changed)' ); $item = $this->get_item( $order_item_id ); $meta_data = $item->get_meta_data(); $item_meta_array = array(); foreach ( $meta_data as $meta ) { $item_meta_array[ $meta->id ] = $meta; } return $item_meta_array; } /** * Get coupon codes only. * * @deprecated 3.7.0 - Replaced with better named method to reflect the actual data being returned. * @return array */ public function get_used_coupons() { wc_deprecated_function( 'get_used_coupons', '3.7', 'WC_Abstract_Order::get_coupon_codes' ); return $this->get_coupon_codes(); } /** * Expand item meta into the $item array. * @deprecated 3.0.0 Item meta no longer expanded due to new order item * classes. This function now does nothing to avoid data breakage. * @param array $item before expansion. * @return array */ public function expand_item_meta( $item ) { wc_deprecated_function( 'WC_Order::expand_item_meta', '3.0' ); return $item; } /** * Load the order object. Called from the constructor. * @deprecated 3.0.0 Logic moved to constructor * @param int|object|WC_Order $order Order to init. */ protected function init( $order ) { wc_deprecated_function( 'WC_Order::init', '3.0', 'Logic moved to constructor' ); if ( is_numeric( $order ) ) { $this->set_id( $order ); } elseif ( $order instanceof WC_Order ) { $this->set_id( absint( $order->get_id() ) ); } elseif ( isset( $order->ID ) ) { $this->set_id( absint( $order->ID ) ); } $this->set_object_read( false ); $this->data_store->read( $this ); } /** * Gets an order from the database. * @deprecated 3.0 * @param int $id (default: 0). * @return bool */ public function get_order( $id = 0 ) { wc_deprecated_function( 'WC_Order::get_order', '3.0' ); if ( ! $id ) { return false; } $result = get_post( $id ); if ( $result ) { $this->populate( $result ); return true; } return false; } /** * Populates an order from the loaded post data. * @deprecated 3.0 * @param mixed $result */ public function populate( $result ) { wc_deprecated_function( 'WC_Order::populate', '3.0' ); $this->set_id( $result->ID ); $this->set_object_read( false ); $this->data_store->read( $this ); } /** * Cancel the order and restore the cart (before payment). * @deprecated 3.0.0 Moved to event handler. * @param string $note (default: '') Optional note to add. */ public function cancel_order( $note = '' ) { wc_deprecated_function( 'WC_Order::cancel_order', '3.0', 'WC_Order::update_status' ); WC()->session->set( 'order_awaiting_payment', false ); $this->update_status( 'cancelled', $note ); } /** * Record sales. * @deprecated 3.0.0 */ public function record_product_sales() { wc_deprecated_function( 'WC_Order::record_product_sales', '3.0', 'wc_update_total_sales_counts' ); wc_update_total_sales_counts( $this->get_id() ); } /** * Increase applied coupon counts. * @deprecated 3.0.0 */ public function increase_coupon_usage_counts() { wc_deprecated_function( 'WC_Order::increase_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' ); wc_update_coupon_usage_counts( $this->get_id() ); } /** * Decrease applied coupon counts. * @deprecated 3.0.0 */ public function decrease_coupon_usage_counts() { wc_deprecated_function( 'WC_Order::decrease_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' ); wc_update_coupon_usage_counts( $this->get_id() ); } /** * Reduce stock levels for all line items in the order. * @deprecated 3.0.0 */ public function reduce_order_stock() { wc_deprecated_function( 'WC_Order::reduce_order_stock', '3.0', 'wc_reduce_stock_levels' ); wc_reduce_stock_levels( $this->get_id() ); } /** * Send the stock notifications. * @deprecated 3.0.0 No longer needs to be called directly. * * @param $product * @param $new_stock * @param $qty_ordered */ public function send_stock_notifications( $product, $new_stock, $qty_ordered ) { wc_deprecated_function( 'WC_Order::send_stock_notifications', '3.0' ); } /** * Output items for display in html emails. * @deprecated 3.0.0 Moved to template functions. * @param array $args Items args. * @return string */ public function email_order_items_table( $args = array() ) { wc_deprecated_function( 'WC_Order::email_order_items_table', '3.0', 'wc_get_email_order_items' ); return wc_get_email_order_items( $this, $args ); } /** * Get currency. * @deprecated 3.0.0 */ public function get_order_currency() { wc_deprecated_function( 'WC_Order::get_order_currency', '3.0', 'WC_Order::get_currency' ); return apply_filters( 'woocommerce_get_order_currency', $this->get_currency(), $this ); } } includes/legacy/class-wc-legacy-webhook.php 0000644 00000005010 15132754524 0014745 0 ustar 00 <?php /** * Legacy Webhook * * Legacy and deprecated functions are here to keep the WC_Legacy_Webhook class clean. * This class will be removed in future versions. * * @version 3.2.0 * @package WooCommerce\Classes * @category Class * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Webhook class. */ abstract class WC_Legacy_Webhook extends WC_Data { /** * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. * * @param string $key Item to check. * @return bool */ public function __isset( $key ) { $legacy_keys = array( 'id', 'status', 'post_data', 'delivery_url', 'secret', 'topic', 'hooks', 'resource', 'event', 'failure_count', 'api_version', ); if ( in_array( $key, $legacy_keys, true ) ) { return true; } return false; } /** * Magic __get method for backwards compatibility. Maps legacy vars to new getters. * * @param string $key Item to get. * @return mixed */ public function __get( $key ) { wc_doing_it_wrong( $key, 'Webhook properties should not be accessed directly.', '3.2' ); switch ( $key ) { case 'id' : $value = $this->get_id(); break; case 'status' : $value = $this->get_status(); break; case 'post_data' : $value = null; break; case 'delivery_url' : $value = $this->get_delivery_url(); break; case 'secret' : $value = $this->get_secret(); break; case 'topic' : $value = $this->get_topic(); break; case 'hooks' : $value = $this->get_hooks(); break; case 'resource' : $value = $this->get_resource(); break; case 'event' : $value = $this->get_event(); break; case 'failure_count' : $value = $this->get_failure_count(); break; case 'api_version' : $value = $this->get_api_version(); break; default : $value = ''; break; } // End switch(). return $value; } /** * Get the post data for the webhook. * * @deprecated 3.2.0 * @since 2.2 * @return null|WP_Post */ public function get_post_data() { wc_deprecated_function( 'WC_Webhook::get_post_data', '3.2' ); return null; } /** * Update the webhook status. * * @deprecated 3.2.0 * @since 2.2.0 * @param string $status Status to set. */ public function update_status( $status ) { wc_deprecated_function( 'WC_Webhook::update_status', '3.2', 'WC_Webhook::set_status' ); $this->set_status( $status ); $this->save(); } } includes/legacy/class-wc-legacy-shipping-zone.php 0000644 00000003107 15132754524 0016106 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Shipping Zone. * * @version 3.0.0 * @package WooCommerce\Classes * @category Class * @author WooThemes */ abstract class WC_Legacy_Shipping_Zone extends WC_Data { /** * Get zone ID * @return int|null Null if the zone does not exist. 0 is the default zone. * @deprecated 3.0 */ public function get_zone_id() { wc_deprecated_function( 'WC_Shipping_Zone::get_zone_id', '3.0', 'WC_Shipping_Zone::get_id' ); return $this->get_id(); } /** * Read a shipping zone by ID. * @deprecated 3.0.0 - Init a shipping zone with an ID. * * @param int $zone_id */ public function read( $zone_id ) { wc_deprecated_function( 'WC_Shipping_Zone::read', '3.0', 'a shipping zone initialized with an ID.' ); $this->set_id( $zone_id ); $data_store = WC_Data_Store::load( 'shipping-zone' ); $data_store->read( $this ); } /** * Update a zone. * @deprecated 3.0.0 - Use ::save instead. */ public function update() { wc_deprecated_function( 'WC_Shipping_Zone::update', '3.0', 'WC_Shipping_Zone::save instead.' ); $data_store = WC_Data_Store::load( 'shipping-zone' ); try { $data_store->update( $this ); } catch ( Exception $e ) { return false; } } /** * Create a zone. * @deprecated 3.0.0 - Use ::save instead. */ public function create() { wc_deprecated_function( 'WC_Shipping_Zone::create', '3.0', 'WC_Shipping_Zone::save instead.' ); $data_store = WC_Data_Store::load( 'shipping-zone' ); try { $data_store->create( $this ); } catch ( Exception $e ) { return false; } } } includes/legacy/class-wc-legacy-coupon.php 0000644 00000011631 15132754524 0014620 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy Coupon. * * Legacy and deprecated functions are here to keep the WC_Legacy_Coupon class clean. * This class will be removed in future versions. * * @class WC_Legacy_Coupon * @version 3.0.0 * @package WooCommerce\Classes * @category Class * @author WooThemes */ abstract class WC_Legacy_Coupon extends WC_Data { /** * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. * @param string $key * @return bool */ public function __isset( $key ) { $legacy_keys = array( 'id', 'exists', 'coupon_custom_fields', 'type', 'discount_type', 'amount', 'coupon_amount', 'code', 'individual_use', 'product_ids', 'exclude_product_ids', 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'usage_count', 'expiry_date', 'product_categories', 'exclude_product_categories', 'minimum_amount', 'maximum_amount', 'customer_email', ); if ( in_array( $key, $legacy_keys ) ) { return true; } return false; } /** * Magic __get method for backwards compatibility. Maps legacy vars to new getters. * @param string $key * @return mixed */ public function __get( $key ) { wc_doing_it_wrong( $key, 'Coupon properties should not be accessed directly.', '3.0' ); switch ( $key ) { case 'id' : $value = $this->get_id(); break; case 'exists' : $value = $this->get_id() > 0; break; case 'coupon_custom_fields' : $legacy_custom_fields = array(); $custom_fields = $this->get_id() ? $this->get_meta_data() : array(); if ( ! empty( $custom_fields ) ) { foreach ( $custom_fields as $cf_value ) { // legacy only supports 1 key $legacy_custom_fields[ $cf_value->key ][0] = $cf_value->value; } } $value = $legacy_custom_fields; break; case 'type' : case 'discount_type' : $value = $this->get_discount_type(); break; case 'amount' : case 'coupon_amount' : $value = $this->get_amount(); break; case 'code' : $value = $this->get_code(); break; case 'individual_use' : $value = ( true === $this->get_individual_use() ) ? 'yes' : 'no'; break; case 'product_ids' : $value = $this->get_product_ids(); break; case 'exclude_product_ids' : $value = $this->get_excluded_product_ids(); break; case 'usage_limit' : $value = $this->get_usage_limit(); break; case 'usage_limit_per_user' : $value = $this->get_usage_limit_per_user(); break; case 'limit_usage_to_x_items' : $value = $this->get_limit_usage_to_x_items(); break; case 'usage_count' : $value = $this->get_usage_count(); break; case 'expiry_date' : $value = ( $this->get_date_expires() ? $this->get_date_expires()->date( 'Y-m-d' ) : '' ); break; case 'product_categories' : $value = $this->get_product_categories(); break; case 'exclude_product_categories' : $value = $this->get_excluded_product_categories(); break; case 'minimum_amount' : $value = $this->get_minimum_amount(); break; case 'maximum_amount' : $value = $this->get_maximum_amount(); break; case 'customer_email' : $value = $this->get_email_restrictions(); break; default : $value = ''; break; } return $value; } /** * Format loaded data as array. * @param string|array $array * @return array */ public function format_array( $array ) { wc_deprecated_function( 'WC_Coupon::format_array', '3.0' ); if ( ! is_array( $array ) ) { if ( is_serialized( $array ) ) { $array = maybe_unserialize( $array ); } else { $array = explode( ',', $array ); } } return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) ); } /** * Check if coupon needs applying before tax. * * @return bool */ public function apply_before_tax() { wc_deprecated_function( 'WC_Coupon::apply_before_tax', '3.0' ); return true; } /** * Check if a coupon enables free shipping. * * @return bool */ public function enable_free_shipping() { wc_deprecated_function( 'WC_Coupon::enable_free_shipping', '3.0', 'WC_Coupon::get_free_shipping' ); return $this->get_free_shipping(); } /** * Check if a coupon excludes sale items. * * @return bool */ public function exclude_sale_items() { wc_deprecated_function( 'WC_Coupon::exclude_sale_items', '3.0', 'WC_Coupon::get_exclude_sale_items' ); return $this->get_exclude_sale_items(); } /** * Increase usage count for current coupon. * * @param string $used_by Either user ID or billing email */ public function inc_usage_count( $used_by = '' ) { $this->increase_usage_count( $used_by ); } /** * Decrease usage count for current coupon. * * @param string $used_by Either user ID or billing email */ public function dcr_usage_count( $used_by = '' ) { $this->decrease_usage_count( $used_by ); } } includes/legacy/class-wc-legacy-cart.php 0000644 00000030561 15132754524 0014251 0 ustar 00 <?php /** * Legacy cart * * Legacy and deprecated functions are here to keep the WC_Cart class clean. * This class will be removed in future versions. * * @version 3.2.0 * @package WooCommerce\Classes * @category Class * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Legacy cart class. */ abstract class WC_Legacy_Cart { /** * Array of defaults. Not used since 3.2. * * @deprecated 3.2.0 */ public $cart_session_data = array( 'cart_contents_total' => 0, 'total' => 0, 'subtotal' => 0, 'subtotal_ex_tax' => 0, 'tax_total' => 0, 'taxes' => array(), 'shipping_taxes' => array(), 'discount_cart' => 0, 'discount_cart_tax' => 0, 'shipping_total' => 0, 'shipping_tax_total' => 0, 'coupon_discount_amounts' => array(), 'coupon_discount_tax_amounts' => array(), 'fee_total' => 0, 'fees' => array(), ); /** * Contains an array of coupon usage counts after they have been applied. * * @deprecated 3.2.0 * @var array */ public $coupon_applied_count = array(); /** * Map legacy variables. * * @param string $name Property name. * @param mixed $value Value to set. */ public function __isset( $name ) { $legacy_keys = array_merge( array( 'dp', 'prices_include_tax', 'round_at_subtotal', 'cart_contents_total', 'total', 'subtotal', 'subtotal_ex_tax', 'tax_total', 'fee_total', 'discount_cart', 'discount_cart_tax', 'shipping_total', 'shipping_tax_total', 'display_totals_ex_tax', 'display_cart_ex_tax', 'cart_contents_weight', 'cart_contents_count', 'coupons', 'taxes', 'shipping_taxes', 'coupon_discount_amounts', 'coupon_discount_tax_amounts', 'fees', 'tax', 'discount_total', 'tax_display_cart', ), is_array( $this->cart_session_data ) ? array_keys( $this->cart_session_data ) : array() ); if ( in_array( $name, $legacy_keys, true ) ) { return true; } return false; } /** * Magic getters. * * If you add/remove cases here please update $legacy_keys in __isset accordingly. * * @param string $name Property name. * @return mixed */ public function &__get( $name ) { $value = ''; switch ( $name ) { case 'dp' : $value = wc_get_price_decimals(); break; case 'prices_include_tax' : $value = wc_prices_include_tax(); break; case 'round_at_subtotal' : $value = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); break; case 'cart_contents_total' : $value = $this->get_cart_contents_total(); break; case 'total' : $value = $this->get_total( 'edit' ); break; case 'subtotal' : $value = $this->get_subtotal() + $this->get_subtotal_tax(); break; case 'subtotal_ex_tax' : $value = $this->get_subtotal(); break; case 'tax_total' : $value = $this->get_fee_tax() + $this->get_cart_contents_tax(); break; case 'fee_total' : $value = $this->get_fee_total(); break; case 'discount_cart' : $value = $this->get_discount_total(); break; case 'discount_cart_tax' : $value = $this->get_discount_tax(); break; case 'shipping_total' : $value = $this->get_shipping_total(); break; case 'shipping_tax_total' : $value = $this->get_shipping_tax(); break; case 'display_totals_ex_tax' : case 'display_cart_ex_tax' : $value = ! $this->display_prices_including_tax(); break; case 'cart_contents_weight' : $value = $this->get_cart_contents_weight(); break; case 'cart_contents_count' : $value = $this->get_cart_contents_count(); break; case 'coupons' : $value = $this->get_coupons(); break; // Arrays returned by reference to allow modification without notices. TODO: Remove in 4.0. case 'taxes' : wc_deprecated_function( 'WC_Cart->taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_cart_contents_taxes()', 'WC_Cart::set_cart_contents_taxes()' ) ); $value = &$this->totals[ 'cart_contents_taxes' ]; break; case 'shipping_taxes' : wc_deprecated_function( 'WC_Cart->shipping_taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_shipping_taxes()', 'WC_Cart::set_shipping_taxes()' ) ); $value = &$this->totals[ 'shipping_taxes' ]; break; case 'coupon_discount_amounts' : $value = &$this->coupon_discount_totals; break; case 'coupon_discount_tax_amounts' : $value = &$this->coupon_discount_tax_totals; break; case 'fees' : wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::get_fees' ) ); // Grab fees from the new API. $new_fees = $this->fees_api()->get_fees(); // Add new fees to the legacy prop so it can be adjusted via legacy property. $this->fees = $new_fees; // Return by reference. $value = &$this->fees; break; // Deprecated args. TODO: Remove in 4.0. case 'tax' : wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax directly' ); $this->tax = new WC_Tax(); $value = $this->tax; break; case 'discount_total': wc_deprecated_argument( 'WC_Cart->discount_total', '2.3', 'After tax coupons are no longer supported. For more information see: https://woocommerce.wordpress.com/2014/12/upcoming-coupon-changes-in-woocommerce-2-3/' ); $value = 0; break; case 'tax_display_cart': wc_deprecated_argument( 'WC_Cart->tax_display_cart', '4.4', 'Use WC_Cart->get_tax_price_display_mode() instead.' ); $value = $this->get_tax_price_display_mode(); break; } return $value; } /** * Map legacy variables to setters. * * @param string $name Property name. * @param mixed $value Value to set. */ public function __set( $name, $value ) { switch ( $name ) { case 'cart_contents_total' : $this->set_cart_contents_total( $value ); break; case 'total' : $this->set_total( $value ); break; case 'subtotal' : $this->set_subtotal( $value ); break; case 'subtotal_ex_tax' : $this->set_subtotal( $value ); break; case 'tax_total' : $this->set_cart_contents_tax( $value ); $this->set_fee_tax( 0 ); break; case 'taxes' : $this->set_cart_contents_taxes( $value ); break; case 'shipping_taxes' : $this->set_shipping_taxes( $value ); break; case 'fee_total' : $this->set_fee_total( $value ); break; case 'discount_cart' : $this->set_discount_total( $value ); break; case 'discount_cart_tax' : $this->set_discount_tax( $value ); break; case 'shipping_total' : $this->set_shipping_total( $value ); break; case 'shipping_tax_total' : $this->set_shipping_tax( $value ); break; case 'coupon_discount_amounts' : $this->set_coupon_discount_totals( $value ); break; case 'coupon_discount_tax_amounts' : $this->set_coupon_discount_tax_totals( $value ); break; case 'fees' : wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::add_fee' ) ); $this->fees = $value; break; default : $this->$name = $value; break; } } /** * Methods moved to session class in 3.2.0. */ public function get_cart_from_session() { $this->session->get_cart_from_session(); } public function maybe_set_cart_cookies() { $this->session->maybe_set_cart_cookies(); } public function set_session() { $this->session->set_session(); } public function get_cart_for_session() { return $this->session->get_cart_for_session(); } public function persistent_cart_update() { $this->session->persistent_cart_update(); } public function persistent_cart_destroy() { $this->session->persistent_cart_destroy(); } /** * Get the total of all cart discounts. * * @return float */ public function get_cart_discount_total() { return $this->get_discount_total(); } /** * Get the total of all cart tax discounts (used for discounts on tax inclusive prices). * * @return float */ public function get_cart_discount_tax_total() { return $this->get_discount_tax(); } /** * Renamed for consistency. * * @param string $coupon_code * @return bool True if the coupon is applied, false if it does not exist or cannot be applied. */ public function add_discount( $coupon_code ) { return $this->apply_coupon( $coupon_code ); } /** * Remove taxes. * * @deprecated 3.2.0 Taxes are never calculated if customer is tax except making this function unused. */ public function remove_taxes() { wc_deprecated_function( 'WC_Cart::remove_taxes', '3.2', '' ); } /** * Init. * * @deprecated 3.2.0 Session is loaded via hooks rather than directly. */ public function init() { wc_deprecated_function( 'WC_Cart::init', '3.2', '' ); $this->get_cart_from_session(); } /** * Function to apply discounts to a product and get the discounted price (before tax is applied). * * @deprecated 3.2.0 Calculation and coupon logic is handled in WC_Cart_Totals. * @param mixed $values Cart item. * @param mixed $price Price of item. * @param bool $add_totals Legacy. * @return float price */ public function get_discounted_price( $values, $price, $add_totals = false ) { wc_deprecated_function( 'WC_Cart::get_discounted_price', '3.2', '' ); $cart_item_key = $values['key']; $cart_item = $this->cart_contents[ $cart_item_key ]; return $cart_item['line_total']; } /** * Gets the url to the cart page. * * @deprecated 2.5.0 in favor to wc_get_cart_url() * @return string url to page */ public function get_cart_url() { wc_deprecated_function( 'WC_Cart::get_cart_url', '2.5', 'wc_get_cart_url' ); return wc_get_cart_url(); } /** * Gets the url to the checkout page. * * @deprecated 2.5.0 in favor to wc_get_checkout_url() * @return string url to page */ public function get_checkout_url() { wc_deprecated_function( 'WC_Cart::get_checkout_url', '2.5', 'wc_get_checkout_url' ); return wc_get_checkout_url(); } /** * Sees if we need a shipping address. * * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only() * @return bool */ public function ship_to_billing_address_only() { wc_deprecated_function( 'WC_Cart::ship_to_billing_address_only', '2.5', 'wc_ship_to_billing_address_only' ); return wc_ship_to_billing_address_only(); } /** * Coupons enabled function. Filterable. * * @deprecated 2.5.0 * @return bool */ public function coupons_enabled() { wc_deprecated_function( 'WC_Legacy_Cart::coupons_enabled', '2.5.0', 'wc_coupons_enabled' ); return wc_coupons_enabled(); } /** * Gets the total (product) discount amount - these are applied before tax. * * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required. * @return mixed formatted price or false if there are none. */ public function get_discounts_before_tax() { wc_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' ); if ( $this->get_cart_discount_total() ) { $discounts_before_tax = wc_price( $this->get_cart_discount_total() ); } else { $discounts_before_tax = false; } return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this ); } /** * Get the total of all order discounts (after tax discounts). * * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3. * @return int */ public function get_order_discount_total() { wc_deprecated_function( 'get_order_discount_total', '2.3' ); return 0; } /** * Function to apply cart discounts after tax. * * @deprecated 2.3.0 Coupons can not be applied after tax. * @param $values * @param $price */ public function apply_cart_discounts_after_tax( $values, $price ) { wc_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' ); } /** * Function to apply product discounts after tax. * * @deprecated 2.3.0 Coupons can not be applied after tax. * * @param $values * @param $price */ public function apply_product_discounts_after_tax( $values, $price ) { wc_deprecated_function( 'apply_product_discounts_after_tax', '2.3' ); } /** * Gets the order discount amount - these are applied after tax. * * @deprecated 2.3.0 Coupons can not be applied after tax. */ public function get_discounts_after_tax() { wc_deprecated_function( 'get_discounts_after_tax', '2.3' ); } } includes/class-wc-background-emailer.php 0000644 00000011115 15132754524 0014337 0 ustar 00 <?php /** * Background Emailer * * @version 3.0.1 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Background_Process', false ) ) { include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php'; } /** * WC_Background_Emailer Class. */ class WC_Background_Emailer extends WC_Background_Process { /** * Initiate new background process. */ public function __construct() { // Uses unique prefix per blog so each blog has separate queue. $this->prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_emailer'; // Dispatch queue after shutdown. add_action( 'shutdown', array( $this, 'dispatch_queue' ), 100 ); parent::__construct(); } /** * Schedule fallback event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param array $callback Update callback function. * @return mixed */ protected function task( $callback ) { if ( isset( $callback['filter'], $callback['args'] ) ) { try { WC_Emails::send_queued_transactional_email( $callback['filter'], $callback['args'] ); } catch ( Exception $e ) { if ( Constants::is_true( 'WP_DEBUG' ) ) { trigger_error( 'Transactional email triggered fatal error for callback ' . esc_html( $callback['filter'] ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } } } return false; } /** * Finishes replying to the client, but keeps the process running for further (async) code execution. * * @see https://core.trac.wordpress.org/ticket/41358 . */ protected function close_http_connection() { // Only 1 PHP process can access a session object at a time, close this so the next request isn't kept waiting. // @codingStandardsIgnoreStart if ( session_id() ) { session_write_close(); } // @codingStandardsIgnoreEnd wc_set_time_limit( 0 ); // fastcgi_finish_request is the cleanest way to send the response and keep the script running, but not every server has it. if ( is_callable( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } else { // Fallback: send headers and flush buffers. if ( ! headers_sent() ) { header( 'Connection: close' ); } @ob_end_flush(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged flush(); } } /** * Save and run queue. */ public function dispatch_queue() { if ( ! empty( $this->data ) ) { $this->close_http_connection(); $this->save()->dispatch(); } } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } // Pass cookies through with the request so nonces function. $cookies = array(); foreach ( $_COOKIE as $name => $value ) { // WPCS: input var ok. if ( 'PHPSESSID' === $name ) { continue; } $cookies[] = new WP_Http_Cookie( array( 'name' => $name, 'value' => $value, ) ); } return array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $cookies, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); } /** * Handle * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); if ( empty( $batch->data ) ) { break; } foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } // Update batch before sending more to prevent duplicate email possibility. $this->update( $batch->key, $batch->data ); if ( $this->time_exceeded() || $this->memory_exceeded() ) { // Batch limits reached. break; } } if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } } } includes/class-wc-frontend-scripts.php 0000644 00000063643 15132754524 0014125 0 ustar 00 <?php /** * Handle frontend scripts * * @package WooCommerce\Classes * @version 2.3.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Frontend scripts class. */ class WC_Frontend_Scripts { /** * Contains an array of script handles registered by WC. * * @var array */ private static $scripts = array(); /** * Contains an array of script handles registered by WC. * * @var array */ private static $styles = array(); /** * Contains an array of script handles localized by WC. * * @var array */ private static $wp_localize_scripts = array(); /** * Hook in methods. */ public static function init() { add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) ); add_action( 'wp_print_scripts', array( __CLASS__, 'localize_printed_scripts' ), 5 ); add_action( 'wp_print_footer_scripts', array( __CLASS__, 'localize_printed_scripts' ), 5 ); } /** * Get styles for the frontend. * * @return array */ public static function get_styles() { $version = Constants::get_constant( 'WC_VERSION' ); return apply_filters( 'woocommerce_enqueue_styles', array( 'woocommerce-layout' => array( 'src' => self::get_asset_url( 'assets/css/woocommerce-layout.css' ), 'deps' => '', 'version' => $version, 'media' => 'all', 'has_rtl' => true, ), 'woocommerce-smallscreen' => array( 'src' => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ), 'deps' => 'woocommerce-layout', 'version' => $version, 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', '768px' ) . ')', 'has_rtl' => true, ), 'woocommerce-general' => array( 'src' => self::get_asset_url( 'assets/css/woocommerce.css' ), 'deps' => '', 'version' => $version, 'media' => 'all', 'has_rtl' => true, ), ) ); } /** * Return asset URL. * * @param string $path Assets path. * @return string */ private static function get_asset_url( $path ) { return apply_filters( 'woocommerce_get_asset_url', plugins_url( $path, WC_PLUGIN_FILE ), $path ); } /** * Register a script for use. * * @uses wp_register_script() * @param string $handle Name of the script. Should be unique. * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. * @param string[] $deps An array of registered script handles this script depends on. * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param boolean $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'. */ private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { self::$scripts[] = $handle; wp_register_script( $handle, $path, $deps, $version, $in_footer ); } /** * Register and enqueue a script for use. * * @uses wp_enqueue_script() * @param string $handle Name of the script. Should be unique. * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. * @param string[] $deps An array of registered script handles this script depends on. * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param boolean $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'. */ private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { if ( ! in_array( $handle, self::$scripts, true ) && $path ) { self::register_script( $handle, $path, $deps, $version, $in_footer ); } wp_enqueue_script( $handle ); } /** * Register a style for use. * * @uses wp_register_style() * @param string $handle Name of the stylesheet. Should be unique. * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $has_rtl If has RTL version to load too. */ private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { self::$styles[] = $handle; wp_register_style( $handle, $path, $deps, $version, $media ); if ( $has_rtl ) { wp_style_add_data( $handle, 'rtl', 'replace' ); } } /** * Register and enqueue a styles for use. * * @uses wp_enqueue_style() * @param string $handle Name of the stylesheet. Should be unique. * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $has_rtl If has RTL version to load too. */ private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { if ( ! in_array( $handle, self::$styles, true ) && $path ) { self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); } wp_enqueue_style( $handle ); } /** * Register all WC scripts. */ private static function register_scripts() { $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); $register_scripts = array( 'flexslider' => array( 'src' => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '2.7.2-wc.' . $version, ), 'js-cookie' => array( 'src' => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ), 'deps' => array(), 'version' => '2.1.4-wc.' . $version, ), 'jquery-blockui' => array( 'src' => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '2.7.0-wc.' . $version, ), 'jquery-cookie' => array( // deprecated. 'src' => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '1.4.1-wc.' . $version, ), 'jquery-payment' => array( 'src' => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '3.0.0-wc.' . $version, ), 'photoswipe' => array( 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ), 'deps' => array(), 'version' => '4.1.1-wc.' . $version, ), 'photoswipe-ui-default' => array( 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ), 'deps' => array( 'photoswipe' ), 'version' => '4.1.1-wc.' . $version, ), 'prettyPhoto' => array( // deprecated. 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '3.1.6-wc.' . $version, ), 'prettyPhoto-init' => array( // deprecated. 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ), 'deps' => array( 'jquery', 'prettyPhoto' ), 'version' => $version, ), 'select2' => array( 'src' => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '4.0.3-wc.' . $version, ), 'selectWoo' => array( 'src' => self::get_asset_url( 'assets/js/selectWoo/selectWoo.full' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '1.0.9-wc.' . $version, ), 'wc-address-i18n' => array( 'src' => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ), 'deps' => array( 'jquery', 'wc-country-select' ), 'version' => $version, ), 'wc-add-payment-method' => array( 'src' => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ), 'deps' => array( 'jquery', 'woocommerce' ), 'version' => $version, ), 'wc-cart' => array( 'src' => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ), 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), 'version' => $version, ), 'wc-cart-fragments' => array( 'src' => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ), 'deps' => array( 'jquery', 'js-cookie' ), 'version' => $version, ), 'wc-checkout' => array( 'src' => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ), 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), 'version' => $version, ), 'wc-country-select' => array( 'src' => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => $version, ), 'wc-credit-card-form' => array( 'src' => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ), 'deps' => array( 'jquery', 'jquery-payment' ), 'version' => $version, ), 'wc-add-to-cart' => array( 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ), 'deps' => array( 'jquery', 'jquery-blockui' ), 'version' => $version, ), 'wc-add-to-cart-variation' => array( 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ), 'deps' => array( 'jquery', 'wp-util', 'jquery-blockui' ), 'version' => $version, ), 'wc-geolocation' => array( 'src' => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => $version, ), 'wc-lost-password' => array( 'src' => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ), 'deps' => array( 'jquery', 'woocommerce' ), 'version' => $version, ), 'wc-password-strength-meter' => array( 'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ), 'deps' => array( 'jquery', 'password-strength-meter' ), 'version' => $version, ), 'wc-single-product' => array( 'src' => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => $version, ), 'woocommerce' => array( 'src' => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ), 'deps' => array( 'jquery', 'jquery-blockui', 'js-cookie' ), 'version' => $version, ), 'zoom' => array( 'src' => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ), 'deps' => array( 'jquery' ), 'version' => '1.7.21-wc.' . $version, ), ); foreach ( $register_scripts as $name => $props ) { self::register_script( $name, $props['src'], $props['deps'], $props['version'] ); } } /** * Register all WC styles. */ private static function register_styles() { $version = Constants::get_constant( 'WC_VERSION' ); $register_styles = array( 'photoswipe' => array( 'src' => self::get_asset_url( 'assets/css/photoswipe/photoswipe.min.css' ), 'deps' => array(), 'version' => $version, 'has_rtl' => false, ), 'photoswipe-default-skin' => array( 'src' => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.min.css' ), 'deps' => array( 'photoswipe' ), 'version' => $version, 'has_rtl' => false, ), 'select2' => array( 'src' => self::get_asset_url( 'assets/css/select2.css' ), 'deps' => array(), 'version' => $version, 'has_rtl' => false, ), 'woocommerce_prettyPhoto_css' => array( // deprecated. 'src' => self::get_asset_url( 'assets/css/prettyPhoto.css' ), 'deps' => array(), 'version' => $version, 'has_rtl' => true, ), ); foreach ( $register_styles as $name => $props ) { self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] ); } } /** * Register/queue frontend scripts. */ public static function load_scripts() { global $post; if ( ! did_action( 'before_woocommerce_init' ) ) { return; } self::register_scripts(); self::register_styles(); if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { self::enqueue_script( 'wc-add-to-cart' ); } if ( is_cart() ) { self::enqueue_script( 'wc-cart' ); } if ( is_cart() || is_checkout() || is_account_page() ) { self::enqueue_script( 'selectWoo' ); self::enqueue_style( 'select2' ); // Password strength meter. Load in checkout, account login and edit account page. if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) { self::enqueue_script( 'wc-password-strength-meter' ); } } if ( is_checkout() ) { self::enqueue_script( 'wc-checkout' ); } if ( is_add_payment_method_page() ) { self::enqueue_script( 'wc-add-payment-method' ); } if ( is_lost_password_page() ) { self::enqueue_script( 'wc-lost-password' ); } // Load gallery scripts on product pages only if supported. if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) { if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { self::enqueue_script( 'zoom' ); } if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { self::enqueue_script( 'flexslider' ); } if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { self::enqueue_script( 'photoswipe-ui-default' ); self::enqueue_style( 'photoswipe-default-skin' ); add_action( 'wp_footer', 'woocommerce_photoswipe' ); } self::enqueue_script( 'wc-single-product' ); } // Only enqueue the geolocation script if the Default Current Address is set to "Geolocate // (with Page Caching Support) and outside of the cart, checkout, account and customizer preview. if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) && ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) ) { $ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent. if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { self::enqueue_script( 'wc-geolocation' ); } } // Global frontend scripts. self::enqueue_script( 'woocommerce' ); self::enqueue_script( 'wc-cart-fragments' ); // CSS Styles. $enqueue_styles = self::get_styles(); if ( $enqueue_styles ) { foreach ( $enqueue_styles as $handle => $args ) { if ( ! isset( $args['has_rtl'] ) ) { $args['has_rtl'] = false; } self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] ); } } // Placeholder style. wp_register_style( 'woocommerce-inline', false ); // phpcs:ignore wp_enqueue_style( 'woocommerce-inline' ); if ( true === wc_string_to_bool( get_option( 'woocommerce_checkout_highlight_required_fields', 'yes' ) ) ) { wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: visible; }' ); } else { wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: hidden; }' ); } } /** * Localize a WC script once. * * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0. * @param string $handle Script handle the data will be attached to. */ private static function localize_script( $handle ) { if ( ! in_array( $handle, self::$wp_localize_scripts, true ) && wp_script_is( $handle ) ) { $data = self::get_script_data( $handle ); if ( ! $data ) { return; } $name = str_replace( '-', '_', $handle ) . '_params'; self::$wp_localize_scripts[] = $handle; wp_localize_script( $handle, $name, apply_filters( $name, $data ) ); } } /** * Return data for script handles. * * @param string $handle Script handle the data will be attached to. * @return array|bool */ private static function get_script_data( $handle ) { global $wp; switch ( $handle ) { case 'woocommerce': $params = array( 'ajax_url' => WC()->ajax_url(), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), ); break; case 'wc-geolocation': $params = array( 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'home_url' => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility. ); break; case 'wc-single-product': $params = array( 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), 'review_rating_required' => wc_review_ratings_required() ? 'yes' : 'no', 'flexslider' => apply_filters( 'woocommerce_single_product_carousel_options', array( 'rtl' => is_rtl(), 'animation' => 'slide', 'smoothHeight' => true, 'directionNav' => false, 'controlNav' => 'thumbnails', 'slideshow' => false, 'animationSpeed' => 500, 'animationLoop' => false, // Breaks photoswipe pagination if true. 'allowOneSlide' => false, ) ), 'zoom_enabled' => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ), 'zoom_options' => apply_filters( 'woocommerce_single_product_zoom_options', array() ), 'photoswipe_enabled' => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ), 'photoswipe_options' => apply_filters( 'woocommerce_single_product_photoswipe_options', array( 'shareEl' => false, 'closeOnScroll' => false, 'history' => false, 'hideAnimationDuration' => 0, 'showAnimationDuration' => 0, ) ), 'flexslider_enabled' => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ), ); break; case 'wc-checkout': $params = array( 'ajax_url' => WC()->ajax_url(), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ), 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), 'checkout_url' => WC_AJAX::get_endpoint( 'checkout' ), 'is_checkout' => is_checkout() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0, 'debug_mode' => Constants::is_true( 'WP_DEBUG' ), 'i18n_checkout_error' => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ), ); break; case 'wc-address-i18n': $params = array( 'locale' => wp_json_encode( WC()->countries->get_country_locale() ), 'locale_fields' => wp_json_encode( WC()->countries->get_country_locale_field_selectors() ), 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), 'i18n_optional_text' => esc_html__( 'optional', 'woocommerce' ), ); break; case 'wc-cart': $params = array( 'ajax_url' => WC()->ajax_url(), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'update_shipping_method_nonce' => wp_create_nonce( 'update-shipping-method' ), 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), ); break; case 'wc-cart-fragments': $params = array( 'ajax_url' => WC()->ajax_url(), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'cart_hash_key' => apply_filters( 'woocommerce_cart_hash_key', 'wc_cart_hash_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), 'request_timeout' => 5000, ); break; case 'wc-add-to-cart': $params = array( 'ajax_url' => WC()->ajax_url(), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'i18n_view_cart' => esc_attr__( 'View cart', 'woocommerce' ), 'cart_url' => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url(), null ), 'is_cart' => is_cart(), 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ), ); break; case 'wc-add-to-cart-variation': // We also need the wp.template for this script :). wc_get_template( 'single-product/add-to-cart/variation.php' ); $params = array( 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), 'i18n_make_a_selection_text' => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ), 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), ); break; case 'wc-country-select': $params = array( 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), ); break; case 'wc-password-strength-meter': $params = array( 'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ), 'stop_checkout' => apply_filters( 'woocommerce_enforce_password_strength_meter_on_checkout', false ), 'i18n_password_error' => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ), 'i18n_password_hint' => esc_attr( wp_get_password_hint() ), ); break; default: $params = false; } $params = apply_filters_deprecated( $handle . '_params', array( $params ), '3.0.0', 'woocommerce_get_script_data' ); return apply_filters( 'woocommerce_get_script_data', $params, $handle ); } /** * Localize scripts only when enqueued. */ public static function localize_printed_scripts() { foreach ( self::$scripts as $handle ) { self::localize_script( $handle ); } } } WC_Frontend_Scripts::init(); includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php 0000644 00000013371 15132754524 0024604 0 ustar 00 <?php /** * Class WC_Shipping_Legacy_Local_Delivery file. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Local Delivery Shipping Method. * * This class is here for backwards compatibility for methods existing before zones existed. * * @deprecated 2.6.0 * @version 2.3.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Legacy_Local_Delivery extends WC_Shipping_Local_Pickup { /** * Constructor. */ public function __construct() { $this->id = 'legacy_local_delivery'; $this->method_title = __( 'Local delivery (legacy)', 'woocommerce' ); /* translators: %s: Admin shipping settings URL */ $this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>'; $this->init(); } /** * Process and redirect if disabled. */ public function process_admin_options() { parent::process_admin_options(); if ( 'no' === $this->settings['enabled'] ) { wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); exit; } } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . 'local_delivery_settings'; } /** * Init function. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->type = $this->get_option( 'type' ); $this->fee = $this->get_option( 'fee' ); $this->type = $this->get_option( 'type' ); $this->codes = $this->get_option( 'codes' ); $this->availability = $this->get_option( 'availability' ); $this->countries = $this->get_option( 'countries' ); add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Calculate_shipping function. * * @param array $package (default: array()). */ public function calculate_shipping( $package = array() ) { $shipping_total = 0; switch ( $this->type ) { case 'fixed': $shipping_total = $this->fee; break; case 'percent': $shipping_total = $package['contents_cost'] * ( $this->fee / 100 ); break; case 'product': foreach ( $package['contents'] as $item_id => $values ) { if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { $shipping_total += $this->fee * $values['quantity']; } } break; } $rate = array( 'id' => $this->id, 'label' => $this->title, 'cost' => $shipping_total, 'package' => $package, ); $this->add_rate( $rate ); } /** * Init form fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Local delivery', 'woocommerce' ), 'desc_tip' => true, ), 'type' => array( 'title' => __( 'Fee type', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'description' => __( 'How to calculate delivery charges', 'woocommerce' ), 'default' => 'fixed', 'options' => array( 'fixed' => __( 'Fixed amount', 'woocommerce' ), 'percent' => __( 'Percentage of cart total', 'woocommerce' ), 'product' => __( 'Fixed amount per product', 'woocommerce' ), ), 'desc_tip' => true, ), 'fee' => array( 'title' => __( 'Delivery fee', 'woocommerce' ), 'type' => 'price', 'description' => __( 'What fee do you want to charge for local delivery, disregarded if you choose free. Leave blank to disable.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => wc_format_localized_price( 0 ), ), 'codes' => array( 'title' => __( 'Allowed ZIP/post codes', 'woocommerce' ), 'type' => 'text', 'desc_tip' => __( 'What ZIP/post codes are available for local delivery?', 'woocommerce' ), 'default' => '', 'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. <code>P*</code> will match a postcode of PE30. Also accepts a pattern, e.g. <code>NG1___</code> would match NG1 1AA but not NG10 1AA', 'woocommerce' ), 'placeholder' => 'e.g. 12345, 56789', ), 'availability' => array( 'title' => __( 'Method availability', 'woocommerce' ), 'type' => 'select', 'default' => 'all', 'class' => 'availability wc-enhanced-select', 'options' => array( 'all' => __( 'All allowed countries', 'woocommerce' ), 'specific' => __( 'Specific Countries', 'woocommerce' ), ), ), 'countries' => array( 'title' => __( 'Specific countries', 'woocommerce' ), 'type' => 'multiselect', 'class' => 'wc-enhanced-select', 'css' => 'width: 400px;', 'default' => '', 'options' => WC()->countries->get_shipping_countries(), 'custom_attributes' => array( 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), ), ), ); } } includes/shipping/local-pickup/class-wc-shipping-local-pickup.php 0000644 00000005367 15132754524 0021226 0 ustar 00 <?php /** * Class WC_Shipping_Local_Pickup file. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Local Pickup Shipping Method. * * A simple shipping method allowing free pickup as a shipping method. * * @class WC_Shipping_Local_Pickup * @version 2.6.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Local_Pickup extends WC_Shipping_Method { /** * Constructor. * * @param int $instance_id Instance ID. */ public function __construct( $instance_id = 0 ) { $this->id = 'local_pickup'; $this->instance_id = absint( $instance_id ); $this->method_title = __( 'Local pickup', 'woocommerce' ); $this->method_description = __( 'Allow customers to pick up orders themselves. By default, when using local pickup store base taxes will apply regardless of customer address.', 'woocommerce' ); $this->supports = array( 'shipping-zones', 'instance-settings', 'instance-settings-modal', ); $this->init(); } /** * Initialize local pickup. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->tax_status = $this->get_option( 'tax_status' ); $this->cost = $this->get_option( 'cost' ); // Actions. add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Calculate local pickup shipping. * * @param array $package Package information. */ public function calculate_shipping( $package = array() ) { $this->add_rate( array( 'label' => $this->title, 'package' => $package, 'cost' => $this->cost, ) ); } /** * Init form fields. */ public function init_form_fields() { $this->instance_form_fields = array( 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Local pickup', 'woocommerce' ), 'desc_tip' => true, ), 'tax_status' => array( 'title' => __( 'Tax status', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => 'taxable', 'options' => array( 'taxable' => __( 'Taxable', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ), ), 'cost' => array( 'title' => __( 'Cost', 'woocommerce' ), 'type' => 'text', 'placeholder' => '0', 'description' => __( 'Optional cost for local pickup.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, ), ); } } includes/shipping/free-shipping/class-wc-shipping-free-shipping.php 0000644 00000015710 15132754524 0021551 0 ustar 00 <?php /** * Class WC_Shipping_Free_Shipping file. * * @package WooCommerce\Shipping */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Free Shipping Method. * * A simple shipping method for free shipping. * * @class WC_Shipping_Free_Shipping * @version 2.6.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Free_Shipping extends WC_Shipping_Method { /** * Min amount to be valid. * * @var integer */ public $min_amount = 0; /** * Requires option. * * @var string */ public $requires = ''; /** * Constructor. * * @param int $instance_id Shipping method instance. */ public function __construct( $instance_id = 0 ) { $this->id = 'free_shipping'; $this->instance_id = absint( $instance_id ); $this->method_title = __( 'Free shipping', 'woocommerce' ); $this->method_description = __( 'Free shipping is a special method which can be triggered with coupons and minimum spends.', 'woocommerce' ); $this->supports = array( 'shipping-zones', 'instance-settings', 'instance-settings-modal', ); $this->init(); } /** * Initialize free shipping. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->min_amount = $this->get_option( 'min_amount', 0 ); $this->requires = $this->get_option( 'requires' ); $this->ignore_discounts = $this->get_option( 'ignore_discounts' ); // Actions. add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'admin_footer', array( 'WC_Shipping_Free_Shipping', 'enqueue_admin_js' ), 10 ); // Priority needs to be higher than wc_print_js (25). } /** * Init form fields. */ public function init_form_fields() { $this->instance_form_fields = array( 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => $this->method_title, 'desc_tip' => true, ), 'requires' => array( 'title' => __( 'Free shipping requires...', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => '', 'options' => array( '' => __( 'N/A', 'woocommerce' ), 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), ), ), 'min_amount' => array( 'title' => __( 'Minimum order amount', 'woocommerce' ), 'type' => 'price', 'placeholder' => wc_format_localized_price( 0 ), 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), 'default' => '0', 'desc_tip' => true, ), 'ignore_discounts' => array( 'title' => __( 'Coupons discounts', 'woocommerce' ), 'label' => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ), 'type' => 'checkbox', 'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ), 'default' => 'no', 'desc_tip' => true, ), ); } /** * Get setting form fields for instances of this shipping method within zones. * * @return array */ public function get_instance_form_fields() { return parent::get_instance_form_fields(); } /** * See if free shipping is available based on the package and cart. * * @param array $package Shipping package. * @return bool */ public function is_available( $package ) { $has_coupon = false; $has_met_min_amount = false; if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { $coupons = WC()->cart->get_coupons(); if ( $coupons ) { foreach ( $coupons as $code => $coupon ) { if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { $has_coupon = true; break; } } } } if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { $total = WC()->cart->get_displayed_subtotal(); if ( WC()->cart->display_prices_including_tax() ) { $total = $total - WC()->cart->get_discount_tax(); } if ( 'no' === $this->ignore_discounts ) { $total = $total - WC()->cart->get_discount_total(); } $total = NumberUtil::round( $total, wc_get_price_decimals() ); if ( $total >= $this->min_amount ) { $has_met_min_amount = true; } } switch ( $this->requires ) { case 'min_amount': $is_available = $has_met_min_amount; break; case 'coupon': $is_available = $has_coupon; break; case 'both': $is_available = $has_met_min_amount && $has_coupon; break; case 'either': $is_available = $has_met_min_amount || $has_coupon; break; default: $is_available = true; break; } return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); } /** * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. * * @uses WC_Shipping_Method::add_rate() * * @param array $package Shipping package. */ public function calculate_shipping( $package = array() ) { $this->add_rate( array( 'label' => $this->title, 'cost' => 0, 'taxes' => false, 'package' => $package, ) ); } /** * Enqueue JS to handle free shipping options. * * Static so that's enqueued only once. */ public static function enqueue_admin_js() { wc_enqueue_js( "jQuery( function( $ ) { function wcFreeShippingShowHideMinAmountField( el ) { var form = $( el ).closest( 'form' ); var minAmountField = $( '#woocommerce_free_shipping_min_amount', form ).closest( 'tr' ); var ignoreDiscountField = $( '#woocommerce_free_shipping_ignore_discounts', form ).closest( 'tr' ); if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) { minAmountField.hide(); ignoreDiscountField.hide(); } else { minAmountField.show(); ignoreDiscountField.show(); } } $( document.body ).on( 'change', '#woocommerce_free_shipping_requires', function() { wcFreeShippingShowHideMinAmountField( this ); }); // Change while load. $( '#woocommerce_free_shipping_requires' ).trigger( 'change' ); $( document.body ).on( 'wc_backbone_modal_loaded', function( evt, target ) { if ( 'wc-modal-shipping-method-settings' === target ) { wcFreeShippingShowHideMinAmountField( $( '#wc-backbone-modal-dialog #woocommerce_free_shipping_requires', evt.currentTarget ) ); } } ); });" ); } } includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php 0000644 00000016051 15132754524 0024254 0 ustar 00 <?php /** * Class WC_Shipping_Legacy_Free_Shipping file. * * @package WooCommerce\Shipping */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Free Shipping Method. * * This class is here for backwards compatibility for methods existing before zones existed. * * @deprecated 2.6.0 * @version 2.4.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Legacy_Free_Shipping extends WC_Shipping_Method { /** * Min amount to be valid. * * @var float */ public $min_amount; /** * Requires option. * * @var string */ public $requires; /** * Constructor. */ public function __construct() { $this->id = 'legacy_free_shipping'; $this->method_title = __( 'Free shipping (legacy)', 'woocommerce' ); /* translators: %s: Admin shipping settings URL */ $this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>'; $this->init(); } /** * Process and redirect if disabled. */ public function process_admin_options() { parent::process_admin_options(); if ( 'no' === $this->settings['enabled'] ) { wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); exit; } } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . 'free_shipping_settings'; } /** * Init function. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->enabled = $this->get_option( 'enabled' ); $this->title = $this->get_option( 'title' ); $this->min_amount = $this->get_option( 'min_amount', 0 ); $this->availability = $this->get_option( 'availability' ); $this->countries = $this->get_option( 'countries' ); $this->requires = $this->get_option( 'requires' ); // Actions. add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Method title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Free Shipping', 'woocommerce' ), 'desc_tip' => true, ), 'availability' => array( 'title' => __( 'Method availability', 'woocommerce' ), 'type' => 'select', 'default' => 'all', 'class' => 'availability wc-enhanced-select', 'options' => array( 'all' => __( 'All allowed countries', 'woocommerce' ), 'specific' => __( 'Specific Countries', 'woocommerce' ), ), ), 'countries' => array( 'title' => __( 'Specific countries', 'woocommerce' ), 'type' => 'multiselect', 'class' => 'wc-enhanced-select', 'css' => 'width: 400px;', 'default' => '', 'options' => WC()->countries->get_shipping_countries(), 'custom_attributes' => array( 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), ), ), 'requires' => array( 'title' => __( 'Free shipping requires...', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => '', 'options' => array( '' => __( 'N/A', 'woocommerce' ), 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), ), ), 'min_amount' => array( 'title' => __( 'Minimum order amount', 'woocommerce' ), 'type' => 'price', 'placeholder' => wc_format_localized_price( 0 ), 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), 'default' => '0', 'desc_tip' => true, ), ); } /** * Check if package is available. * * @param array $package Package information. * @return bool */ public function is_available( $package ) { if ( 'no' === $this->enabled ) { return false; } if ( 'specific' === $this->availability ) { $ship_to_countries = $this->countries; } else { $ship_to_countries = array_keys( WC()->countries->get_shipping_countries() ); } if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) { return false; } // Enabled logic. $is_available = false; $has_coupon = false; $has_met_min_amount = false; if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { $coupons = WC()->cart->get_coupons(); if ( $coupons ) { foreach ( $coupons as $code => $coupon ) { if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { $has_coupon = true; } } } } if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { $total = WC()->cart->get_displayed_subtotal(); if ( WC()->cart->display_prices_including_tax() ) { $total = NumberUtil::round( $total - ( WC()->cart->get_discount_total() + WC()->cart->get_discount_tax() ), wc_get_price_decimals() ); } else { $total = NumberUtil::round( $total - WC()->cart->get_discount_total(), wc_get_price_decimals() ); } if ( $total >= $this->min_amount ) { $has_met_min_amount = true; } } switch ( $this->requires ) { case 'min_amount': if ( $has_met_min_amount ) { $is_available = true; } break; case 'coupon': if ( $has_coupon ) { $is_available = true; } break; case 'both': if ( $has_met_min_amount && $has_coupon ) { $is_available = true; } break; case 'either': if ( $has_met_min_amount || $has_coupon ) { $is_available = true; } break; default: $is_available = true; break; } return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); } /** * Calculate shipping. * * @param array $package Package information. */ public function calculate_shipping( $package = array() ) { $args = array( 'id' => $this->id, 'label' => $this->title, 'cost' => 0, 'taxes' => false, 'package' => $package, ); $this->add_rate( $args ); } } includes/shipping/flat-rate/includes/settings-flat-rate.php 0000644 00000006517 15132754524 0020127 0 ustar 00 <?php /** * Settings for flat rate shipping. * * @package WooCommerce\Classes\Shipping */ defined( 'ABSPATH' ) || exit; $cost_desc = __( 'Enter a cost (excl. tax) or sum, e.g. <code>10.00 * [qty]</code>.', 'woocommerce' ) . '<br/><br/>' . __( 'Use <code>[qty]</code> for the number of items, <br/><code>[cost]</code> for the total cost of items, and <code>[fee percent="10" min_fee="20" max_fee=""]</code> for percentage based fees.', 'woocommerce' ); $settings = array( 'title' => array( 'title' => __( 'Method title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Flat rate', 'woocommerce' ), 'desc_tip' => true, ), 'tax_status' => array( 'title' => __( 'Tax status', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => 'taxable', 'options' => array( 'taxable' => __( 'Taxable', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ), ), 'cost' => array( 'title' => __( 'Cost', 'woocommerce' ), 'type' => 'text', 'placeholder' => '', 'description' => $cost_desc, 'default' => '0', 'desc_tip' => true, 'sanitize_callback' => array( $this, 'sanitize_cost' ), ), ); $shipping_classes = WC()->shipping()->get_shipping_classes(); if ( ! empty( $shipping_classes ) ) { $settings['class_costs'] = array( 'title' => __( 'Shipping class costs', 'woocommerce' ), 'type' => 'title', 'default' => '', /* translators: %s: URL for link. */ 'description' => sprintf( __( 'These costs can optionally be added based on the <a href="%s">product shipping class</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=classes' ) ), ); foreach ( $shipping_classes as $shipping_class ) { if ( ! isset( $shipping_class->term_id ) ) { continue; } $settings[ 'class_cost_' . $shipping_class->term_id ] = array( /* translators: %s: shipping class name */ 'title' => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ), 'type' => 'text', 'placeholder' => __( 'N/A', 'woocommerce' ), 'description' => $cost_desc, 'default' => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names. 'desc_tip' => true, 'sanitize_callback' => array( $this, 'sanitize_cost' ), ); } $settings['no_class_cost'] = array( 'title' => __( 'No shipping class cost', 'woocommerce' ), 'type' => 'text', 'placeholder' => __( 'N/A', 'woocommerce' ), 'description' => $cost_desc, 'default' => '', 'desc_tip' => true, 'sanitize_callback' => array( $this, 'sanitize_cost' ), ); $settings['type'] = array( 'title' => __( 'Calculation type', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => 'class', 'options' => array( 'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ), 'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ), ), ); } return $settings; includes/shipping/flat-rate/class-wc-shipping-flat-rate.php 0000644 00000017372 15132754524 0020015 0 ustar 00 <?php /** * Flat Rate Shipping Method. * * @version 2.6.0 * @package WooCommerce\Classes\Shipping */ defined( 'ABSPATH' ) || exit; /** * WC_Shipping_Flat_Rate class. */ class WC_Shipping_Flat_Rate extends WC_Shipping_Method { /** * Cost passed to [fee] shortcode. * * @var string Cost. */ protected $fee_cost = ''; /** * Constructor. * * @param int $instance_id Shipping method instance ID. */ public function __construct( $instance_id = 0 ) { $this->id = 'flat_rate'; $this->instance_id = absint( $instance_id ); $this->method_title = __( 'Flat rate', 'woocommerce' ); $this->method_description = __( 'Lets you charge a fixed rate for shipping.', 'woocommerce' ); $this->supports = array( 'shipping-zones', 'instance-settings', 'instance-settings-modal', ); $this->init(); add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Init user set variables. */ public function init() { $this->instance_form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; $this->title = $this->get_option( 'title' ); $this->tax_status = $this->get_option( 'tax_status' ); $this->cost = $this->get_option( 'cost' ); $this->type = $this->get_option( 'type', 'class' ); } /** * Evaluate a cost from a sum/string. * * @param string $sum Sum of shipping. * @param array $args Args, must contain `cost` and `qty` keys. Having `array()` as default is for back compat reasons. * @return string */ protected function evaluate_cost( $sum, $args = array() ) { // Add warning for subclasses. if ( ! is_array( $args ) || ! array_key_exists( 'qty', $args ) || ! array_key_exists( 'cost', $args ) ) { wc_doing_it_wrong( __FUNCTION__, '$args must contain `cost` and `qty` keys.', '4.0.1' ); } include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; // Allow 3rd parties to process shipping cost arguments. $args = apply_filters( 'woocommerce_evaluate_shipping_cost_args', $args, $sum, $this ); $locale = localeconv(); $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'], ',' ); $this->fee_cost = $args['cost']; // Expand shortcodes. add_shortcode( 'fee', array( $this, 'fee' ) ); $sum = do_shortcode( str_replace( array( '[qty]', '[cost]', ), array( $args['qty'], $args['cost'], ), $sum ) ); remove_shortcode( 'fee', array( $this, 'fee' ) ); // Remove whitespace from string. $sum = preg_replace( '/\s+/', '', $sum ); // Remove locale from string. $sum = str_replace( $decimals, '.', $sum ); // Trim invalid start/end characters. $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); // Do the math. return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; } /** * Work out fee (shortcode). * * @param array $atts Attributes. * @return string */ public function fee( $atts ) { $atts = shortcode_atts( array( 'percent' => '', 'min_fee' => '', 'max_fee' => '', ), $atts, 'fee' ); $calculated_fee = 0; if ( $atts['percent'] ) { $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); } if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { $calculated_fee = $atts['min_fee']; } if ( $atts['max_fee'] && $calculated_fee > $atts['max_fee'] ) { $calculated_fee = $atts['max_fee']; } return $calculated_fee; } /** * Calculate the shipping costs. * * @param array $package Package of items from cart. */ public function calculate_shipping( $package = array() ) { $rate = array( 'id' => $this->get_rate_id(), 'label' => $this->title, 'cost' => 0, 'package' => $package, ); // Calculate the costs. $has_costs = false; // True when a cost is set. False if all costs are blank strings. $cost = $this->get_option( 'cost' ); if ( '' !== $cost ) { $has_costs = true; $rate['cost'] = $this->evaluate_cost( $cost, array( 'qty' => $this->get_package_item_qty( $package ), 'cost' => $package['contents_cost'], ) ); } // Add shipping class costs. $shipping_classes = WC()->shipping()->get_shipping_classes(); if ( ! empty( $shipping_classes ) ) { $found_shipping_classes = $this->find_shipping_classes( $package ); $highest_class_cost = 0; foreach ( $found_shipping_classes as $shipping_class => $products ) { // Also handles BW compatibility when slugs were used instead of ids. $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); if ( '' === $class_cost_string ) { continue; } $has_costs = true; $class_cost = $this->evaluate_cost( $class_cost_string, array( 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), ) ); if ( 'class' === $this->type ) { $rate['cost'] += $class_cost; } else { $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; } } if ( 'order' === $this->type && $highest_class_cost ) { $rate['cost'] += $highest_class_cost; } } if ( $has_costs ) { $this->add_rate( $rate ); } /** * Developers can add additional flat rates based on this one via this action since @version 2.4. * * Previously there were (overly complex) options to add additional rates however this was not user. * friendly and goes against what Flat Rate Shipping was originally intended for. */ do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate ); } /** * Get items in package. * * @param array $package Package of items from cart. * @return int */ public function get_package_item_qty( $package ) { $total_quantity = 0; foreach ( $package['contents'] as $item_id => $values ) { if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { $total_quantity += $values['quantity']; } } return $total_quantity; } /** * Finds and returns shipping classes and the products with said class. * * @param mixed $package Package of items from cart. * @return array */ public function find_shipping_classes( $package ) { $found_shipping_classes = array(); foreach ( $package['contents'] as $item_id => $values ) { if ( $values['data']->needs_shipping() ) { $found_class = $values['data']->get_shipping_class(); if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { $found_shipping_classes[ $found_class ] = array(); } $found_shipping_classes[ $found_class ][ $item_id ] = $values; } } return $found_shipping_classes; } /** * Sanitize the cost field. * * @since 3.4.0 * @param string $value Unsanitized value. * @throws Exception Last error triggered. * @return string */ public function sanitize_cost( $value ) { $value = is_null( $value ) ? '' : $value; $value = wp_kses_post( trim( wp_unslash( $value ) ) ); $value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value ); // Thrown an error on the front end if the evaluate_cost will fail. $dummy_cost = $this->evaluate_cost( $value, array( 'cost' => 1, 'qty' => 1, ) ); if ( false === $dummy_cost ) { throw new Exception( WC_Eval_Math::$last_error ); } return $value; } } includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php 0000644 00000005123 15132754524 0030132 0 ustar 00 <?php /** * Class WC_Shipping_Legacy_International_Delivery file. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * International Delivery - Based on the Flat Rate Shipping Method. * * This class is here for backwards compatibility for methods existing before zones existed. * * @deprecated 2.6.0 * @version 2.4.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Legacy_International_Delivery extends WC_Shipping_Legacy_Flat_Rate { /** * Constructor. */ public function __construct() { $this->id = 'legacy_international_delivery'; $this->method_title = __( 'International flat rate (legacy)', 'woocommerce' ); /* translators: %s: Admin shipping settings URL */ $this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>'; $this->init(); add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . 'international_delivery_settings'; } /** * Initialise settings form fields. */ public function init_form_fields() { parent::init_form_fields(); $this->form_fields['availability'] = array( 'title' => __( 'Availability', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'description' => '', 'default' => 'including', 'options' => array( 'including' => __( 'Selected countries', 'woocommerce' ), 'excluding' => __( 'Excluding selected countries', 'woocommerce' ), ), ); } /** * Check if package is available. * * @param array $package Package information. * @return bool */ public function is_available( $package ) { if ( 'no' === $this->enabled ) { return false; } if ( 'including' === $this->availability ) { if ( is_array( $this->countries ) && ! in_array( $package['destination']['country'], $this->countries, true ) ) { return false; } } else { if ( is_array( $this->countries ) && ( in_array( $package['destination']['country'], $this->countries, true ) || ! $package['destination']['country'] ) ) { return false; } } return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', true, $package, $this ); } } includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php 0000644 00000011745 15132754524 0021370 0 ustar 00 <?php /** * Legacy flat rate settings. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } $cost_desc = __( 'Enter a cost (excl. tax) or sum, e.g. <code>10.00 * [qty]</code>.', 'woocommerce' ) . '<br/>' . __( 'Supports the following placeholders: <code>[qty]</code> = number of items, <code>[cost]</code> = cost of items, <code>[fee percent="10" min_fee="20"]</code> = Percentage based fee.', 'woocommerce' ); /** * Settings for flat rate shipping. */ $settings = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Method title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Flat rate', 'woocommerce' ), 'desc_tip' => true, ), 'availability' => array( 'title' => __( 'Availability', 'woocommerce' ), 'type' => 'select', 'default' => 'all', 'class' => 'availability wc-enhanced-select', 'options' => array( 'all' => __( 'All allowed countries', 'woocommerce' ), 'specific' => __( 'Specific Countries', 'woocommerce' ), ), ), 'countries' => array( 'title' => __( 'Specific countries', 'woocommerce' ), 'type' => 'multiselect', 'class' => 'wc-enhanced-select', 'css' => 'width: 400px;', 'default' => '', 'options' => WC()->countries->get_shipping_countries(), 'custom_attributes' => array( 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), ), ), 'tax_status' => array( 'title' => __( 'Tax status', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => 'taxable', 'options' => array( 'taxable' => __( 'Taxable', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ), ), 'cost' => array( 'title' => __( 'Cost', 'woocommerce' ), 'type' => 'text', 'placeholder' => '', 'description' => $cost_desc, 'default' => '', 'desc_tip' => true, ), ); $shipping_classes = WC()->shipping()->get_shipping_classes(); if ( ! empty( $shipping_classes ) ) { $settings['class_costs'] = array( 'title' => __( 'Shipping class costs', 'woocommerce' ), 'type' => 'title', 'default' => '', /* translators: %s: Admin shipping settings URL */ 'description' => sprintf( __( 'These costs can optionally be added based on the <a href="%s">product shipping class</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=classes' ) ), ); foreach ( $shipping_classes as $shipping_class ) { if ( ! isset( $shipping_class->term_id ) ) { continue; } $settings[ 'class_cost_' . $shipping_class->term_id ] = array( /* translators: %s: shipping class name */ 'title' => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ), 'type' => 'text', 'placeholder' => __( 'N/A', 'woocommerce' ), 'description' => $cost_desc, 'default' => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names. 'desc_tip' => true, ); } $settings['no_class_cost'] = array( 'title' => __( 'No shipping class cost', 'woocommerce' ), 'type' => 'text', 'placeholder' => __( 'N/A', 'woocommerce' ), 'description' => $cost_desc, 'default' => '', 'desc_tip' => true, ); $settings['type'] = array( 'title' => __( 'Calculation type', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'default' => 'class', 'options' => array( 'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ), 'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ), ), ); } if ( apply_filters( 'woocommerce_enable_deprecated_additional_flat_rates', $this->get_option( 'options', false ) ) ) { $settings['additional_rates'] = array( 'title' => __( 'Additional rates', 'woocommerce' ), 'type' => 'title', 'default' => '', 'description' => __( 'These rates are extra shipping options with additional costs (based on the flat rate).', 'woocommerce' ), ); $settings['options'] = array( 'title' => __( 'Additional rates', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'One per line: Option name | Additional cost [+- Percents] | Per cost type (order, class, or item) Example: <code>Priority mail | 6.95 [+ 0.2%] | order</code>.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Option name | Additional cost [+- Percents%] | Per cost type (order, class, or item)', 'woocommerce' ), ); } return $settings; includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php 0000644 00000027337 15132754524 0022523 0 ustar 00 <?php /** * Class WC_Shipping_Legacy_Flat_Rate file. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Flat Rate Shipping Method. * * This class is here for backwards compatibility for methods existing before zones existed. * * @deprecated 2.6.0 * @version 2.4.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Legacy_Flat_Rate extends WC_Shipping_Method { /** * Cost passed to [fee] shortcode. * * @var string */ protected $fee_cost = ''; /** * Constructor. */ public function __construct() { $this->id = 'legacy_flat_rate'; $this->method_title = __( 'Flat rate (legacy)', 'woocommerce' ); /* translators: %s: Admin shipping settings URL */ $this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>'; $this->init(); add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_flat_rate_shipping_add_rate', array( $this, 'calculate_extra_shipping' ), 10, 2 ); } /** * Process and redirect if disabled. */ public function process_admin_options() { parent::process_admin_options(); if ( 'no' === $this->settings['enabled'] ) { wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); exit; } } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . 'flat_rate_settings'; } /** * Init function. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->availability = $this->get_option( 'availability' ); $this->countries = $this->get_option( 'countries' ); $this->tax_status = $this->get_option( 'tax_status' ); $this->cost = $this->get_option( 'cost' ); $this->type = $this->get_option( 'type', 'class' ); $this->options = $this->get_option( 'options', false ); // @deprecated 2.4.0 } /** * Initialise Settings Form Fields. */ public function init_form_fields() { $this->form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; } /** * Evaluate a cost from a sum/string. * * @param string $sum Sum to evaluate. * @param array $args Arguments. * @return string */ protected function evaluate_cost( $sum, $args = array() ) { include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; $locale = localeconv(); $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); $this->fee_cost = $args['cost']; // Expand shortcodes. add_shortcode( 'fee', array( $this, 'fee' ) ); $sum = do_shortcode( str_replace( array( '[qty]', '[cost]', ), array( $args['qty'], $args['cost'], ), $sum ) ); remove_shortcode( 'fee', array( $this, 'fee' ) ); // Remove whitespace from string. $sum = preg_replace( '/\s+/', '', $sum ); // Remove locale from string. $sum = str_replace( $decimals, '.', $sum ); // Trim invalid start/end characters. $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); // Do the math. return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; } /** * Work out fee (shortcode). * * @param array $atts Shortcode attributes. * @return string */ public function fee( $atts ) { $atts = shortcode_atts( array( 'percent' => '', 'min_fee' => '', ), $atts, 'fee' ); $calculated_fee = 0; if ( $atts['percent'] ) { $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); } if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { $calculated_fee = $atts['min_fee']; } return $calculated_fee; } /** * Calculate shipping. * * @param array $package (default: array()). */ public function calculate_shipping( $package = array() ) { $rate = array( 'id' => $this->id, 'label' => $this->title, 'cost' => 0, 'package' => $package, ); // Calculate the costs. $has_costs = false; // True when a cost is set. False if all costs are blank strings. $cost = $this->get_option( 'cost' ); if ( '' !== $cost ) { $has_costs = true; $rate['cost'] = $this->evaluate_cost( $cost, array( 'qty' => $this->get_package_item_qty( $package ), 'cost' => $package['contents_cost'], ) ); } // Add shipping class costs. $found_shipping_classes = $this->find_shipping_classes( $package ); $highest_class_cost = 0; foreach ( $found_shipping_classes as $shipping_class => $products ) { // Also handles BW compatibility when slugs were used instead of ids. $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); if ( '' === $class_cost_string ) { continue; } $has_costs = true; $class_cost = $this->evaluate_cost( $class_cost_string, array( 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), ) ); if ( 'class' === $this->type ) { $rate['cost'] += $class_cost; } else { $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; } } if ( 'order' === $this->type && $highest_class_cost ) { $rate['cost'] += $highest_class_cost; } $rate['package'] = $package; // Add the rate. if ( $has_costs ) { $this->add_rate( $rate ); } /** * Developers can add additional flat rates based on this one via this action since @version 2.4. * * Previously there were (overly complex) options to add additional rates however this was not user. * friendly and goes against what Flat Rate Shipping was originally intended for. * * This example shows how you can add an extra rate based on this flat rate via custom function: * * add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 ); * * function add_another_custom_flat_rate( $method, $rate ) { * $new_rate = $rate; * $new_rate['id'] .= ':' . 'custom_rate_name'; // Append a custom ID. * $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'. * $new_rate['cost'] += 2; // Add $2 to the cost. * * // Add it to WC. * $method->add_rate( $new_rate ); * }. */ do_action( 'woocommerce_flat_rate_shipping_add_rate', $this, $rate ); } /** * Get items in package. * * @param array $package Package information. * @return int */ public function get_package_item_qty( $package ) { $total_quantity = 0; foreach ( $package['contents'] as $item_id => $values ) { if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { $total_quantity += $values['quantity']; } } return $total_quantity; } /** * Finds and returns shipping classes and the products with said class. * * @param mixed $package Package information. * @return array */ public function find_shipping_classes( $package ) { $found_shipping_classes = array(); foreach ( $package['contents'] as $item_id => $values ) { if ( $values['data']->needs_shipping() ) { $found_class = $values['data']->get_shipping_class(); if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { $found_shipping_classes[ $found_class ] = array(); } $found_shipping_classes[ $found_class ][ $item_id ] = $values; } } return $found_shipping_classes; } /** * Adds extra calculated flat rates. * * @deprecated 2.4.0 * * Additional rates defined like this: * Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item). * * @param null $method Deprecated. * @param array $rate Rate information. */ public function calculate_extra_shipping( $method, $rate ) { if ( $this->options ) { $options = array_filter( (array) explode( "\n", $this->options ) ); foreach ( $options as $option ) { $this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) ); if ( count( $this_option ) !== 3 ) { continue; } $extra_rate = $rate; $extra_rate['id'] = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) ); $extra_rate['label'] = $this_option[0]; $extra_cost = $this->get_extra_cost( $this_option[1], $this_option[2], $rate['package'] ); if ( is_array( $extra_rate['cost'] ) ) { $extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $extra_cost; } else { $extra_rate['cost'] += $extra_cost; } $this->add_rate( $extra_rate ); } } } /** * Calculate the percentage adjustment for each shipping rate. * * @deprecated 2.4.0 * @param float $cost Cost. * @param float $percent_adjustment Percent adjusment. * @param string $percent_operator Percent operator. * @param float $base_price Base price. * @return float */ public function calc_percentage_adjustment( $cost, $percent_adjustment, $percent_operator, $base_price ) { if ( '+' === $percent_operator ) { $cost += $percent_adjustment * $base_price; } else { $cost -= $percent_adjustment * $base_price; } return $cost; } /** * Get extra cost. * * @deprecated 2.4.0 * @param string $cost_string Cost string. * @param string $type Type. * @param array $package Package information. * @return float */ public function get_extra_cost( $cost_string, $type, $package ) { $cost = $cost_string; $cost_percent = false; // @codingStandardsIgnoreStart $pattern = '/' . // Start regex. '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. '\s*' . // Match whitespace. '(\+|-)' . // Capture the operand. '\s*' . // Match whitespace. '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. '\%/'; // Match the percent sign & end regex. // @codingStandardsIgnoreEnd if ( preg_match( $pattern, $cost_string, $this_cost_matches ) ) { $cost_operator = $this_cost_matches[2]; $cost_percent = $this_cost_matches[3] / 100; $cost = $this_cost_matches[1]; } switch ( $type ) { case 'class': $cost = $cost * count( $this->find_shipping_classes( $package ) ); break; case 'item': $cost = $cost * $this->get_package_item_qty( $package ); break; } if ( $cost_percent ) { switch ( $type ) { case 'class': $shipping_classes = $this->find_shipping_classes( $package ); foreach ( $shipping_classes as $shipping_class => $items ) { foreach ( $items as $item_id => $values ) { $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); } } break; case 'item': foreach ( $package['contents'] as $item_id => $values ) { if ( $values['data']->needs_shipping() ) { $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); } } break; case 'order': $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $package['contents_cost'] ); break; } } return $cost; } } includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php 0000644 00000015354 15132754524 0023727 0 ustar 00 <?php /** * Class WC_Shipping_Legacy_Local_Pickup file. * * @package WooCommerce\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Local Pickup Shipping Method. * * This class is here for backwards compatibility for methods existing before zones existed. * * @deprecated 2.6.0 * @version 2.3.0 * @package WooCommerce\Classes\Shipping */ class WC_Shipping_Legacy_Local_Pickup extends WC_Shipping_Method { /** * Constructor. */ public function __construct() { $this->id = 'legacy_local_pickup'; $this->method_title = __( 'Local pickup (legacy)', 'woocommerce' ); /* translators: %s: Admin shipping settings URL */ $this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>'; $this->init(); } /** * Process and redirect if disabled. */ public function process_admin_options() { parent::process_admin_options(); if ( 'no' === $this->settings['enabled'] ) { wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); exit; } } /** * Return the name of the option in the WP DB. * * @since 2.6.0 * @return string */ public function get_option_key() { return $this->plugin_id . 'local_pickup_settings'; } /** * Init function. */ public function init() { // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->enabled = $this->get_option( 'enabled' ); $this->title = $this->get_option( 'title' ); $this->codes = $this->get_option( 'codes' ); $this->availability = $this->get_option( 'availability' ); $this->countries = $this->get_option( 'countries' ); // Actions. add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); } /** * Calculate shipping. * * @param array $package Package information. */ public function calculate_shipping( $package = array() ) { $rate = array( 'id' => $this->id, 'label' => $this->title, 'package' => $package, ); $this->add_rate( $rate ); } /** * Initialize form fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Local pickup', 'woocommerce' ), 'desc_tip' => true, ), 'codes' => array( 'title' => __( 'Allowed ZIP/post codes', 'woocommerce' ), 'type' => 'text', 'desc_tip' => __( 'What ZIP/post codes are available for local pickup?', 'woocommerce' ), 'default' => '', 'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. <code>P*</code> will match a postcode of PE30. Also accepts a pattern, e.g. <code>NG1___</code> would match NG1 1AA but not NG10 1AA', 'woocommerce' ), 'placeholder' => 'e.g. 12345, 56789', ), 'availability' => array( 'title' => __( 'Method availability', 'woocommerce' ), 'type' => 'select', 'default' => 'all', 'class' => 'availability wc-enhanced-select', 'options' => array( 'all' => __( 'All allowed countries', 'woocommerce' ), 'specific' => __( 'Specific countries', 'woocommerce' ), ), ), 'countries' => array( 'title' => __( 'Specific countries', 'woocommerce' ), 'type' => 'multiselect', 'class' => 'wc-enhanced-select', 'css' => 'width: 400px;', 'default' => '', 'options' => WC()->countries->get_shipping_countries(), 'custom_attributes' => array( 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), ), ), ); } /** * Get postcodes for this method. * * @return array */ public function get_valid_postcodes() { $codes = array(); if ( '' !== $this->codes ) { foreach ( explode( ',', $this->codes ) as $code ) { $codes[] = strtoupper( trim( $code ) ); } } return $codes; } /** * See if a given postcode matches valid postcodes. * * @param string $postcode Postcode to check. * @param string $country code Code of the country to check postcode against. * @return boolean */ public function is_valid_postcode( $postcode, $country ) { $codes = $this->get_valid_postcodes(); $postcode = $this->clean( $postcode ); $formatted_postcode = wc_format_postcode( $postcode, $country ); if ( in_array( $postcode, $codes, true ) || in_array( $formatted_postcode, $codes, true ) ) { return true; } // Pattern matching. foreach ( $codes as $c ) { $pattern = '/^' . str_replace( '_', '[0-9a-zA-Z]', preg_quote( $c ) ) . '$/i'; if ( preg_match( $pattern, $postcode ) ) { return true; } } // Wildcard search. $wildcard_postcode = $formatted_postcode . '*'; $postcode_length = strlen( $formatted_postcode ); for ( $i = 0; $i < $postcode_length; $i++ ) { if ( in_array( $wildcard_postcode, $codes, true ) ) { return true; } $wildcard_postcode = substr( $wildcard_postcode, 0, -2 ) . '*'; } return false; } /** * See if the method is available. * * @param array $package Package information. * @return bool */ public function is_available( $package ) { $is_available = 'yes' === $this->enabled; if ( $is_available && $this->get_valid_postcodes() ) { $is_available = $this->is_valid_postcode( $package['destination']['postcode'], $package['destination']['country'] ); } if ( $is_available ) { if ( 'specific' === $this->availability ) { $ship_to_countries = $this->countries; } else { $ship_to_countries = array_keys( WC()->countries->get_shipping_countries() ); } if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) { $is_available = false; } } return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); } /** * Clean function. * * @access public * @param mixed $code Code. * @return string */ public function clean( $code ) { return str_replace( '-', '', sanitize_title( $code ) ) . ( strstr( $code, '*' ) ? '*' : '' ); } } includes/class-wc-cart-totals.php 0000644 00000070227 15132754524 0013052 0 ustar 00 <?php /** * Cart totals calculation class. * * Methods are protected and class is final to keep this as an internal API. * May be opened in the future once structure is stable. * * Rounding guide: * - if something is being stored e.g. item total, store unrounded. This is so taxes can be recalculated later accurately. * - if calculating a total, round (if settings allow). * * @package WooCommerce\Classes * @version 3.2.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Cart_Totals class. * * @since 3.2.0 */ final class WC_Cart_Totals { use WC_Item_Totals; /** * Reference to cart object. * * @since 3.2.0 * @var WC_Cart */ protected $cart; /** * Reference to customer object. * * @since 3.2.0 * @var array */ protected $customer; /** * Line items to calculate. * * @since 3.2.0 * @var array */ protected $items = array(); /** * Fees to calculate. * * @since 3.2.0 * @var array */ protected $fees = array(); /** * Shipping costs. * * @since 3.2.0 * @var array */ protected $shipping = array(); /** * Applied coupon objects. * * @since 3.2.0 * @var array */ protected $coupons = array(); /** * Item/coupon discount totals. * * @since 3.2.0 * @var array */ protected $coupon_discount_totals = array(); /** * Item/coupon discount tax totals. * * @since 3.2.0 * @var array */ protected $coupon_discount_tax_totals = array(); /** * Should taxes be calculated? * * @var boolean */ protected $calculate_tax = true; /** * Stores totals. * * @since 3.2.0 * @var array */ protected $totals = array( 'fees_total' => 0, 'fees_total_tax' => 0, 'items_subtotal' => 0, 'items_subtotal_tax' => 0, 'items_total' => 0, 'items_total_tax' => 0, 'total' => 0, 'shipping_total' => 0, 'shipping_tax_total' => 0, 'discounts_total' => 0, ); /** * Sets up the items provided, and calculate totals. * * @since 3.2.0 * @throws Exception If missing WC_Cart object. * @param WC_Cart $cart Cart object to calculate totals for. */ public function __construct( &$cart = null ) { if ( ! is_a( $cart, 'WC_Cart' ) ) { throw new Exception( 'A valid WC_Cart object is required' ); } $this->cart = $cart; $this->calculate_tax = wc_tax_enabled() && ! $cart->get_customer()->get_is_vat_exempt(); $this->calculate(); } /** * Run all calculation methods on the given items in sequence. * * @since 3.2.0 */ protected function calculate() { $this->calculate_item_totals(); $this->calculate_shipping_totals(); $this->calculate_fee_totals(); $this->calculate_totals(); } /** * Get default blank set of props used per item. * * @since 3.2.0 * @return array */ protected function get_default_item_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'quantity' => 0, 'product' => false, 'price_includes_tax' => false, 'subtotal' => 0, 'subtotal_tax' => 0, 'subtotal_taxes' => array(), 'total' => 0, 'total_tax' => 0, 'taxes' => array(), ); } /** * Get default blank set of props used per fee. * * @since 3.2.0 * @return array */ protected function get_default_fee_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'total_tax' => 0, 'taxes' => array(), ); } /** * Get default blank set of props used per shipping row. * * @since 3.2.0 * @return array */ protected function get_default_shipping_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'total' => 0, 'total_tax' => 0, 'taxes' => array(), ); } /** * Handles a cart or order object passed in for calculation. Normalises data * into the same format for use by this class. * * Each item is made up of the following props, in addition to those returned by get_default_item_props() for totals. * - key: An identifier for the item (cart item key or line item ID). * - cart_item: For carts, the cart item from the cart which may include custom data. * - quantity: The qty for this line. * - price: The line price in cents. * - product: The product object this cart item is for. * * @since 3.2.0 */ protected function get_items_from_cart() { $this->items = array(); foreach ( $this->cart->get_cart() as $cart_item_key => $cart_item ) { $item = $this->get_default_item_props(); $item->key = $cart_item_key; $item->object = $cart_item; $item->tax_class = $cart_item['data']->get_tax_class(); $item->taxable = 'taxable' === $cart_item['data']->get_tax_status(); $item->price_includes_tax = wc_prices_include_tax(); $item->quantity = $cart_item['quantity']; $item->price = wc_add_number_precision_deep( (float) $cart_item['data']->get_price() * (float) $cart_item['quantity'] ); $item->product = $cart_item['data']; $item->tax_rates = $this->get_item_tax_rates( $item ); $this->items[ $cart_item_key ] = $item; } } /** * Get item costs grouped by tax class. * * @since 3.2.0 * @return array */ protected function get_tax_class_costs() { $item_tax_classes = wp_list_pluck( $this->items, 'tax_class' ); $shipping_tax_classes = wp_list_pluck( $this->shipping, 'tax_class' ); $fee_tax_classes = wp_list_pluck( $this->fees, 'tax_class' ); $costs = array_fill_keys( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes, 0 ); $costs['non-taxable'] = 0; foreach ( $this->items + $this->fees + $this->shipping as $item ) { if ( 0 > $item->total ) { continue; } if ( ! $item->taxable ) { $costs['non-taxable'] += $item->total; } elseif ( 'inherit' === $item->tax_class ) { $costs[ reset( $item_tax_classes ) ] += $item->total; } else { $costs[ $item->tax_class ] += $item->total; } } return array_filter( $costs ); } /** * Get fee objects from the cart. Normalises data * into the same format for use by this class. * * @since 3.2.0 */ protected function get_fees_from_cart() { $this->fees = array(); $this->cart->calculate_fees(); $fee_running_total = 0; foreach ( $this->cart->get_fees() as $fee_key => $fee_object ) { $fee = $this->get_default_fee_props(); $fee->object = $fee_object; $fee->tax_class = $fee->object->tax_class; $fee->taxable = $fee->object->taxable; $fee->total = wc_add_number_precision_deep( $fee->object->amount ); // Negative fees should not make the order total go negative. if ( 0 > $fee->total ) { $max_discount = NumberUtil::round( $this->get_total( 'items_total', true ) + $fee_running_total + $this->get_total( 'shipping_total', true ) ) * -1; if ( $fee->total < $max_discount ) { $fee->total = $max_discount; } } $fee_running_total += $fee->total; if ( $this->calculate_tax ) { if ( 0 > $fee->total ) { // Negative fees should have the taxes split between all items so it works as a true discount. $tax_class_costs = $this->get_tax_class_costs(); $total_cost = array_sum( $tax_class_costs ); if ( $total_cost ) { foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { if ( 'non-taxable' === $tax_class ) { continue; } $proportion = $tax_class_cost / $total_cost; $cart_discount_proportion = $fee->total * $proportion; $fee->taxes = wc_array_merge_recursive_numeric( $fee->taxes, WC_Tax::calc_tax( $fee->total * $proportion, WC_Tax::get_rates( $tax_class ) ) ); } } } elseif ( $fee->object->taxable ) { $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->tax_class, $this->cart->get_customer() ), false ); } } $fee->taxes = apply_filters( 'woocommerce_cart_totals_get_fees_from_cart_taxes', $fee->taxes, $fee, $this ); $fee->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $fee->taxes ) ); // Set totals within object. $fee->object->total = wc_remove_number_precision_deep( $fee->total ); $fee->object->tax_data = wc_remove_number_precision_deep( $fee->taxes ); $fee->object->tax = wc_remove_number_precision_deep( $fee->total_tax ); $this->fees[ $fee_key ] = $fee; } } /** * Get shipping methods from the cart and normalise. * * @since 3.2.0 */ protected function get_shipping_from_cart() { $this->shipping = array(); if ( ! $this->cart->show_shipping() ) { return; } foreach ( $this->cart->calculate_shipping() as $key => $shipping_object ) { $shipping_line = $this->get_default_shipping_props(); $shipping_line->object = $shipping_object; $shipping_line->tax_class = get_option( 'woocommerce_shipping_tax_class' ); $shipping_line->taxable = true; $shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost ); $shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes, false ); $shipping_line->taxes = array_map( array( $this, 'round_item_subtotal' ), $shipping_line->taxes ); $shipping_line->total_tax = array_sum( $shipping_line->taxes ); $this->shipping[ $key ] = $shipping_line; } } /** * Return array of coupon objects from the cart. Normalises data * into the same format for use by this class. * * @since 3.2.0 */ protected function get_coupons_from_cart() { $this->coupons = $this->cart->get_coupons(); foreach ( $this->coupons as $coupon ) { switch ( $coupon->get_discount_type() ) { case 'fixed_product': $coupon->sort = 1; break; case 'percent': $coupon->sort = 2; break; case 'fixed_cart': $coupon->sort = 3; break; default: $coupon->sort = 0; break; } // Allow plugins to override the default order. $coupon->sort = apply_filters( 'woocommerce_coupon_sort', $coupon->sort, $coupon ); } uasort( $this->coupons, array( $this, 'sort_coupons_callback' ) ); } /** * Sort coupons so discounts apply consistently across installs. * * In order of priority; * - sort param * - usage restriction * - coupon value * - ID * * @param WC_Coupon $a Coupon object. * @param WC_Coupon $b Coupon object. * @return int */ protected function sort_coupons_callback( $a, $b ) { if ( $a->sort === $b->sort ) { if ( $a->get_limit_usage_to_x_items() === $b->get_limit_usage_to_x_items() ) { if ( $a->get_amount() === $b->get_amount() ) { return $b->get_id() - $a->get_id(); } return ( $a->get_amount() < $b->get_amount() ) ? -1 : 1; } return ( $a->get_limit_usage_to_x_items() < $b->get_limit_usage_to_x_items() ) ? -1 : 1; } return ( $a->sort < $b->sort ) ? -1 : 1; } /** * Ran to remove all base taxes from an item. Used when prices include tax, and the customer is tax exempt. * * @since 3.2.2 * @param object $item Item to adjust the prices of. * @return object */ protected function remove_item_base_taxes( $item ) { if ( $item->price_includes_tax && $item->taxable ) { if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) ); } else { /** * If we want all customers to pay the same price on this store, we should not remove base taxes from a VAT exempt user's price, * but just the relevent tax rate. See issue #20911. */ $base_tax_rates = $item->tax_rates; } // Work out a new base price without the shop's base tax. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true ); // Now we have a new item price (excluding TAX). $item->price = NumberUtil::round( $item->price - array_sum( $taxes ) ); $item->price_includes_tax = false; } return $item; } /** * Only ran if woocommerce_adjust_non_base_location_prices is true. * * If the customer is outside of the base location, this removes the base * taxes. This is off by default unless the filter is used. * * Uses edit context so unfiltered tax class is returned. * * @since 3.2.0 * @param object $item Item to adjust the prices of. * @return object */ protected function adjust_non_base_location_price( $item ) { if ( $item->price_includes_tax && $item->taxable ) { $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) ); if ( $item->tax_rates !== $base_tax_rates ) { // Work out a new base price without the shop's base tax. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true ); $new_taxes = WC_Tax::calc_tax( $item->price - array_sum( $taxes ), $item->tax_rates, false ); // Now we have a new item price. $item->price = $item->price - array_sum( $taxes ) + array_sum( $new_taxes ); } } return $item; } /** * Get discounted price of an item with precision (in cents). * * @since 3.2.0 * @param object $item_key Item to get the price of. * @return int */ protected function get_discounted_price_in_cents( $item_key ) { $item = $this->items[ $item_key ]; $price = isset( $this->coupon_discount_totals[ $item_key ] ) ? $item->price - $this->coupon_discount_totals[ $item_key ] : $item->price; return $price; } /** * Get tax rates for an item. Caches rates in class to avoid multiple look ups. * * @param object $item Item to get tax rates for. * @return array of taxes */ protected function get_item_tax_rates( $item ) { if ( ! wc_tax_enabled() ) { return array(); } $tax_class = $item->product->get_tax_class(); $item_tax_rates = isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->cart->get_customer() ); // Allow plugins to filter item tax rates. return apply_filters( 'woocommerce_cart_totals_get_item_tax_rates', $item_tax_rates, $item, $this->cart ); } /** * Get item costs grouped by tax class. * * @since 3.2.0 * @return array */ protected function get_item_costs_by_tax_class() { $tax_classes = array( 'non-taxable' => 0, ); foreach ( $this->items + $this->fees + $this->shipping as $item ) { if ( ! isset( $tax_classes[ $item->tax_class ] ) ) { $tax_classes[ $item->tax_class ] = 0; } if ( $item->taxable ) { $tax_classes[ $item->tax_class ] += $item->total; } else { $tax_classes['non-taxable'] += $item->total; } } return $tax_classes; } /** * Get a single total with or without precision (in cents). * * @since 3.2.0 * @param string $key Total to get. * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return int|float */ public function get_total( $key = 'total', $in_cents = false ) { $totals = $this->get_totals( $in_cents ); return isset( $totals[ $key ] ) ? $totals[ $key ] : 0; } /** * Set a single total. * * @since 3.2.0 * @param string $key Total name you want to set. * @param int $total Total to set. */ protected function set_total( $key, $total ) { $this->totals[ $key ] = $total; } /** * Get all totals with or without precision (in cents). * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array. */ public function get_totals( $in_cents = false ) { return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals ); } /** * Returns array of values for totals calculation. * * @param string $field Field name. Will probably be `total` or `subtotal`. * @return array Items object */ protected function get_values_for_total( $field ) { return array_values( wp_list_pluck( $this->items, $field ) ); } /** * Get taxes merged by type. * * @since 3.2.0 * @param bool $in_cents If returned value should be in cents. * @param array|string $types Types to merge and return. Defaults to all. * @return array */ protected function get_merged_taxes( $in_cents = false, $types = array( 'items', 'fees', 'shipping' ) ) { $items = array(); $taxes = array(); if ( is_string( $types ) ) { $types = array( $types ); } foreach ( $types as $type ) { if ( isset( $this->$type ) ) { $items = array_merge( $items, $this->$type ); } } foreach ( $items as $item ) { foreach ( $item->taxes as $rate_id => $rate ) { if ( ! isset( $taxes[ $rate_id ] ) ) { $taxes[ $rate_id ] = 0; } $taxes[ $rate_id ] += $this->round_line_tax( $rate ); } } return $in_cents ? $taxes : wc_remove_number_precision_deep( $taxes ); } /** * Round merged taxes. * * @deprecated 3.9.0 `calculate_item_subtotals` should already appropriately round the tax values. * @since 3.5.4 * @param array $taxes Taxes to round. * @return array */ protected function round_merged_taxes( $taxes ) { foreach ( $taxes as $rate_id => $tax ) { $taxes[ $rate_id ] = $this->round_line_tax( $tax ); } return $taxes; } /** * Combine item taxes into a single array, preserving keys. * * @since 3.2.0 * @param array $item_taxes Taxes to combine. * @return array */ protected function combine_item_taxes( $item_taxes ) { $merged_taxes = array(); foreach ( $item_taxes as $taxes ) { foreach ( $taxes as $tax_id => $tax_amount ) { if ( ! isset( $merged_taxes[ $tax_id ] ) ) { $merged_taxes[ $tax_id ] = 0; } $merged_taxes[ $tax_id ] += $tax_amount; } } return $merged_taxes; } /* |-------------------------------------------------------------------------- | Calculation methods. |-------------------------------------------------------------------------- */ /** * Calculate item totals. * * @since 3.2.0 */ protected function calculate_item_totals() { $this->get_items_from_cart(); $this->calculate_item_subtotals(); $this->calculate_discounts(); foreach ( $this->items as $item_key => $item ) { $item->total = $this->get_discounted_price_in_cents( $item_key ); $item->total_tax = 0; if ( has_filter( 'woocommerce_get_discounted_price' ) ) { /** * Allow plugins to filter this price like in the legacy cart class. * * This is legacy and should probably be deprecated in the future. * $item->object is the cart item object. * $this->cart is the cart object. */ $item->total = wc_add_number_precision( apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->cart ) ); } if ( $this->calculate_tax && $item->product->is_taxable() ) { $total_taxes = apply_filters( 'woocommerce_calculate_item_totals_taxes', WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ), $item, $this ); $item->taxes = $total_taxes; $item->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->taxes ) ); if ( $item->price_includes_tax ) { // Use unrounded taxes so we can re-calculate from the orders screen accurately later. $item->total = $item->total - array_sum( $item->taxes ); } } $this->cart->cart_contents[ $item_key ]['line_tax_data']['total'] = wc_remove_number_precision_deep( $item->taxes ); $this->cart->cart_contents[ $item_key ]['line_total'] = wc_remove_number_precision( $item->total ); $this->cart->cart_contents[ $item_key ]['line_tax'] = wc_remove_number_precision( $item->total_tax ); } $items_total = $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) ); $this->set_total( 'items_total', $items_total ); $this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) ); $this->cart->set_cart_contents_total( $this->get_total( 'items_total' ) ); $this->cart->set_cart_contents_tax( array_sum( $this->get_merged_taxes( false, 'items' ) ) ); $this->cart->set_cart_contents_taxes( $this->get_merged_taxes( false, 'items' ) ); } /** * Subtotals are costs before discounts. * * To prevent rounding issues we need to work with the inclusive price where possible * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would * be 8.325 leading to totals being 1p off. * * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated * afterwards. * * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that. * * @since 3.2.0 */ protected function calculate_item_subtotals() { $merged_subtotal_taxes = array(); // Taxes indexed by tax rate ID for storage later. $adjust_non_base_location_prices = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ); $is_customer_vat_exempt = $this->cart->get_customer()->get_is_vat_exempt(); foreach ( $this->items as $item_key => $item ) { if ( $item->price_includes_tax ) { if ( $is_customer_vat_exempt ) { $item = $this->remove_item_base_taxes( $item ); } elseif ( $adjust_non_base_location_prices ) { $item = $this->adjust_non_base_location_price( $item ); } } $item->subtotal = $item->price; if ( $this->calculate_tax && $item->product->is_taxable() ) { $item->subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax ); $item->subtotal_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->subtotal_taxes ) ); if ( $item->price_includes_tax ) { // Use unrounded taxes so we can re-calculate from the orders screen accurately later. $item->subtotal = $item->subtotal - array_sum( $item->subtotal_taxes ); } foreach ( $item->subtotal_taxes as $rate_id => $rate ) { if ( ! isset( $merged_subtotal_taxes[ $rate_id ] ) ) { $merged_subtotal_taxes[ $rate_id ] = 0; } $merged_subtotal_taxes[ $rate_id ] += $this->round_line_tax( $rate ); } } $this->cart->cart_contents[ $item_key ]['line_tax_data'] = array( 'subtotal' => wc_remove_number_precision_deep( $item->subtotal_taxes ) ); $this->cart->cart_contents[ $item_key ]['line_subtotal'] = wc_remove_number_precision( $item->subtotal ); $this->cart->cart_contents[ $item_key ]['line_subtotal_tax'] = wc_remove_number_precision( $item->subtotal_tax ); } $items_subtotal = $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) ); // Prices are not rounded here because they should already be rounded based on settings in `get_rounded_items_total` and in `round_line_tax` method calls. $this->set_total( 'items_subtotal', $items_subtotal ); $this->set_total( 'items_subtotal_tax', array_sum( $merged_subtotal_taxes ), 0 ); $this->cart->set_subtotal( $this->get_total( 'items_subtotal' ) ); $this->cart->set_subtotal_tax( $this->get_total( 'items_subtotal_tax' ) ); } /** * Calculate COUPON based discounts which change item prices. * * @since 3.2.0 * @uses WC_Discounts class. */ protected function calculate_discounts() { $this->get_coupons_from_cart(); $discounts = new WC_Discounts( $this->cart ); // Set items directly so the discounts class can see any tax adjustments made thus far using subtotals. $discounts->set_items( $this->items ); foreach ( $this->coupons as $coupon ) { $discounts->apply_coupon( $coupon ); } $coupon_discount_amounts = $discounts->get_discounts_by_coupon( true ); $coupon_discount_tax_amounts = array(); // See how much tax was 'discounted' per item and per coupon. if ( $this->calculate_tax ) { foreach ( $discounts->get_discounts( true ) as $coupon_code => $coupon_discounts ) { $coupon_discount_tax_amounts[ $coupon_code ] = 0; foreach ( $coupon_discounts as $item_key => $coupon_discount ) { $item = $this->items[ $item_key ]; if ( $item->product->is_taxable() ) { // Item subtotals were sent, so set 3rd param. $item_tax = array_sum( WC_Tax::calc_tax( $coupon_discount, $item->tax_rates, $item->price_includes_tax ) ); // Sum total tax. $coupon_discount_tax_amounts[ $coupon_code ] += $item_tax; // Remove tax from discount total. if ( $item->price_includes_tax ) { $coupon_discount_amounts[ $coupon_code ] -= $item_tax; } } } } } $this->coupon_discount_totals = (array) $discounts->get_discounts_by_item( true ); $this->coupon_discount_tax_totals = $coupon_discount_tax_amounts; if ( wc_prices_include_tax() ) { $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) - array_sum( $this->coupon_discount_tax_totals ) ); $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) ); } else { $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) ); $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) ); } $this->cart->set_coupon_discount_totals( wc_remove_number_precision_deep( $coupon_discount_amounts ) ); $this->cart->set_coupon_discount_tax_totals( wc_remove_number_precision_deep( $coupon_discount_tax_amounts ) ); // Add totals to cart object. Note: Discount total for cart is excl tax. $this->cart->set_discount_total( $this->get_total( 'discounts_total' ) ); $this->cart->set_discount_tax( $this->get_total( 'discounts_tax_total' ) ); } /** * Triggers the cart fees API, grabs the list of fees, and calculates taxes. * * Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed. * * @since 3.2.0 */ protected function calculate_fee_totals() { $this->get_fees_from_cart(); $this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); $this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ); $this->cart->fees_api()->set_fees( wp_list_pluck( $this->fees, 'object' ) ); $this->cart->set_fee_total( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ) ); $this->cart->set_fee_tax( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ) ); $this->cart->set_fee_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->fees, 'taxes' ) ) ) ); } /** * Calculate any shipping taxes. * * @since 3.2.0 */ protected function calculate_shipping_totals() { $this->get_shipping_from_cart(); $this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) ); $this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) ); $this->cart->set_shipping_total( $this->get_total( 'shipping_total' ) ); $this->cart->set_shipping_tax( $this->get_total( 'shipping_tax_total' ) ); $this->cart->set_shipping_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->shipping, 'taxes' ) ) ) ); } /** * Main cart totals. * * @since 3.2.0 */ protected function calculate_totals() { $this->set_total( 'total', NumberUtil::round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) ); $items_tax = array_sum( $this->get_merged_taxes( false, array( 'items' ) ) ); // Shipping and fee taxes are rounded seperately because they were entered excluding taxes (as opposed to item prices, which may or may not be including taxes depending upon settings). $shipping_and_fee_taxes = NumberUtil::round( array_sum( $this->get_merged_taxes( false, array( 'fees', 'shipping' ) ) ), wc_get_price_decimals() ); $this->cart->set_total_tax( $items_tax + $shipping_and_fee_taxes ); // Allow plugins to hook and alter totals before final total is calculated. if ( has_action( 'woocommerce_calculate_totals' ) ) { do_action( 'woocommerce_calculate_totals', $this->cart ); } // Allow plugins to filter the grand total, and sum the cart totals in case of modifications. $this->cart->set_total( max( 0, apply_filters( 'woocommerce_calculated_total', $this->get_total( 'total' ), $this->cart ) ) ); } } includes/class-wc-privacy.php 0000644 00000037030 15132754524 0012265 0 ustar 00 <?php /** * Privacy/GDPR related functionality which ties into WordPress functionality. * * @since 3.4.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Privacy_Background_Process', false ) ) { include_once __DIR__ . '/class-wc-privacy-background-process.php'; } /** * WC_Privacy Class. */ class WC_Privacy extends WC_Abstract_Privacy { /** * Background process to clean up orders. * * @var WC_Privacy_Background_Process */ protected static $background_process; /** * Init - hook into events. */ public function __construct() { parent::__construct(); // Initialize data exporters and erasers. add_action( 'plugins_loaded', array( $this, 'register_erasers_exporters' ) ); // Cleanup orders daily - this is a callback on a daily cron event. add_action( 'woocommerce_cleanup_personal_data', array( $this, 'queue_cleanup_personal_data' ) ); // Handles custom anonomization types not included in core. add_filter( 'wp_privacy_anonymize_data', array( $this, 'anonymize_custom_data_types' ), 10, 3 ); // When this is fired, data is removed in a given order. Called from bulk actions. add_action( 'woocommerce_remove_order_personal_data', array( 'WC_Privacy_Erasers', 'remove_order_personal_data' ) ); } /** * Initial registration of privacy erasers and exporters. * * Due to the use of translation functions, this should run only after plugins loaded. */ public function register_erasers_exporters() { $this->name = __( 'WooCommerce', 'woocommerce' ); if ( ! self::$background_process ) { self::$background_process = new WC_Privacy_Background_Process(); } // Include supporting classes. include_once __DIR__ . '/class-wc-privacy-erasers.php'; include_once __DIR__ . '/class-wc-privacy-exporters.php'; // This hook registers WooCommerce data exporters. $this->add_exporter( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) ); $this->add_exporter( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) ); $this->add_exporter( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) ); $this->add_exporter( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) ); // This hook registers WooCommerce data erasers. $this->add_eraser( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) ); $this->add_eraser( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) ); $this->add_eraser( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) ); $this->add_eraser( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) ); } /** * Add privacy policy content for the privacy policy page. * * @since 3.4.0 */ public function get_privacy_message() { $content = '<div class="wp-suggested-text">' . '<p class="privacy-policy-tutorial">' . __( 'This sample language includes the basics around what personal data your store may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional plugins are used, the specific information shared by your store will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'woocommerce' ) . '</p>' . '<p>' . __( 'We collect information about you during the checkout process on our store.', 'woocommerce' ) . '</p>' . '<h2>' . __( 'What we collect and store', 'woocommerce' ) . '</h2>' . '<p>' . __( 'While you visit our site, we’ll track:', 'woocommerce' ) . '</p>' . '<ul>' . '<li>' . __( 'Products you’ve viewed: we’ll use this to, for example, show you products you’ve recently viewed', 'woocommerce' ) . '</li>' . '<li>' . __( 'Location, IP address and browser type: we’ll use this for purposes like estimating taxes and shipping', 'woocommerce' ) . '</li>' . '<li>' . __( 'Shipping address: we’ll ask you to enter this so we can, for instance, estimate shipping before you place an order, and send you the order!', 'woocommerce' ) . '</li>' . '</ul>' . '<p>' . __( 'We’ll also use cookies to keep track of cart contents while you’re browsing our site.', 'woocommerce' ) . '</p>' . '<p class="privacy-policy-tutorial">' . __( 'Note: you may want to further detail your cookie policy, and link to that section from here.', 'woocommerce' ) . '</p>' . '<p>' . __( 'When you purchase from us, we’ll ask you to provide information including your name, billing address, shipping address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'woocommerce' ) . '</p>' . '<ul>' . '<li>' . __( 'Send you information about your account and order', 'woocommerce' ) . '</li>' . '<li>' . __( 'Respond to your requests, including refunds and complaints', 'woocommerce' ) . '</li>' . '<li>' . __( 'Process payments and prevent fraud', 'woocommerce' ) . '</li>' . '<li>' . __( 'Set up your account for our store', 'woocommerce' ) . '</li>' . '<li>' . __( 'Comply with any legal obligations we have, such as calculating taxes', 'woocommerce' ) . '</li>' . '<li>' . __( 'Improve our store offerings', 'woocommerce' ) . '</li>' . '<li>' . __( 'Send you marketing messages, if you choose to receive them', 'woocommerce' ) . '</li>' . '</ul>' . '<p>' . __( 'If you create an account, we will store your name, address, email and phone number, which will be used to populate the checkout for future orders.', 'woocommerce' ) . '</p>' . '<p>' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing and shipping addresses.', 'woocommerce' ) . '</p>' . '<p>' . __( 'We will also store comments or reviews, if you choose to leave them.', 'woocommerce' ) . '</p>' . '<h2>' . __( 'Who on our team has access', 'woocommerce' ) . '</h2>' . '<p>' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Shop Managers can access:', 'woocommerce' ) . '</p>' . '<ul>' . '<li>' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'woocommerce' ) . '</li>' . '<li>' . __( 'Customer information like your name, email address, and billing and shipping information.', 'woocommerce' ) . '</li>' . '</ul>' . '<p>' . __( 'Our team members have access to this information to help fulfill orders, process refunds and support you.', 'woocommerce' ) . '</p>' . '<h2>' . __( 'What we share with others', 'woocommerce' ) . '</h2>' . '<p class="privacy-policy-tutorial">' . __( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, shipping providers, and third party embeds.', 'woocommerce' ) . '</p>' . '<p>' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'woocommerce' ) . '</p>' . '<h3>' . __( 'Payments', 'woocommerce' ) . '</h3>' . '<p class="privacy-policy-tutorial">' . __( 'In this subsection you should list which third party payment processors you’re using to take payments on your store since these may handle customer data. We’ve included PayPal as an example, but you should remove this if you’re not using PayPal.', 'woocommerce' ) . '</p>' . '<p>' . __( 'We accept payments through PayPal. When processing payments, some of your data will be passed to PayPal, including information required to process or support the payment, such as the purchase total and billing information.', 'woocommerce' ) . '</p>' . '<p>' . __( 'Please see the <a href="https://www.paypal.com/us/webapps/mpp/ua/privacy-full">PayPal Privacy Policy</a> for more details.', 'woocommerce' ) . '</p>' . '</div>'; return apply_filters( 'wc_privacy_policy_content', $content ); } /** * Spawn events for order cleanup. */ public function queue_cleanup_personal_data() { self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) ); self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) ); self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) ); self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) ); self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) ); self::$background_process->save()->dispatch(); } /** * Handle some custom types of data and anonymize them. * * @param string $anonymous Anonymized string. * @param string $type Type of data. * @param string $data The data being anonymized. * @return string Anonymized string. */ public function anonymize_custom_data_types( $anonymous, $type, $data ) { switch ( $type ) { case 'address_state': case 'address_country': $anonymous = ''; // Empty string - we don't want to store anything after removal. break; case 'phone': $anonymous = preg_replace( '/\d/u', '0', $data ); break; case 'numeric_id': $anonymous = 0; break; } return $anonymous; } /** * Find and trash old orders. * * @since 3.4.0 * @param int $limit Limit orders to process per batch. * @return int Number of orders processed. */ public static function trash_pending_orders( $limit = 20 ) { $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) ); if ( empty( $option['number'] ) ) { return 0; } return self::trash_orders_query( apply_filters( 'woocommerce_trash_pending_orders_query_args', array( 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), 'limit' => $limit, // Batches of 20. 'status' => 'wc-pending', 'type' => 'shop_order', ) ) ); } /** * Find and trash old orders. * * @since 3.4.0 * @param int $limit Limit orders to process per batch. * @return int Number of orders processed. */ public static function trash_failed_orders( $limit = 20 ) { $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) ); if ( empty( $option['number'] ) ) { return 0; } return self::trash_orders_query( apply_filters( 'woocommerce_trash_failed_orders_query_args', array( 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), 'limit' => $limit, // Batches of 20. 'status' => 'wc-failed', 'type' => 'shop_order', ) ) ); } /** * Find and trash old orders. * * @since 3.4.0 * @param int $limit Limit orders to process per batch. * @return int Number of orders processed. */ public static function trash_cancelled_orders( $limit = 20 ) { $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) ); if ( empty( $option['number'] ) ) { return 0; } return self::trash_orders_query( apply_filters( 'woocommerce_trash_cancelled_orders_query_args', array( 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), 'limit' => $limit, // Batches of 20. 'status' => 'wc-cancelled', 'type' => 'shop_order', ) ) ); } /** * For a given query trash all matches. * * @since 3.4.0 * @param array $query Query array to pass to wc_get_orders(). * @return int Count of orders that were trashed. */ protected static function trash_orders_query( $query ) { $orders = wc_get_orders( $query ); $count = 0; if ( $orders ) { foreach ( $orders as $order ) { $order->delete( false ); $count ++; } } return $count; } /** * Anonymize old completed orders. * * @since 3.4.0 * @param int $limit Limit orders to process per batch. * @return int Number of orders processed. */ public static function anonymize_completed_orders( $limit = 20 ) { $option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) ); if ( empty( $option['number'] ) ) { return 0; } return self::anonymize_orders_query( apply_filters( 'woocommerce_anonymize_completed_orders_query_args', array( 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), 'limit' => $limit, // Batches of 20. 'status' => 'wc-completed', 'anonymized' => false, 'type' => 'shop_order', ) ) ); } /** * For a given query, anonymize all matches. * * @since 3.4.0 * @param array $query Query array to pass to wc_get_orders(). * @return int Count of orders that were anonymized. */ protected static function anonymize_orders_query( $query ) { $orders = wc_get_orders( $query ); $count = 0; if ( $orders ) { foreach ( $orders as $order ) { WC_Privacy_Erasers::remove_order_personal_data( $order ); $count ++; } } return $count; } /** * Delete inactive accounts. * * @since 3.4.0 * @param int $limit Limit users to process per batch. * @return int Number of users processed. */ public static function delete_inactive_accounts( $limit = 20 ) { $option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) ); if ( empty( $option['number'] ) ) { return 0; } return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit ); } /** * Delete inactive accounts. * * @since 3.4.0 * @param int $timestamp Timestamp to delete customers before. * @param int $limit Limit number of users to delete per run. * @return int Count of customers that were deleted. */ protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) { $count = 0; $user_query = new WP_User_Query( array( 'fields' => 'ID', 'number' => $limit, 'role__in' => apply_filters( 'woocommerce_delete_inactive_account_roles', array( 'Customer', 'Subscriber', ) ), 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'AND', array( 'key' => 'wc_last_active', 'value' => (string) $timestamp, 'compare' => '<', 'type' => 'NUMERIC', ), array( 'key' => 'wc_last_active', 'value' => '0', 'compare' => '>', 'type' => 'NUMERIC', ), ), ) ); $user_ids = $user_query->get_results(); if ( $user_ids ) { if ( ! function_exists( 'wp_delete_user' ) ) { require_once ABSPATH . 'wp-admin/includes/user.php'; } foreach ( $user_ids as $user_id ) { wp_delete_user( $user_id ); $count ++; } } return $count; } } new WC_Privacy(); includes/class-wc-autoloader.php 0000644 00000005372 15132754524 0012753 0 ustar 00 <?php /** * WooCommerce Autoloader. * * @package WooCommerce\Classes * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Autoloader class. */ class WC_Autoloader { /** * Path to the includes directory. * * @var string */ private $include_path = ''; /** * The Constructor. */ public function __construct() { if ( function_exists( '__autoload' ) ) { spl_autoload_register( '__autoload' ); } spl_autoload_register( array( $this, 'autoload' ) ); $this->include_path = untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ) . '/includes/'; } /** * Take a class name and turn it into a file name. * * @param string $class Class name. * @return string */ private function get_file_name_from_class( $class ) { return 'class-' . str_replace( '_', '-', $class ) . '.php'; } /** * Include a class file. * * @param string $path File path. * @return bool Successful or not. */ private function load_file( $path ) { if ( $path && is_readable( $path ) ) { include_once $path; return true; } return false; } /** * Auto-load WC classes on demand to reduce memory consumption. * * @param string $class Class name. */ public function autoload( $class ) { $class = strtolower( $class ); if ( 0 !== strpos( $class, 'wc_' ) ) { return; } $file = $this->get_file_name_from_class( $class ); $path = ''; if ( 0 === strpos( $class, 'wc_addons_gateway_' ) ) { $path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 18 ) . '/'; } elseif ( 0 === strpos( $class, 'wc_gateway_' ) ) { $path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 11 ) . '/'; } elseif ( 0 === strpos( $class, 'wc_shipping_' ) ) { $path = $this->include_path . 'shipping/' . substr( str_replace( '_', '-', $class ), 12 ) . '/'; } elseif ( 0 === strpos( $class, 'wc_shortcode_' ) ) { $path = $this->include_path . 'shortcodes/'; } elseif ( 0 === strpos( $class, 'wc_meta_box' ) ) { $path = $this->include_path . 'admin/meta-boxes/'; } elseif ( 0 === strpos( $class, 'wc_admin' ) ) { $path = $this->include_path . 'admin/'; } elseif ( 0 === strpos( $class, 'wc_payment_token_' ) ) { $path = $this->include_path . 'payment-tokens/'; } elseif ( 0 === strpos( $class, 'wc_log_handler_' ) ) { $path = $this->include_path . 'log-handlers/'; } elseif ( 0 === strpos( $class, 'wc_integration' ) ) { $path = $this->include_path . 'integrations/' . substr( str_replace( '_', '-', $class ), 15 ) . '/'; } elseif ( 0 === strpos( $class, 'wc_notes_' ) ) { $path = $this->include_path . 'admin/notes/'; } if ( empty( $path ) || ! $this->load_file( $path . $file ) ) { $this->load_file( $this->include_path . $file ); } } } new WC_Autoloader(); includes/tracks/class-wc-tracks-client.php 0000644 00000012345 15132754524 0014644 0 ustar 00 <?php /** * Send Tracks events on behalf of a user. * * @package WooCommerce\Tracks */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * WC_Tracks_Client class. */ class WC_Tracks_Client { /** * Pixel URL. */ const PIXEL = 'https://pixel.wp.com/t.gif'; /** * Browser type. */ const BROWSER_TYPE = 'php-agent'; /** * User agent. */ const USER_AGENT_SLUG = 'tracks-client'; /** * Initialize tracks client class * * @return void */ public static function init() { // Use wp hook for setting the identity cookie to avoid headers already sent warnings. add_action( 'admin_init', array( __CLASS__, 'maybe_set_identity_cookie' ) ); } /** * Check if identiy cookie is set, if not set it. * * @return void */ public static function maybe_set_identity_cookie() { // Do not set on AJAX requests. if ( Constants::is_true( 'DOING_AJAX' ) ) { return; } // Bail if cookie already set. if ( isset( $_COOKIE['tk_ai'] ) ) { return; } $user = wp_get_current_user(); // We don't want to track user events during unit tests/CI runs. if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) { return false; } $user_id = $user->ID; $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); // If an id is still not found, create one and save it. if ( ! $anon_id ) { $anon_id = self::get_anon_id(); update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); } // Don't set cookie on API requests. if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) { wc_setcookie( 'tk_ai', $anon_id ); } } /** * Record a Tracks event * * @param array $event Array of event properties. * @return bool|WP_Error True on success, WP_Error on failure. */ public static function record_event( $event ) { if ( ! $event instanceof WC_Tracks_Event ) { $event = new WC_Tracks_Event( $event ); } if ( is_wp_error( $event ) ) { return $event; } $pixel = $event->build_pixel_url( $event ); if ( ! $pixel ) { return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); } return self::record_pixel( $pixel ); } /** * Synchronously request the pixel. * * @param string $pixel pixel url and query string. * @return bool Always returns true. */ public static function record_pixel( $pixel ) { // Add the Request Timestamp and URL terminator just before the HTTP request. $pixel .= '&_rt=' . self::build_timestamp() . '&_=_'; wp_safe_remote_get( $pixel, array( 'blocking' => false, 'redirection' => 2, 'httpversion' => '1.1', 'timeout' => 1, ) ); return true; } /** * Create a timestap representing milliseconds since 1970-01-01 * * @return string A string representing a timestamp. */ public static function build_timestamp() { $ts = NumberUtil::round( microtime( true ) * 1000 ); return number_format( $ts, 0, '', '' ); } /** * Get a user's identity to send to Tracks. If Jetpack exists, default to its implementation. * * @param int $user_id User id. * @return array Identity properties. */ public static function get_identity( $user_id ) { $jetpack_lib = '/tracks/client.php'; if ( class_exists( 'Jetpack' ) && Constants::is_defined( 'JETPACK__VERSION' ) ) { if ( version_compare( Constants::get_constant( 'JETPACK__VERSION' ), '7.5', '<' ) ) { if ( file_exists( jetpack_require_lib_dir() . $jetpack_lib ) ) { include_once jetpack_require_lib_dir() . $jetpack_lib; if ( function_exists( 'jetpack_tracks_get_identity' ) ) { return jetpack_tracks_get_identity( $user_id ); } } } else { $tracking = new Automattic\Jetpack\Tracking(); return $tracking->tracks_get_identity( $user_id ); } } // Start with a previously set cookie. $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : false; // If there is no cookie, apply a saved id. if ( ! $anon_id ) { $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); } // If an id is still not found, create one and save it. if ( ! $anon_id ) { $anon_id = self::get_anon_id(); update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); } return array( '_ut' => 'anon', '_ui' => $anon_id, ); } /** * Grabs the user's anon id from cookies, or generates and sets a new one * * @return string An anon id for the user */ public static function get_anon_id() { static $anon_id = null; if ( ! isset( $anon_id ) ) { // Did the browser send us a cookie? if ( isset( $_COOKIE['tk_ai'] ) ) { $anon_id = sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ); } else { $binary = ''; // Generate a new anonId and try to save it in the browser's cookies. // Note that base64-encoding an 18 character string generates a 24-character anon id. for ( $i = 0; $i < 18; ++$i ) { $binary .= chr( wp_rand( 0, 255 ) ); } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $anon_id = 'woo:' . base64_encode( $binary ); } } return $anon_id; } } WC_Tracks_Client::init(); includes/tracks/class-wc-tracks-footer-pixel.php 0000644 00000004214 15132754524 0015777 0 ustar 00 <?php /** * Send Tracks events on behalf of a user using pixel images in page footer. * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * WC_Tracks_Footer_Pixel class. */ class WC_Tracks_Footer_Pixel { /** * Singleton instance. * * @var WC_Tracks_Footer_Pixel */ protected static $instance = null; /** * Events to send to Tracks. * * @var array */ protected $events = array(); /** * Instantiate the singleton. * * @return WC_Tracks_Footer_Pixel */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new WC_Tracks_Footer_Pixel(); } return self::$instance; } /** * Constructor - attach hooks to the singleton instance. */ public function __construct() { add_action( 'admin_footer', array( $this, 'render_tracking_pixels' ) ); add_action( 'shutdown', array( $this, 'send_tracks_requests' ) ); } /** * Record a Tracks event * * @param array $event Array of event properties. * @return bool|WP_Error True on success, WP_Error on failure. */ public static function record_event( $event ) { if ( ! $event instanceof WC_Tracks_Event ) { $event = new WC_Tracks_Event( $event ); } if ( is_wp_error( $event ) ) { return $event; } self::instance()->add_event( $event ); return true; } /** * Add a Tracks event to the queue. * * @param WC_Tracks_Event $event Event to track. */ public function add_event( $event ) { $this->events[] = $event; } /** * Add events as tracking pixels to page footer. */ public function render_tracking_pixels() { if ( empty( $this->events ) ) { return; } foreach ( $this->events as $event ) { $pixel = $event->build_pixel_url(); if ( ! $pixel ) { continue; } echo '<img style="position: fixed;" src="', esc_url( $pixel ), '" />'; } $this->events = array(); } /** * Fire off API calls for events that weren't converted to pixels. * * This handles wp_redirect(). */ public function send_tracks_requests() { if ( empty( $this->events ) ) { return; } foreach ( $this->events as $event ) { WC_Tracks_Client::record_event( $event ); } } } includes/tracks/events/class-wc-coupons-tracking.php 0000644 00000003472 15132754524 0016674 0 ustar 00 <?php /** * WooCommerce Coupons Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Orders. */ class WC_Coupons_Tracking { /** * Init tracking. */ public function init() { add_action( 'load-edit.php', array( $this, 'tracks_coupons_events' ), 10 ); } /** * Add a listener on the "Apply" button to track bulk actions. */ public function tracks_coupons_bulk_actions() { wc_enqueue_js( " function onApplyBulkActions( event ) { var id = event.data.id; var action = $( '#' + id ).val(); if ( action && '-1' !== action ) { window.wcTracks.recordEvent( 'coupons_view_bulk_action', { action: action } ); } } $( '#doaction' ).on( 'click', { id: 'bulk-action-selector-top' }, onApplyBulkActions ); $( '#doaction2' ).on( 'click', { id: 'bulk-action-selector-bottom' }, onApplyBulkActions ); " ); } /** * Track page view events. */ public function tracks_coupons_events() { if ( isset( $_GET['post_type'] ) && 'shop_coupon' === $_GET['post_type'] ) { $this->tracks_coupons_bulk_actions(); WC_Tracks::record_event( 'coupons_view', array( 'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : 'all', ) ); if ( isset( $_GET['filter_action'] ) && 'Filter' === sanitize_text_field( wp_unslash( $_GET['filter_action'] ) ) && isset( $_GET['coupon_type'] ) ) { WC_Tracks::record_event( 'coupons_filter', array( 'filter' => 'coupon_type', 'value' => sanitize_text_field( wp_unslash( $_GET['coupon_type'] ) ), ) ); } if ( isset( $_GET['s'] ) && 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) ) { WC_Tracks::record_event( 'coupons_search' ); } } } } includes/tracks/events/class-wc-products-tracking.php 0000644 00000014356 15132754524 0017054 0 ustar 00 <?php /** * WooCommerce Import Tracking * * @package WooCommerce\Tracks */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Products. */ class WC_Products_Tracking { /** * Init tracking. */ public function init() { add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 ); add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 ); add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 ); add_action( 'transition_post_status', array( $this, 'track_product_published' ), 10, 3 ); add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) ); add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 ); } /** * Send a Tracks event when the Products page is viewed. */ public function track_products_view() { // We only record Tracks event when no `_wp_http_referer` query arg is set, since // when searching, the request gets sent from the browser twice, // once with the `_wp_http_referer` and once without it. // // Otherwise, we would double-record the view and search events. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification if ( isset( $_GET['post_type'] ) && 'product' === wp_unslash( $_GET['post_type'] ) && ! isset( $_GET['_wp_http_referer'] ) ) { // phpcs:enable WC_Tracks::record_event( 'products_view' ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification if ( isset( $_GET['s'] ) && 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) ) { // phpcs:enable WC_Tracks::record_event( 'products_search' ); } } } /** * Send a Tracks event when the Products Categories and Tags page is viewed. */ public function track_categories_and_tags_view() { // We only record Tracks event when no `_wp_http_referer` query arg is set, since // when searching, the request gets sent from the browser twice, // once with the `_wp_http_referer` and once without it. // // Otherwise, we would double-record the view and search events. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification if ( isset( $_GET['post_type'] ) && 'product' === wp_unslash( $_GET['post_type'] ) && isset( $_GET['taxonomy'] ) && ! isset( $_GET['_wp_http_referer'] ) ) { $taxonomy = wp_unslash( $_GET['taxonomy'] ); // phpcs:enable if ( 'product_cat' === $taxonomy ) { WC_Tracks::record_event( 'categories_view' ); } elseif ( 'product_tag' === $taxonomy ) { WC_Tracks::record_event( 'tags_view' ); } // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification if ( isset( $_GET['s'] ) && 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) ) { // phpcs:enable if ( 'product_cat' === $taxonomy ) { WC_Tracks::record_event( 'categories_search' ); } elseif ( 'product_tag' === $taxonomy ) { WC_Tracks::record_event( 'tags_search' ); } } } } /** * Send a Tracks event when a product is updated. * * @param int $product_id Product id. * @param object $post WordPress post. */ public function track_product_updated( $product_id, $post ) { if ( 'product' !== $post->post_type ) { return; } $properties = array( 'product_id' => $product_id, ); WC_Tracks::record_event( 'product_edit', $properties ); } /** * Track the Update button being clicked on the client side. * This is needed because `track_product_updated` (using the `edit_post` * hook) is called in response to a number of other triggers. * * @param WP_Post $post The post, not used. */ public function track_product_updated_client_side( $post ) { wc_enqueue_js( " if ( $( 'h1.wp-heading-inline' ).text().trim() === '" . __( 'Edit product', 'woocommerce' ) . "') { var initialStockValue = $( '#_stock' ).val(); var hasRecordedEvent = false; $( '#publish' ).on( 'click', function() { if ( hasRecordedEvent ) { return; } var currentStockValue = $( '#_stock' ).val(); var properties = { product_type: $( '#product-type' ).val(), is_virtual: $( '#_virtual' ).is( ':checked' ) ? 'Y' : 'N', is_downloadable: $( '#_downloadable' ).is( ':checked' ) ? 'Y' : 'N', manage_stock: $( '#_manage_stock' ).is( ':checked' ) ? 'Y' : 'N', stock_quantity_update: ( initialStockValue != currentStockValue ) ? 'Y' : 'N', }; window.wcTracks.recordEvent( 'product_update', properties ); hasRecordedEvent = true; } ); } " ); } /** * Send a Tracks event when a product is published. * * @param string $new_status New post_status. * @param string $old_status Previous post_status. * @param object $post WordPress post. */ public function track_product_published( $new_status, $old_status, $post ) { if ( 'product' !== $post->post_type || 'publish' !== $new_status || 'publish' === $old_status ) { return; } $properties = array( 'product_id' => $post->ID, ); WC_Tracks::record_event( 'product_add_publish', $properties ); } /** * Send a Tracks event when a product category is created. * * @param int $category_id Category ID. */ public function track_product_category_created( $category_id ) { // phpcs:disable WordPress.Security.NonceVerification.Missing // Only track category creation from the edit product screen or the // category management screen (which both occur via AJAX). if ( ! Constants::is_defined( 'DOING_AJAX' ) || empty( $_POST['action'] ) || ( // Product Categories screen. 'add-tag' !== $_POST['action'] && // Edit Product screen. 'add-product_cat' !== $_POST['action'] ) ) { return; } $category = get_term( $category_id, 'product_cat' ); $properties = array( 'category_id' => $category_id, 'parent_id' => $category->parent, 'page' => ( 'add-tag' === $_POST['action'] ) ? 'categories' : 'product', ); // phpcs:enable WC_Tracks::record_event( 'product_category_add', $properties ); } } includes/tracks/events/class-wc-importer-tracking.php 0000644 00000004313 15132754524 0017042 0 ustar 00 <?php /** * WooCommerce Import Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Imports. */ class WC_Importer_Tracking { /** * Init tracking. */ public function init() { add_action( 'product_page_product_importer', array( $this, 'track_product_importer' ) ); } /** * Route product importer action to the right callback. * * @return void */ public function track_product_importer() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['step'] ) ) { return; } if ( 'import' === $_REQUEST['step'] ) { return $this->track_product_importer_start(); } if ( 'done' === $_REQUEST['step'] ) { return $this->track_product_importer_complete(); } // phpcs:enable } /** * Send a Tracks event when the product importer is started. * * @return void */ public function track_product_importer_start() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['file'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) { return; } $properties = array( 'update_existing' => isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false, 'delimiter' => empty( $_REQUEST['delimiter'] ) ? ',' : wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ), ); // phpcs:enable WC_Tracks::record_event( 'product_import_start', $properties ); } /** * Send a Tracks event when the product importer has finished. * * @return void */ public function track_product_importer_complete() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['nonce'] ) ) { return; } $properties = array( 'imported' => isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0, 'updated' => isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0, 'failed' => isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0, 'skipped' => isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0, ); // phpcs:enable WC_Tracks::record_event( 'product_import_complete', $properties ); } } includes/tracks/events/class-wc-order-tracking.php 0000644 00000001611 15132754524 0016312 0 ustar 00 <?php /** * WooCommerce Order Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of a WooCommerce Order. */ class WC_Order_Tracking { /** * Init tracking. */ public function init() { add_action( 'woocommerce_admin_order_data_after_order_details', array( $this, 'track_order_viewed' ) ); } /** * Send a Tracks event when an order is viewed. * * @param WC_Order $order Order. */ public function track_order_viewed( $order ) { if ( ! $order instanceof WC_Order || ! $order->get_id() ) { return; } $properties = array( 'current_status' => $order->get_status(), 'date_created' => $order->get_date_created() ? $order->get_date_created()->format( DateTime::ATOM ) : '', 'payment_method' => $order->get_payment_method(), ); WC_Tracks::record_event( 'single_order_view', $properties ); } } includes/tracks/events/class-wc-settings-tracking.php 0000644 00000006123 15132754524 0017042 0 ustar 00 <?php /** * WooCommerce Settings Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Settings. */ class WC_Settings_Tracking { /** * List of allowed WooCommerce settings to potentially track updates for. * * @var array */ protected $allowed_options = array(); /** * WooCommerce settings that have been updated (and will be tracked). * * @var array */ protected $updated_options = array(); /** * Init tracking. */ public function init() { add_action( 'woocommerce_settings_page_init', array( $this, 'track_settings_page_view' ) ); add_action( 'woocommerce_update_option', array( $this, 'add_option_to_list' ) ); add_action( 'woocommerce_update_options', array( $this, 'send_settings_change_event' ) ); } /** * Add a WooCommerce option name to our allowed options list and attach * the `update_option` hook. Rather than inspecting every updated * option and pattern matching for "woocommerce", just build a dynamic * list for WooCommerce options that might get updated. * * See `woocommerce_update_option` hook. * * @param array $option WooCommerce option (config) that might get updated. */ public function add_option_to_list( $option ) { $this->allowed_options[] = $option['id']; // Delay attaching this action since it could get fired a lot. if ( false === has_action( 'update_option', array( $this, 'track_setting_change' ) ) ) { add_action( 'update_option', array( $this, 'track_setting_change' ), 10, 3 ); } } /** * Add WooCommerce option to a list of updated options. * * @param string $option_name Option being updated. * @param mixed $old_value Old value of option. * @param mixed $new_value New value of option. */ public function track_setting_change( $option_name, $old_value, $new_value ) { // Make sure this is a WooCommerce option. if ( ! in_array( $option_name, $this->allowed_options, true ) ) { return; } // Check to make sure the new value is truly different. // `woocommerce_price_num_decimals` tends to trigger this // because form values aren't coerced (e.g. '2' vs. 2). if ( is_scalar( $old_value ) && is_scalar( $new_value ) && (string) $old_value === (string) $new_value ) { return; } $this->updated_options[] = $option_name; } /** * Send a Tracks event for WooCommerce options that changed values. */ public function send_settings_change_event() { global $current_tab; if ( empty( $this->updated_options ) ) { return; } $properties = array( 'settings' => implode( ',', $this->updated_options ), ); if ( isset( $current_tab ) ) { $properties['tab'] = $current_tab; } WC_Tracks::record_event( 'settings_change', $properties ); } /** * Send a Tracks event for WooCommerce settings page views. */ public function track_settings_page_view() { global $current_tab, $current_section; $properties = array( 'tab' => $current_tab, 'section' => empty( $current_section ) ? null : $current_section, ); WC_Tracks::record_event( 'settings_view', $properties ); } } includes/tracks/events/class-wc-admin-setup-wizard-tracking.php 0000644 00000011425 15132754524 0020727 0 ustar 00 <?php /** * WooCommerce Admin Setup Wizard Tracking * * @package WooCommerce\Tracks * * @deprecated 4.6.0 */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of the WooCommerce Onboarding Wizard. */ class WC_Admin_Setup_Wizard_Tracking { /** * Steps for the setup wizard * * @var array */ private $steps = array(); /** * Init tracking. * * @deprecated 4.6.0 */ public function init() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Get the name of the current step. * * @deprecated 4.6.0 * @return string */ public function get_current_step() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); return isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Add footer scripts to OBW via woocommerce_setup_footer * * @deprecated 4.6.0 */ public function add_footer_scripts() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Dequeue unwanted scripts from OBW footer. * * @deprecated 4.6.0 */ public function dequeue_non_allowed_scripts() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); global $wp_scripts; $allowed = array( 'woo-tracks' ); foreach ( $wp_scripts->queue as $script ) { if ( in_array( $script, $allowed, true ) ) { continue; } wp_dequeue_script( $script ); } } /** * Track when tracking is opted into and OBW has started. * * @param string $option Option name. * @param string $value Option value. * * @deprecated 4.6.0 */ public function track_start( $option, $value ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track the marketing form on submit. * * @deprecated 4.6.0 */ public function track_ready_next_steps() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track various events when a step is saved. * * @deprecated 4.6.0 */ public function add_step_save_events() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track store setup and store properties on save. * * @deprecated 4.6.0 */ public function track_store_setup() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track payment gateways selected. * * @deprecated 4.6.0 */ public function track_payments() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track shipping units and whether or not labels are set. * * @deprecated 4.6.0 */ public function track_shipping() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track recommended plugins selected for install. * * @deprecated 4.6.0 */ public function track_recommended() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Tracks when Jetpack is activated through the OBW. * * @deprecated 4.6.0 */ public function track_jetpack_activate() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Tracks when last next_steps screen is viewed in the OBW. * * @deprecated 4.6.0 */ public function track_next_steps() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Track skipped steps. * * @deprecated 4.6.0 */ public function track_skip_step() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Set the OBW steps inside this class instance. * * @param array $steps Array of OBW steps. * * @deprecated 4.6.0 */ public function set_obw_steps( $steps ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); $this->steps = $steps; return $steps; } } includes/tracks/events/class-wc-extensions-tracking.php 0000644 00000006103 15132754524 0017377 0 ustar 00 <?php /** * WooCommerce Extensions Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of the WooCommerce Extensions page. */ class WC_Extensions_Tracking { /** * Init tracking. */ public function init() { add_action( 'load-woocommerce_page_wc-addons', array( $this, 'track_extensions_page' ) ); add_action( 'woocommerce_helper_connect_start', array( $this, 'track_helper_connection_start' ) ); add_action( 'woocommerce_helper_denied', array( $this, 'track_helper_connection_cancelled' ) ); add_action( 'woocommerce_helper_connected', array( $this, 'track_helper_connection_complete' ) ); add_action( 'woocommerce_helper_disconnected', array( $this, 'track_helper_disconnected' ) ); add_action( 'woocommerce_helper_subscriptions_refresh', array( $this, 'track_helper_subscriptions_refresh' ) ); add_action( 'woocommerce_addon_installed', array( $this, 'track_addon_install' ), 10, 2 ); } /** * Send a Tracks event when an Extensions page is viewed. */ public function track_extensions_page() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $properties = array( 'section' => empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ), ); $event = 'extensions_view'; if ( 'helper' === $properties['section'] ) { $event = 'subscriptions_view'; } if ( ! empty( $_REQUEST['search'] ) ) { $event = 'extensions_view_search'; $properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) ); } // phpcs:enable WC_Tracks::record_event( $event, $properties ); } /** * Send a Tracks even when a Helper connection process is initiated. */ public function track_helper_connection_start() { WC_Tracks::record_event( 'extensions_subscriptions_connect' ); } /** * Send a Tracks even when a Helper connection process is cancelled. */ public function track_helper_connection_cancelled() { WC_Tracks::record_event( 'extensions_subscriptions_cancelled' ); } /** * Send a Tracks even when a Helper connection process completed successfully. */ public function track_helper_connection_complete() { WC_Tracks::record_event( 'extensions_subscriptions_connected' ); } /** * Send a Tracks even when a Helper has been disconnected. */ public function track_helper_disconnected() { WC_Tracks::record_event( 'extensions_subscriptions_disconnect' ); } /** * Send a Tracks even when Helper subscriptions are refreshed. */ public function track_helper_subscriptions_refresh() { WC_Tracks::record_event( 'extensions_subscriptions_update' ); } /** * Send a Tracks event when addon is installed via the Extensions page. * * @param string $addon_id Addon slug. * @param string $section Extensions tab. */ public function track_addon_install( $addon_id, $section ) { $properties = array( 'context' => 'extensions', 'section' => $section, ); if ( 'woocommerce-payments' === $addon_id ) { WC_Tracks::record_event( 'woocommerce_payments_install', $properties ); } } } includes/tracks/events/class-wc-orders-tracking.php 0000644 00000013575 15132754524 0016511 0 ustar 00 <?php /** * WooCommerce Orders Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Orders. */ class WC_Orders_Tracking { /** * Init tracking. */ public function init() { add_action( 'woocommerce_order_status_changed', array( $this, 'track_order_status_change' ), 10, 3 ); add_action( 'load-edit.php', array( $this, 'track_orders_view' ), 10 ); add_action( 'pre_post_update', array( $this, 'track_created_date_change' ), 10 ); // WC_Meta_Box_Order_Actions::save() hooks in at priority 50. add_action( 'woocommerce_process_shop_order_meta', array( $this, 'track_order_action' ), 51 ); add_action( 'load-post-new.php', array( $this, 'track_add_order_from_edit' ), 10 ); add_filter( 'woocommerce_shop_order_search_results', array( $this, 'track_order_search' ), 10, 3 ); } /** * Send a track event when on the Order Listing page, and search results are being displayed. * * @param array $order_ids Array of order_ids that are matches for the search. * @param string $term The string that was used in the search. * @param array $search_fields Fields that were used in the original search. */ public function track_order_search( $order_ids, $term, $search_fields ) { // Since `woocommerce_shop_order_search_results` can run in the front-end context, exit if get_current_screen isn't defined. if ( ! function_exists( 'get_current_screen' ) ) { return $order_ids; } $screen = get_current_screen(); // We only want to record this track when the filter is executed on the order listing page. if ( 'edit-shop_order' === $screen->id ) { // we are on the order listing page, and query results are being shown. WC_Tracks::record_event( 'orders_view_search' ); } return $order_ids; } /** * Send a Tracks event when the Orders page is viewed. */ public function track_orders_view() { if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput $properties = array( 'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( $_GET['post_status'] ) : 'all', ); // phpcs:enable WC_Tracks::record_event( 'orders_view', $properties ); } } /** * Send a Tracks event when an order status is changed. * * @param int $id Order id. * @param string $previous_status the old WooCommerce order status. * @param string $next_status the new WooCommerce order status. */ public function track_order_status_change( $id, $previous_status, $next_status ) { $order = wc_get_order( $id ); $properties = array( 'order_id' => $id, 'next_status' => $next_status, 'previous_status' => $previous_status, 'date_created' => $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d' ) : '', 'payment_method' => $order->get_payment_method(), 'order_total' => $order->get_total(), ); WC_Tracks::record_event( 'orders_edit_status_change', $properties ); } /** * Send a Tracks event when an order date is changed. * * @param int $id Order id. */ public function track_created_date_change( $id ) { $post_type = get_post_type( $id ); if ( 'shop_order' !== $post_type ) { return; } if ( 'auto-draft' === get_post_status( $id ) ) { return; } $order = wc_get_order( $id ); $date_created = $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d H:i:s' ) : ''; // phpcs:disable WordPress.Security.NonceVerification $new_date = sprintf( '%s %2d:%2d:%2d', isset( $_POST['order_date'] ) ? wc_clean( wp_unslash( $_POST['order_date'] ) ) : '', isset( $_POST['order_date_hour'] ) ? wc_clean( wp_unslash( $_POST['order_date_hour'] ) ) : '', isset( $_POST['order_date_minute'] ) ? wc_clean( wp_unslash( $_POST['order_date_minute'] ) ) : '', isset( $_POST['order_date_second'] ) ? wc_clean( wp_unslash( $_POST['order_date_second'] ) ) : '' ); // phpcs:enable if ( $new_date !== $date_created ) { $properties = array( 'order_id' => $id, 'status' => $order->get_status(), ); WC_Tracks::record_event( 'order_edit_date_created', $properties ); } } /** * Track order actions taken. * * @param int $order_id Order ID. */ public function track_order_action( $order_id ) { // phpcs:disable WordPress.Security.NonceVerification if ( ! empty( $_POST['wc_order_action'] ) ) { $order = wc_get_order( $order_id ); $action = wc_clean( wp_unslash( $_POST['wc_order_action'] ) ); $properties = array( 'order_id' => $order_id, 'status' => $order->get_status(), 'action' => $action, ); WC_Tracks::record_event( 'order_edit_order_action', $properties ); } // phpcs:enable } /** * Track "add order" button on the Edit Order screen. */ public function track_add_order_from_edit() { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) { $referer = wp_get_referer(); if ( $referer ) { $referring_page = wp_parse_url( $referer ); $referring_args = array(); $post_edit_page = wp_parse_url( admin_url( 'post.php' ) ); if ( ! empty( $referring_page['query'] ) ) { parse_str( $referring_page['query'], $referring_args ); } // Determine if we arrived from an Order Edit screen. if ( $post_edit_page['path'] === $referring_page['path'] && isset( $referring_args['action'] ) && 'edit' === $referring_args['action'] && isset( $referring_args['post'] ) && 'shop_order' === get_post_type( $referring_args['post'] ) ) { WC_Tracks::record_event( 'order_edit_add_order' ); } } } } } includes/tracks/events/class-wc-coupon-tracking.php 0000644 00000002205 15132754524 0016502 0 ustar 00 <?php /** * WooCommerce Coupon Tracking * * @package WooCommerce\Tracks */ /** * This class adds actions to track usage of a WooCommerce Coupon. */ class WC_Coupon_Tracking { /** * Init tracking. */ public function init() { add_action( 'woocommerce_coupon_object_updated_props', array( $this, 'track_coupon_updated' ), 10, 2 ); } /** * Send a Tracks event when a coupon is updated. * * @param WC_Coupon $coupon The coupon that has been updated. * @param Array $updated_props The props of the coupon that have been updated. */ public function track_coupon_updated( $coupon, $updated_props ) { $properties = array( 'discount_code' => $coupon->get_code(), 'free_shipping' => $coupon->get_free_shipping(), 'individual_use' => $coupon->get_individual_use(), 'exclude_sale_items' => $coupon->get_exclude_sale_items(), 'usage_limits_applied' => 0 < intval( $coupon->get_usage_limit() ) || 0 < intval( $coupon->get_usage_limit_per_user() ) || 0 < intval( $coupon->get_limit_usage_to_x_items() ), ); WC_Tracks::record_event( 'coupon_updated', $properties ); } } includes/tracks/events/class-wc-status-tracking.php 0000644 00000002025 15132754524 0016522 0 ustar 00 <?php /** * WooCommerce Status Tracking * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce Orders. */ class WC_Status_Tracking { /** * Init tracking. */ public function init() { add_action( 'admin_init', array( $this, 'track_status_view' ), 10 ); } /** * Add Tracks events to the status page. */ public function track_status_view() { if ( isset( $_GET['page'] ) && 'wc-status' === sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) { $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status'; WC_Tracks::record_event( 'status_view', array( 'tab' => $tab, 'tool_used' => isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null, ) ); if ( 'status' === $tab ) { wc_enqueue_js( " $( 'a.debug-report' ).on( 'click', function() { window.wcTracks.recordEvent( 'status_view_reports' ); } ); " ); } } } } includes/tracks/class-wc-site-tracking.php 0000644 00000013354 15132754524 0014646 0 ustar 00 <?php /** * Nosara Tracks for WooCommerce * * @package WooCommerce\Tracks */ defined( 'ABSPATH' ) || exit; /** * This class adds actions to track usage of WooCommerce. */ class WC_Site_Tracking { /** * Check if tracking is enabled. * * @return bool */ public static function is_tracking_enabled() { /** * Don't track users if a filter has been applied to turn it off. * `woocommerce_apply_tracking` will be deprecated. Please use * `woocommerce_apply_user_tracking` instead. */ if ( ! apply_filters( 'woocommerce_apply_user_tracking', true ) || ! apply_filters( 'woocommerce_apply_tracking', true ) ) { return false; } // Check if tracking is actively being opted into. $is_obw_opting_in = isset( $_POST['wc_tracker_checkbox'] ) && 'yes' === sanitize_text_field( $_POST['wc_tracker_checkbox'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput /** * Don't track users who haven't opted-in to tracking or aren't in * the process of opting-in. */ if ( 'yes' !== get_option( 'woocommerce_allow_tracking' ) && ! $is_obw_opting_in ) { return false; } if ( ! class_exists( 'WC_Tracks' ) ) { return false; } return true; } /** * Register scripts required to record events from javascript. */ public static function register_scripts() { wp_register_script( 'woo-tracks', 'https://stats.wp.com/w.js', array( 'wp-hooks' ), gmdate( 'YW' ), false ); } /** * Add scripts required to record events from javascript. */ public static function enqueue_scripts() { wp_enqueue_script( 'woo-tracks' ); } /** * Adds the tracking function to the admin footer. */ public static function add_tracking_function() { ?> <!-- WooCommerce Tracks --> <script type="text/javascript"> window.wcTracks = window.wcTracks || {}; window.wcTracks.isEnabled = <?php echo self::is_tracking_enabled() ? 'true' : 'false'; ?>; window.wcTracks.recordEvent = function( name, properties ) { if ( ! window.wcTracks.isEnabled ) { return; } var eventName = '<?php echo esc_attr( WC_Tracks::PREFIX ); ?>' + name; var eventProperties = properties || {}; eventProperties.url = '<?php echo esc_html( home_url() ); ?>' eventProperties.products_count = '<?php echo intval( WC_Tracks::get_products_count() ); ?>'; if ( window.wp && window.wp.hooks && window.wp.hooks.applyFilters ) { eventProperties = window.wp.hooks.applyFilters( 'woocommerce_tracks_client_event_properties', eventProperties, eventName ); delete( eventProperties._ui ); delete( eventProperties._ut ); } window._tkq = window._tkq || []; window._tkq.push( [ 'recordEvent', eventName, eventProperties ] ); } </script> <?php } /** * Adds a function to load tracking scripts and enable them client-side on the fly. * Note that this function does not update `woocommerce_allow_tracking` in the database * and will not persist enabled tracking across page loads. */ public static function add_enable_tracking_function() { global $wp_scripts; if ( ! isset( $wp_scripts->registered['woo-tracks'] ) ) { return; } $woo_tracks_script = $wp_scripts->registered['woo-tracks']->src; ?> <script type="text/javascript"> window.wcTracks.enable = function( callback ) { window.wcTracks.isEnabled = true; var scriptUrl = '<?php echo esc_url( $woo_tracks_script ); ?>'; var existingScript = document.querySelector( `script[src="${ scriptUrl }"]` ); if ( existingScript ) { return; } var script = document.createElement('script'); script.src = scriptUrl; document.body.append(script); // Callback after scripts have loaded. script.onload = function() { if ( 'function' === typeof callback ) { callback( true ); } } // Callback triggered if the script fails to load. script.onerror = function() { if ( 'function' === typeof callback ) { callback( false ); } } } </script> <?php } /** * Init tracking. */ public static function init() { // Define window.wcTracks.recordEvent in case it is enabled client-side. self::register_scripts(); add_filter( 'admin_footer', array( __CLASS__, 'add_tracking_function' ), 24 ); if ( ! self::is_tracking_enabled() ) { add_filter( 'admin_footer', array( __CLASS__, 'add_enable_tracking_function' ), 24 ); return; } self::enqueue_scripts(); include_once WC_ABSPATH . 'includes/tracks/events/class-wc-admin-setup-wizard-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-extensions-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-importer-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-products-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-orders-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-settings-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-status-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupons-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-order-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupon-tracking.php'; $tracking_classes = array( 'WC_Extensions_Tracking', 'WC_Importer_Tracking', 'WC_Products_Tracking', 'WC_Orders_Tracking', 'WC_Settings_Tracking', 'WC_Status_Tracking', 'WC_Coupons_Tracking', 'WC_Order_Tracking', 'WC_Coupon_Tracking', ); foreach ( $tracking_classes as $tracking_class ) { $tracker_instance = new $tracking_class(); $tracker_init_method = array( $tracker_instance, 'init' ); if ( is_callable( $tracker_init_method ) ) { call_user_func( $tracker_init_method ); } } } } includes/tracks/class-wc-tracks.php 0000644 00000007232 15132754524 0013367 0 ustar 00 <?php /** * PHP Tracks Client * * @package WooCommerce\Tracks */ /** * WC_Tracks class. */ class WC_Tracks { /** * Tracks event name prefix. */ const PREFIX = 'wcadmin_'; /** * Get total product counts. * * @return int Number of products. */ public static function get_products_count() { $product_counts = WC_Tracker::get_product_counts(); return $product_counts['total']; } /** * Gather blog related properties. * * @param int $user_id User id. * @return array Blog details. */ public static function get_blog_details( $user_id ) { $blog_details = get_transient( 'wc_tracks_blog_details' ); if ( false === $blog_details ) { $blog_details = array( 'url' => home_url(), 'blog_lang' => get_user_locale( $user_id ), 'blog_id' => class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null, 'products_count' => self::get_products_count(), 'wc_version' => WC()->version, ); set_transient( 'wc_tracks_blog_details', $blog_details, DAY_IN_SECONDS ); } return $blog_details; } /** * Gather details from the request to the server. * * @return array Server details. */ public static function get_server_details() { $data = array(); $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; $data['_dr'] = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; $uri = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $host = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; $data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : ''; return $data; } /** * Record an event in Tracks - this is the preferred way to record events from PHP. * * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. * @return bool|WP_Error True for success or WP_Error if the event pixel could not be fired. */ public static function record_event( $event_name, $properties = array() ) { /** * Don't track users who don't have tracking enabled. */ if ( ! WC_Site_Tracking::is_tracking_enabled() ) { return false; } $user = wp_get_current_user(); // We don't want to track user events during unit tests/CI runs. if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) { return false; } $prefixed_event_name = self::PREFIX . $event_name; $data = array( '_en' => $prefixed_event_name, '_ts' => WC_Tracks_Client::build_timestamp(), ); $server_details = self::get_server_details(); $identity = WC_Tracks_Client::get_identity( $user->ID ); $blog_details = self::get_blog_details( $user->ID ); // Allow event props to be filtered to enable adding site-wide props. $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $properties, $prefixed_event_name ); // Delete _ui and _ut protected properties. unset( $filtered_properties['_ui'] ); unset( $filtered_properties['_ut'] ); $event_obj = new WC_Tracks_Event( array_merge( $data, $server_details, $identity, $blog_details, $filtered_properties ) ); if ( is_wp_error( $event_obj->error ) ) { return $event_obj->error; } return $event_obj->record(); } } includes/tracks/class-wc-tracks-event.php 0000644 00000007131 15132754524 0014504 0 ustar 00 <?php /** * This class represents an event used to record a Tracks event * * @package WooCommerce\Tracks */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Tracks_Event class. */ class WC_Tracks_Event { /** * Event name regex. */ const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/'; /** * Property name regex. */ const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/'; /** * Error message as WP_Error. * * @var WP_Error */ public $error; /** * WC_Tracks_Event constructor. * * @param array $event Event properties. */ public function __construct( $event ) { $_event = self::validate_and_sanitize( $event ); if ( is_wp_error( $_event ) ) { $this->error = $_event; return; } foreach ( $_event as $key => $value ) { $this->{$key} = $value; } } /** * Record Tracks event * * @return bool Always returns true. */ public function record() { if ( wp_doing_ajax() || Constants::is_true( 'REST_REQUEST' ) ) { return WC_Tracks_Client::record_event( $this ); } return WC_Tracks_Footer_Pixel::record_event( $this ); } /** * Annotate the event with all relevant info. * * @param array $event Event arguments. * @return bool|WP_Error True on success, WP_Error on failure. */ public static function validate_and_sanitize( $event ) { $event = (object) $event; // Required. if ( ! $event->_en ) { return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 ); } // Delete non-routable addresses otherwise geoip will discard the record entirely. if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) { unset( $event->_via_ip ); } $validated = array( 'browser_type' => WC_Tracks_Client::BROWSER_TYPE, ); $_event = (object) array_merge( (array) $event, $validated ); // If you want to block property names, do it here. // Make sure we have an event timestamp. if ( ! isset( $_event->_ts ) ) { $_event->_ts = WC_Tracks_Client::build_timestamp(); } return $_event; } /** * Build a pixel URL that will send a Tracks event when fired. * On error, returns an empty string (''). * * @return string A pixel URL or empty string ('') if there were invalid args. */ public function build_pixel_url() { if ( $this->error ) { return ''; } $args = get_object_vars( $this ); // Request Timestamp and URL Terminator must be added just before the HTTP request or not at all. unset( $args['_rt'], $args['_'] ); $validated = self::validate_and_sanitize( $args ); if ( is_wp_error( $validated ) ) { return ''; } return esc_url_raw( WC_Tracks_Client::PIXEL . '?' . http_build_query( $validated ) ); } /** * Check if event name is valid. * * @param string $name Event name. * @return false|int */ public static function event_name_is_valid( $name ) { return preg_match( self::EVENT_NAME_REGEX, $name ); } /** * Check if a property name is valid. * * @param string $name Event property. * @return false|int */ public static function prop_name_is_valid( $name ) { return preg_match( self::PROP_NAME_REGEX, $name ); } /** * Check event names * * @param object $event An event object. */ public static function scrutinize_event_names( $event ) { if ( ! self::event_name_is_valid( $event->_en ) ) { return; } $allowed_key_names = array( 'anonId', 'Browser_Type', ); foreach ( array_keys( (array) $event ) as $key ) { if ( in_array( $key, $allowed_key_names, true ) ) { continue; } if ( ! self::prop_name_is_valid( $key ) ) { return; } } } } includes/class-wc-api.php 0000644 00000011761 15132754524 0011364 0 ustar 00 <?php /** * WC-API endpoint handler. * * This handles API related functionality in WooCommerce. * - wc-api endpoint - Commonly used by Payment gateways for callbacks. * - Legacy REST API - Deprecated in 2.6.0. @see class-wc-legacy-api.php * - WP REST API - The main REST API in WooCommerce which is built on top of the WP REST API. * * @package WooCommerce\RestApi * @since 2.0.0 */ defined( 'ABSPATH' ) || exit; /** * WC_API class. */ class WC_API extends WC_Legacy_API { /** * Init the API by setting up action and filter hooks. */ public function init() { parent::init(); add_action( 'init', array( $this, 'add_endpoint' ), 0 ); add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 ); add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) ); } /** * Get the version of the REST API package being ran. Since API package was merged into core, this now follows WC version. * * @since 3.7.0 * @return string|null */ public function get_rest_api_package_version() { if ( ! $this->is_rest_api_loaded() ) { return null; } if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) { $path = \Automattic\WooCommerce\RestApi\Server::get_path(); if ( 0 === strpos( $path, __DIR__ ) ) { // We are loading API from included version. return WC()->version; } } // We are loading API from external plugin. return \Automattic\WooCommerce\RestApi\Package::get_version(); } /** * Get the version of the REST API package being ran. * * @since 3.7.0 * @return string */ public function get_rest_api_package_path() { if ( ! $this->is_rest_api_loaded() ) { return null; } if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) { // We are loading API from included version. return \Automattic\WooCommerce\RestApi\Server::get_path(); } // We are loading API from external plugin. return \Automattic\WooCommerce\RestApi\Package::get_path(); } /** * Return if the rest API classes were already loaded. * * @since 3.7.0 * @return boolean */ protected function is_rest_api_loaded() { return class_exists( '\Automattic\WooCommerce\RestApi\Server', false ); } /** * Get data from a WooCommerce API endpoint. * * @since 3.7.0 * @param string $endpoint Endpoint. * @param array $params Params to passwith request. * @return array|\WP_Error */ public function get_endpoint_data( $endpoint, $params = array() ) { if ( ! $this->is_rest_api_loaded() ) { return new WP_Error( 'rest_api_unavailable', __( 'The Rest API is unavailable.', 'woocommerce' ) ); } $request = new \WP_REST_Request( 'GET', $endpoint ); if ( $params ) { $request->set_query_params( $params ); } $response = rest_do_request( $request ); $server = rest_get_server(); $json = wp_json_encode( $server->response_to_data( $response, false ) ); return json_decode( $json, true ); } /** * Add new query vars. * * @since 2.0 * @param array $vars Query vars. * @return string[] */ public function add_query_vars( $vars ) { $vars = parent::add_query_vars( $vars ); $vars[] = 'wc-api'; return $vars; } /** * WC API for payment gateway IPNs, etc. * * @since 2.0 */ public static function add_endpoint() { parent::add_endpoint(); add_rewrite_endpoint( 'wc-api', EP_ALL ); } /** * API request - Trigger any API requests. * * @since 2.0 * @version 2.4 */ public function handle_api_requests() { global $wp; if ( ! empty( $_GET['wc-api'] ) ) { // WPCS: input var okay, CSRF ok. $wp->query_vars['wc-api'] = sanitize_key( wp_unslash( $_GET['wc-api'] ) ); // WPCS: input var okay, CSRF ok. } // wc-api endpoint requests. if ( ! empty( $wp->query_vars['wc-api'] ) ) { // Buffer, we won't want any output here. ob_start(); // No cache headers. wc_nocache_headers(); // Clean the API request. $api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) ); // Make sure gateways are available for request. WC()->payment_gateways(); // Trigger generic action before request hook. do_action( 'woocommerce_api_request', $api_request ); // Is there actually something hooked into this API request? If not trigger 400 - Bad request. status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 ); // Trigger an action which plugins can hook into to fulfill the request. do_action( 'woocommerce_api_' . $api_request ); // Done, clear buffer and exit. ob_end_clean(); die( '-1' ); } } /** * Register WC settings from WP-API to the REST API. * * @since 3.0.0 */ public function register_wp_admin_settings() { $pages = WC_Admin_Settings::get_settings_pages(); foreach ( $pages as $page ) { new WC_Register_WP_Admin_Settings( $page, 'page' ); } $emails = WC_Emails::instance(); foreach ( $emails->get_emails() as $email ) { new WC_Register_WP_Admin_Settings( $email, 'email' ); } } } includes/interfaces/class-wc-order-item-type-data-store-interface.php 0000644 00000001013 15132754524 0021750 0 ustar 00 <?php /** * Order Item Type Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Order Item Data Store Interface * * Functions that must be defined by order item store classes. * * @version 3.0.0 */ interface WC_Order_Item_Type_Data_Store_Interface { /** * Saves an item's data to the database / item meta. * Ran after both create and update, so $item->get_id() will be set. * * @param WC_Order_Item $item Item object. */ public function save_item_data( &$item ); } includes/interfaces/class-wc-order-item-product-data-store-interface.php 0000644 00000001040 15132754524 0022447 0 ustar 00 <?php /** * Order Item Product Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Order Item Data Store Interface * * Functions that must be defined by order item store classes. * * @version 3.0.0 */ interface WC_Order_Item_Product_Data_Store_Interface { /** * Get a list of download IDs for a specific item from an order. * * @param WC_Order_Item $item Item object. * @param WC_Order $order Order object. * @return array */ public function get_download_ids( $item, $order ); } includes/interfaces/class-wc-shipping-zone-data-store-interface.php 0000644 00000003465 15132754524 0021531 0 ustar 00 <?php /** * Shipping Zone Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Shipping Zone Data Store Interface. * * Functions that must be defined by shipping zone store classes. * * @version 3.0.0 */ interface WC_Shipping_Zone_Data_Store_Interface { /** * Get a list of shipping methods for a specific zone. * * @param int $zone_id Zone ID. * @param bool $enabled_only True to request enabled methods only. * @return array Array of objects containing method_id, method_order, instance_id, is_enabled */ public function get_methods( $zone_id, $enabled_only ); /** * Get count of methods for a zone. * * @param int $zone_id Zone ID. * @return int Method Count */ public function get_method_count( $zone_id ); /** * Add a shipping method to a zone. * * @param int $zone_id Zone ID. * @param string $type Method Type/ID. * @param int $order Method Order ID. * @return int Instance ID */ public function add_method( $zone_id, $type, $order ); /** * Delete a method instance. * * @param int $instance_id Intance ID. */ public function delete_method( $instance_id ); /** * Get a shipping zone method instance. * * @param int $instance_id Instance ID. * @return object */ public function get_method( $instance_id ); /** * Find a matching zone ID for a given package. * * @param object $package Zone package object. * @return int */ public function get_zone_id_from_package( $package ); /** * Return an ordered list of zones. * * @return array An array of objects containing a zone_id, zone_name, and zone_order. */ public function get_zones(); /** * Return a zone ID from an instance ID. * * @param int $id Instance ID. * @return int */ public function get_zone_id_by_instance_id( $id ); } includes/interfaces/class-wc-coupon-data-store-interface.php 0000644 00000002521 15132754524 0020232 0 ustar 00 <?php /** * Coupon Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interfaces */ /** * WC Coupon Data Store Interface * * Functions that must be defined by coupon store classes. * * @version 3.0.0 */ interface WC_Coupon_Data_Store_Interface { /** * Increase usage count for current coupon. * * @param WC_Coupon $coupon Coupon object. * @param string $used_by Either user ID or billing email. */ public function increase_usage_count( &$coupon, $used_by = '' ); /** * Decrease usage count for current coupon. * * @param WC_Coupon $coupon Coupon object. * @param string $used_by Either user ID or billing email. */ public function decrease_usage_count( &$coupon, $used_by = '' ); /** * Get the number of uses for a coupon by user ID. * * @param WC_Coupon $coupon Coupon object. * @param int $user_id User ID. * @return int */ public function get_usage_by_user_id( &$coupon, $user_id ); /** * Return a coupon code for a specific ID. * * @param int $id Coupon ID. * @return string Coupon Code. */ public function get_code_by_id( $id ); /** * Return an array of IDs for for a specific coupon code. * Can return multiple to check for existence. * * @param string $code Coupon code. * @return array Array of IDs. */ public function get_ids_by_code( $code ); } includes/interfaces/class-wc-queue-interface.php 0000644 00000012635 15132754524 0016021 0 ustar 00 <?php /** * Queue Interface * * @version 3.5.0 * @package WooCommerce\Interface */ /** * WC Queue Interface * * Functions that must be defined to implement an action/job/event queue. * * @version 3.5.0 */ interface WC_Queue_Interface { /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID */ public function add( $hook, $args = array(), $group = '' ); /** * Schedule an action to run once at some time in the future * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID */ public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ); /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID */ public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ); /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $cron_schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return string The action ID */ public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ); /** * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). * * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to (if any). */ public function cancel( $hook, $args = array(), $group = '' ); /** * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to (if any). */ public function cancel_all( $hook, $args = array(), $group = '' ); /** * Get the date and time for the next scheduled occurence of an action with a given hook * (an optionally that matches certain args and group), if any. * * @param string $hook The hook that the job will trigger. * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. * @param string $group Filter to only actions assigned to a specific group. * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook */ public function get_next( $hook, $args = null, $group = '' ); /** * Find scheduled actions. * * @param array $args Possible arguments, with their default values. * 'hook' => '' - the name of the action that will be triggered. * 'args' => null - the args array that will be passed with the action. * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'group' => '' - the group the action belongs to. * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. * 'per_page' => 5 - Number of results to return. * 'offset' => 0. * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'. * 'order' => 'ASC'. * @param string $return_format OBJECT, ARRAY_A, or ids. * @return array */ public function search( $args = array(), $return_format = OBJECT ); } includes/interfaces/class-wc-customer-download-log-data-store-interface.php 0000644 00000001222 15132754524 0023151 0 ustar 00 <?php /** * Customer Download Log Data Store Interface * * @version 3.3.0 * @package WooCommerce\Interface */ /** * WC Customer Download Log Data Store Interface. * * @version 3.3.0 */ interface WC_Customer_Download_Log_Data_Store_Interface { /** * Get array of download log ids by specified args. * * @param array $args Arguments. * @return array of WC_Customer_Download_Log */ public function get_download_logs( $args = array() ); /** * Get logs for a specific download permission. * * @param int $permission_id Permission ID. * @return array */ public function get_download_logs_for_permission( $permission_id ); } includes/interfaces/class-wc-abstract-order-data-store-interface.php 0000644 00000002134 15132754524 0021643 0 ustar 00 <?php /** * Order Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interfaces */ /** * WC Order Data Store Interface * * Functions that must be defined by order store classes. * * @version 3.0.0 */ interface WC_Abstract_Order_Data_Store_Interface { /** * Read order items of a specific type from the database for this order. * * @param WC_Order $order Order object. * @param string $type Order item type. * @return array */ public function read_items( $order, $type ); /** * Remove all line items (products, coupons, shipping, taxes) from the order. * * @param WC_Order $order Order object. * @param string $type Order item type. Default null. */ public function delete_items( $order, $type = null ); /** * Get token ids for an order. * * @param WC_Order $order Order object. * @return array */ public function get_payment_token_ids( $order ); /** * Update token ids for an order. * * @param WC_Order $order Order object. * @param array $token_ids Token IDs. */ public function update_payment_token_ids( $order, $token_ids ); } includes/interfaces/class-wc-importer-interface.php 0000644 00000002171 15132754524 0016530 0 ustar 00 <?php /** * WooCommerce Importer Interface * * @package WooCommerce\Interface * @version 3.1.0 */ /** * WC_Importer_Interface class. */ interface WC_Importer_Interface { /** * Process importation. * Returns an array with the imported and failed items. * 'imported' contains a list of IDs. * 'failed' contains a list of WP_Error objects. * * Example: * ['imported' => [], 'failed' => []] * * @return array */ public function import(); /** * Get file raw keys. * * CSV - Headers. * XML - Element names. * JSON - Keys * * @return array */ public function get_raw_keys(); /** * Get file mapped headers. * * @return array */ public function get_mapped_keys(); /** * Get raw data. * * @return array */ public function get_raw_data(); /** * Get parsed data. * * @return array */ public function get_parsed_data(); /** * Get file pointer position from the last read. * * @return int */ public function get_file_position(); /** * Get file pointer position as a percentage of file size. * * @return int */ public function get_percent_complete(); } includes/interfaces/class-wc-log-handler-interface.php 0000644 00000001262 15132754524 0017063 0 ustar 00 <?php /** * Log Handler Interface * * @version 3.3.0 * @package WooCommerce\Interface */ /** * WC Log Handler Interface * * Functions that must be defined to correctly fulfill log handler API. * * @version 3.3.0 */ interface WC_Log_Handler_Interface { /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ); } includes/interfaces/class-wc-order-refund-data-store-interface.php 0000644 00000000441 15132754524 0021322 0 ustar 00 <?php /** * Order Refund Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Order Refund Data Store Interface * * Functions that must be defined by order store classes. * * @version 3.0.0 */ interface WC_Order_Refund_Data_Store_Interface { } includes/interfaces/class-wc-payment-token-data-store-interface.php 0000644 00000003423 15132754524 0021524 0 ustar 00 <?php /** * Payment Token Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Payment Token Data Store Interface * * Functions that must be defined by payment token store classes. * * @version 3.0.0 */ interface WC_Payment_Token_Data_Store_Interface { /** * Returns an array of objects (stdObject) matching specific token criteria. * Accepts token_id, user_id, gateway_id, and type. * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @param array $args Arguments. * @return array */ public function get_tokens( $args ); /** * Returns an stdObject of a token for a user's default token. * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @param int $user_id User ID. * @return object */ public function get_users_default_token( $user_id ); /** * Returns an stdObject of a token. * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. * * @param int $token_id Token ID. * @return object */ public function get_token_by_id( $token_id ); /** * Returns metadata for a specific payment token. * * @param int $token_id Token ID. * @return array */ public function get_metadata( $token_id ); /** * Get a token's type by ID. * * @param int $token_id Token ID. * @return string */ public function get_token_type_by_id( $token_id ); /** * Update's a tokens default status in the database. Used for quickly * looping through tokens and setting their statuses instead of creating a bunch * of objects. * * @param int $token_id Token ID. * @param bool $status If should update status. * @return string */ public function set_default_status( $token_id, $status = true ); } includes/interfaces/class-wc-webhooks-data-store-interface.php 0000644 00000001423 15132754524 0020550 0 ustar 00 <?php /** * Webhook Data Store Interface * * @version 3.2.0 * @package WooCommerce\Interface */ /** * WooCommerce Webhook data store interface. */ interface WC_Webhook_Data_Store_Interface { /** * Get API version number. * * @since 3.2.0 * @param string $api_version REST API version. * @return int */ public function get_api_version_number( $api_version ); /** * Get all webhooks IDs. * * @since 3.2.0 * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys. * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0. * @return int[] */ public function get_webhooks_ids( $status = '' ); } includes/interfaces/class-wc-product-data-store-interface.php 0000644 00000007370 15132754524 0020416 0 ustar 00 <?php /** * Product Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Product Data Store Interface * * Functions that must be defined by product store classes. * * @version 3.0.0 */ interface WC_Product_Data_Store_Interface { /** * Returns an array of on sale products, as an array of objects with an * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. * * @return array */ public function get_on_sale_products(); /** * Returns a list of product IDs ( id as key => parent as value) that are * featured. Uses get_posts instead of wc_get_products since we want * some extra meta queries and ALL products (posts_per_page = -1). * * @return array */ public function get_featured_product_ids(); /** * Check if product sku is found for any other product IDs. * * @param int $product_id Product ID. * @param string $sku SKU. * @return bool */ public function is_existing_sku( $product_id, $sku ); /** * Return product ID based on SKU. * * @param string $sku SKU. * @return int */ public function get_product_id_by_sku( $sku ); /** * Returns an array of IDs of products that have sales starting soon. * * @return array */ public function get_starting_sales(); /** * Returns an array of IDs of products that have sales which are due to end. * * @return array */ public function get_ending_sales(); /** * Find a matching (enabled) variation within a variable product. * * @param WC_Product $product Variable product object. * @param array $match_attributes Array of attributes we want to try to match. * @return int Matching variation ID or 0. */ public function find_matching_product_variation( $product, $match_attributes = array() ); /** * Make sure all variations have a sort order set so they can be reordered correctly. * * @param int $parent_id Parent ID. */ public function sort_all_product_variations( $parent_id ); /** * Return a list of related products (using data like categories and IDs). * * @param array $cats_array List of categories IDs. * @param array $tags_array List of tags IDs. * @param array $exclude_ids Excluded IDs. * @param int $limit Limit of results. * @param int $product_id Product ID. * @return array */ public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ); /** * Update a product's stock amount directly. * * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). * * @param int $product_id_with_stock Product ID. * @param int|null $stock_quantity Stock quantity to update to. * @param string $operation Either set, increase or decrease. */ public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ); /** * Update a product's sale count directly. * * Uses queries rather than update_post_meta so we can do this in one query for performance. * * @param int $product_id Product ID. * @param int|null $quantity Stock quantity to use for update. * @param string $operation Either set, increase or decrease. */ public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ); /** * Get shipping class ID by slug. * * @param string $slug Shipping class slug. * @return int|false */ public function get_shipping_class_id_by_slug( $slug ); /** * Returns an array of products. * * @param array $args @see wc_get_products. * @return array */ public function get_products( $args = array() ); /** * Get the product type based on product ID. * * @param int $product_id Product ID. * @return bool|string */ public function get_product_type( $product_id ); } includes/interfaces/class-wc-customer-download-data-store-interface.php 0000644 00000003116 15132754524 0022376 0 ustar 00 <?php /** * Customer Download Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Customer Download Data Store Interface. * * @version 3.0.0 */ interface WC_Customer_Download_Data_Store_Interface { /** * Method to delete a download permission from the database by ID. * * @param int $id Download Permission ID. */ public function delete_by_id( $id ); /** * Method to delete a download permission from the database by order ID. * * @param int $id Order ID. */ public function delete_by_order_id( $id ); /** * Method to delete a download permission from the database by download ID. * * @param int $id Download ID. */ public function delete_by_download_id( $id ); /** * Get array of download ids by specified args. * * @param array $args Arguments. * @return array of WC_Customer_Download */ public function get_downloads( $args = array() ); /** * Update download ids if the hash changes. * * @param int $product_id Product ID. * @param string $old_id Old ID. * @param string $new_id New ID. */ public function update_download_id( $product_id, $old_id, $new_id ); /** * Get a customers downloads. * * @param int $customer_id Customer ID. * @return array */ public function get_downloads_for_customer( $customer_id ); /** * Update user prop for downloads based on order id. * * @param int $order_id Order ID. * @param int $customer_id Customer ID. * @param string $email Email Address. */ public function update_user_by_order_id( $order_id, $customer_id, $email ); } includes/interfaces/class-wc-order-item-data-store-interface.php 0000644 00000004657 15132754524 0021012 0 ustar 00 <?php /** * Order Item Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Order Item Data Store Interface * * Functions that must be defined by the order item data store (for functions). * * @version 3.0.0 */ interface WC_Order_Item_Data_Store_Interface { /** * Add an order item to an order. * * @param int $order_id Order ID. * @param array $item order_item_name and order_item_type. * @return int Order Item ID */ public function add_order_item( $order_id, $item ); /** * Update an order item. * * @param int $item_id Item ID. * @param array $item order_item_name or order_item_type. * @return boolean */ public function update_order_item( $item_id, $item ); /** * Delete an order item. * * @param int $item_id Item ID. */ public function delete_order_item( $item_id ); /** * Update term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param string $prev_value Previous value (default: ''). * @return bool */ public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ); /** * Add term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param bool $unique Unique? (default: false). * @return int New row ID or 0 */ public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ); /** * Delete term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value (default: ''). * @param bool $delete_all Delete all matching entries? (default: false). * @return bool */ public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ); /** * Get term meta. * * @param int $item_id Item ID. * @param string $key Meta key. * @param bool $single Store as single value and not serialised (default: true). * @return mixed */ public function get_metadata( $item_id, $key, $single = true ); /** * Get order ID by order item ID. * * @param int $item_id Item ID. * @return int */ public function get_order_id_by_order_item_id( $item_id ); /** * Get the order item type based on Item ID. * * @param int $item_id Item ID. * @return string */ public function get_order_item_type( $item_id ); } includes/interfaces/class-wc-product-variable-data-store-interface.php 0000644 00000003605 15132754524 0022176 0 ustar 00 <?php /** * Product Variable Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Product Variable Data Store Interface * * Functions that must be defined by product variable store classes. * * @version 3.0.0 */ interface WC_Product_Variable_Data_Store_Interface { /** * Does a child have a weight set? * * @param WC_Product $product Product object. * @return boolean */ public function child_has_weight( $product ); /** * Does a child have dimensions set? * * @param WC_Product $product Product object. * @return boolean */ public function child_has_dimensions( $product ); /** * Is a child in stock? * * @param WC_Product $product Product object. * @return boolean */ public function child_is_in_stock( $product ); /** * Syncs all variation names if the parent name is changed. * * @param WC_Product $product Product object. * @param string $previous_name Previous name. * @param string $new_name New name. */ public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ); /** * Stock managed at the parent level - update children being managed by this product. * This sync function syncs downwards (from parent to child) when the variable product is saved. * * @param WC_Product $product Product object. */ public function sync_managed_variation_stock_status( &$product ); /** * Sync variable product prices with children. * * @param WC_Product|int $product Product object or ID. */ public function sync_price( &$product ); /** * Delete variations of a product. * * @param int $product_id Product ID. * @param bool $force_delete False to trash. */ public function delete_variations( $product_id, $force_delete = false ); /** * Untrash variations. * * @param int $product_id Product ID. */ public function untrash_variations( $product_id ); } includes/interfaces/class-wc-object-data-store-interface.php 0000644 00000003167 15132754524 0020204 0 ustar 00 <?php /** * Object Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Data Store Interface * * @version 3.0.0 */ interface WC_Object_Data_Store_Interface { /** * Method to create a new record of a WC_Data based object. * * @param WC_Data $data Data object. */ public function create( &$data ); /** * Method to read a record. Creates a new WC_Data based object. * * @param WC_Data $data Data object. */ public function read( &$data ); /** * Updates a record in the database. * * @param WC_Data $data Data object. */ public function update( &$data ); /** * Deletes a record from the database. * * @param WC_Data $data Data object. * @param array $args Array of args to pass to the delete method. * @return bool result */ public function delete( &$data, $args = array() ); /** * Returns an array of meta for an object. * * @param WC_Data $data Data object. * @return array */ public function read_meta( &$data ); /** * Deletes meta based on meta ID. * * @param WC_Data $data Data object. * @param object $meta Meta object (containing at least ->id). * @return array */ public function delete_meta( &$data, $meta ); /** * Add new piece of meta. * * @param WC_Data $data Data object. * @param object $meta Meta object (containing ->key and ->value). * @return int meta ID */ public function add_meta( &$data, $meta ); /** * Update meta. * * @param WC_Data $data Data object. * @param object $meta Meta object (containing ->id, ->key and ->value). */ public function update_meta( &$data, $meta ); } includes/interfaces/class-wc-customer-data-store-interface.php 0000644 00000001464 15132754524 0020575 0 ustar 00 <?php /** * Customer Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Customer Data Store Interface * * Functions that must be defined by customer store classes. * * @version 3.0.0 */ interface WC_Customer_Data_Store_Interface { /** * Gets the customers last order. * * @param WC_Customer $customer Customer object. * @return WC_Order|false */ public function get_last_order( &$customer ); /** * Return the number of orders this customer has. * * @param WC_Customer $customer Customer object. * @return integer */ public function get_order_count( &$customer ); /** * Return how much money this customer has spent. * * @param WC_Customer $customer Customer object. * @return float */ public function get_total_spent( &$customer ); } includes/interfaces/class-wc-order-data-store-interface.php 0000644 00000006013 15132754524 0020042 0 ustar 00 <?php /** * Order Data Store Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Order Data Store Interface * * Functions that must be defined by order store classes. * * @version 3.0.0 */ interface WC_Order_Data_Store_Interface { /** * Get amount already refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_refunded( $order ); /** * Get the total tax refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_tax_refunded( $order ); /** * Get the total shipping refunded. * * @param WC_Order $order Order object. * @return float */ public function get_total_shipping_refunded( $order ); /** * Finds an Order ID based on an order key. * * @param string $order_key An order key has generated by. * @return int The ID of an order, or 0 if the order could not be found. */ public function get_order_id_by_order_key( $order_key ); /** * Return count of orders with a specific status. * * @param string $status Order status. * @return int */ public function get_order_count( $status ); /** * Get all orders matching the passed in args. * * @see wc_get_orders() * @param array $args Arguments. * @return array of orders */ public function get_orders( $args = array() ); /** * Get unpaid orders after a certain date, * * @param int $date timestamp. * @return array */ public function get_unpaid_orders( $date ); /** * Search order data for a term and return ids. * * @param string $term Term name. * @return array of ids */ public function search_orders( $term ); /** * Gets information about whether permissions were generated yet. * * @param WC_Order $order Order object. * @return bool */ public function get_download_permissions_granted( $order ); /** * Stores information about whether permissions were generated yet. * * @param WC_Order $order Order object. * @param bool $set If should set. */ public function set_download_permissions_granted( $order, $set ); /** * Gets information about whether sales were recorded. * * @param WC_Order $order Order object. * @return bool */ public function get_recorded_sales( $order ); /** * Stores information about whether sales were recorded. * * @param WC_Order $order Order object. * @param bool $set If should set. */ public function set_recorded_sales( $order, $set ); /** * Gets information about whether coupon counts were updated. * * @param WC_Order $order Order object. * @return bool */ public function get_recorded_coupon_usage_counts( $order ); /** * Stores information about whether coupon counts were updated. * * @param WC_Order $order Order object. * @param bool $set If should set. */ public function set_recorded_coupon_usage_counts( $order, $set ); /** * Get the order type based on Order ID. * * @param int $order_id Order ID. * @return string */ public function get_order_type( $order_id ); } includes/interfaces/class-wc-logger-interface.php 0000644 00000007261 15132754524 0016153 0 ustar 00 <?php /** * Logger Interface * * @version 3.0.0 * @package WooCommerce\Interface */ /** * WC Logger Interface * * Functions that must be defined to correctly fulfill logger API. * * @version 3.0.0 */ interface WC_Logger_Interface { /** * Add a log entry. * * This is not the preferred method for adding log messages. Please use log() or any one of * the level methods (debug(), info(), etc.). This method may be deprecated in the future. * * @param string $handle File handle. * @param string $message Log message. * @param string $level Log level. * * @return bool True if log was added, otherwise false. */ public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ); /** * Add a log entry. * * @param string $level One of the following: * 'emergency': System is unusable. * 'alert': Action must be taken immediately. * 'critical': Critical conditions. * 'error': Error conditions. * 'warning': Warning conditions. * 'notice': Normal but significant condition. * 'info': Informational messages. * 'debug': Debug-level messages. * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function log( $level, $message, $context = array() ); /** * Adds an emergency level message. * * System is unusable. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function emergency( $message, $context = array() ); /** * Adds an alert level message. * * Action must be taken immediately. * Example: Entire website down, database unavailable, etc. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function alert( $message, $context = array() ); /** * Adds a critical level message. * * Critical conditions. * Example: Application component unavailable, unexpected exception. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function critical( $message, $context = array() ); /** * Adds an error level message. * * Runtime errors that do not require immediate action but should typically be logged * and monitored. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function error( $message, $context = array() ); /** * Adds a warning level message. * * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not * necessarily wrong. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function warning( $message, $context = array() ); /** * Adds a notice level message. * * Normal but significant events. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function notice( $message, $context = array() ); /** * Adds a info level message. * * Interesting events. * Example: User logs in, SQL logs. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function info( $message, $context = array() ); /** * Adds a debug level message. * * Detailed debug information. * * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. */ public function debug( $message, $context = array() ); } includes/wc-rest-functions.php 0000644 00000025336 15132754524 0012476 0 ustar 00 <?php /** * WooCommerce REST Functions * * Functions for REST specific things. * * @package WooCommerce\Functions * @version 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Parses and formats a date for ISO8601/RFC3339. * * Required WP 4.4 or later. * See https://developer.wordpress.org/reference/functions/mysql_to_rfc3339/ * * @since 2.6.0 * @param string|null|WC_DateTime $date Date. * @param bool $utc Send false to get local/offset time. * @return string|null ISO8601/RFC3339 formatted datetime. */ function wc_rest_prepare_date_response( $date, $utc = true ) { if ( is_numeric( $date ) ) { $date = new WC_DateTime( "@$date", new DateTimeZone( 'UTC' ) ); $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } elseif ( is_string( $date ) ) { $date = new WC_DateTime( $date, new DateTimeZone( 'UTC' ) ); $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } if ( ! is_a( $date, 'WC_DateTime' ) ) { return null; } // Get timestamp before changing timezone to UTC. return gmdate( 'Y-m-d\TH:i:s', $utc ? $date->getTimestamp() : $date->getOffsetTimestamp() ); } /** * Returns image mime types users are allowed to upload via the API. * * @since 2.6.4 * @return array */ function wc_rest_allowed_image_mime_types() { return apply_filters( 'woocommerce_rest_allowed_image_mime_types', array( 'jpg|jpeg|jpe' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'bmp' => 'image/bmp', 'tiff|tif' => 'image/tiff', 'ico' => 'image/x-icon', ) ); } /** * Upload image from URL. * * @since 2.6.0 * @param string $image_url Image URL. * @return array|WP_Error Attachment data or error message. */ function wc_rest_upload_image_from_url( $image_url ) { $parsed_url = wp_parse_url( $image_url ); // Check parsed URL. if ( ! $parsed_url || ! is_array( $parsed_url ) ) { /* translators: %s: image URL */ return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); } // Ensure url is valid. $image_url = esc_url_raw( $image_url ); // download_url function is part of wp-admin. if ( ! function_exists( 'download_url' ) ) { include_once ABSPATH . 'wp-admin/includes/file.php'; } $file_array = array(); $file_array['name'] = basename( current( explode( '?', $image_url ) ) ); // Download file to temp location. $file_array['tmp_name'] = download_url( $image_url ); // If error storing temporarily, return the error. if ( is_wp_error( $file_array['tmp_name'] ) ) { return new WP_Error( 'woocommerce_rest_invalid_remote_image_url', /* translators: %s: image URL */ sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' /* translators: %s: error message */ . sprintf( __( 'Error: %s', 'woocommerce' ), $file_array['tmp_name']->get_error_message() ), array( 'status' => 400 ) ); } // Do the validation and storage stuff. $file = wp_handle_sideload( $file_array, array( 'test_form' => false, 'mimes' => wc_rest_allowed_image_mime_types(), ), current_time( 'Y/m' ) ); if ( isset( $file['error'] ) ) { @unlink( $file_array['tmp_name'] ); // @codingStandardsIgnoreLine. /* translators: %s: error message */ return new WP_Error( 'woocommerce_rest_invalid_image', sprintf( __( 'Invalid image: %s', 'woocommerce' ), $file['error'] ), array( 'status' => 400 ) ); } do_action( 'woocommerce_rest_api_uploaded_image_from_url', $file, $image_url ); return $file; } /** * Set uploaded image as attachment. * * @since 2.6.0 * @param array $upload Upload information from wp_upload_bits. * @param int $id Post ID. Default to 0. * @return int Attachment ID */ function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { $info = wp_check_filetype( $upload['file'] ); $title = ''; $content = ''; if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { include_once ABSPATH . 'wp-admin/includes/image.php'; } $image_meta = wp_read_image_metadata( $upload['file'] ); if ( $image_meta ) { if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = wc_clean( $image_meta['title'] ); } if ( trim( $image_meta['caption'] ) ) { $content = wc_clean( $image_meta['caption'] ); } } $attachment = array( 'post_mime_type' => $info['type'], 'guid' => $upload['url'], 'post_parent' => $id, 'post_title' => $title ? $title : basename( $upload['file'] ), 'post_content' => $content, ); $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); if ( ! is_wp_error( $attachment_id ) ) { wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); } return $attachment_id; } /** * Validate reports request arguments. * * @since 2.6.0 * @param mixed $value Value to valdate. * @param WP_REST_Request $request Request instance. * @param string $param Param to validate. * @return WP_Error|boolean */ function wc_rest_validate_reports_request_arg( $value, $request, $param ) { $attributes = $request->get_attributes(); if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { return true; } $args = $attributes['args'][ $param ]; if ( 'string' === $args['type'] && ! is_string( $value ) ) { /* translators: 1: param 2: type */ return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%1$s is not of type %2$s', 'woocommerce' ), $param, 'string' ) ); } if ( 'date' === $args['format'] ) { $regex = '#^\d{4}-\d{2}-\d{2}$#'; if ( ! preg_match( $regex, $value, $matches ) ) { return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); } } return true; } /** * Encodes a value according to RFC 3986. * Supports multidimensional arrays. * * @since 2.6.0 * @param string|array $value The value to encode. * @return string|array Encoded values. */ function wc_rest_urlencode_rfc3986( $value ) { if ( is_array( $value ) ) { return array_map( 'wc_rest_urlencode_rfc3986', $value ); } return str_replace( array( '+', '%7E' ), array( ' ', '~' ), rawurlencode( $value ) ); } /** * Check permissions of posts on REST API. * * @since 2.6.0 * @param string $post_type Post type. * @param string $context Request context. * @param int $object_id Post ID. * @return bool */ function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) { $contexts = array( 'read' => 'read_private_posts', 'create' => 'publish_posts', 'edit' => 'edit_post', 'delete' => 'delete_post', 'batch' => 'edit_others_posts', ); if ( 'revision' === $post_type ) { $permission = false; } else { $cap = $contexts[ $context ]; $post_type_object = get_post_type_object( $post_type ); $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); } return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type ); } /** * Check permissions of users on REST API. * * @since 2.6.0 * @param string $context Request context. * @param int $object_id Post ID. * @return bool */ function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { $contexts = array( 'read' => 'list_users', 'create' => 'promote_users', // Check if current user can create users, shop managers are not allowed to create users. 'edit' => 'edit_users', 'delete' => 'delete_users', 'batch' => 'promote_users', ); // Check to allow shop_managers to manage only customers. if ( in_array( $context, array( 'edit', 'delete' ), true ) && wc_current_user_has_role( 'shop_manager' ) ) { $permission = false; $user_data = get_userdata( $object_id ); $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); if ( isset( $user_data->roles ) ) { $can_manage_users = array_intersect( $user_data->roles, array_unique( $shop_manager_editable_roles ) ); // Check if Shop Manager can edit customer or with the is same shop manager. if ( 0 < count( $can_manage_users ) || intval( $object_id ) === intval( get_current_user_id() ) ) { $permission = current_user_can( $contexts[ $context ], $object_id ); } } } else { $permission = current_user_can( $contexts[ $context ], $object_id ); } return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' ); } /** * Check permissions of product terms on REST API. * * @since 2.6.0 * @param string $taxonomy Taxonomy. * @param string $context Request context. * @param int $object_id Post ID. * @return bool */ function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) { $contexts = array( 'read' => 'manage_terms', 'create' => 'edit_terms', 'edit' => 'edit_terms', 'delete' => 'delete_terms', 'batch' => 'edit_terms', ); $cap = $contexts[ $context ]; $taxonomy_object = get_taxonomy( $taxonomy ); $permission = current_user_can( $taxonomy_object->cap->$cap, $object_id ); return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); } /** * Check manager permissions on REST API. * * @since 2.6.0 * @param string $object Object. * @param string $context Request context. * @return bool */ function wc_rest_check_manager_permissions( $object, $context = 'read' ) { $objects = array( 'reports' => 'view_woocommerce_reports', 'settings' => 'manage_woocommerce', 'system_status' => 'manage_woocommerce', 'attributes' => 'manage_product_terms', 'shipping_methods' => 'manage_woocommerce', 'payment_gateways' => 'manage_woocommerce', 'webhooks' => 'manage_woocommerce', ); $permission = current_user_can( $objects[ $object ] ); return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); } /** * Check product reviews permissions on REST API. * * @since 3.5.0 * @param string $context Request context. * @param string $object_id Object ID. * @return bool */ function wc_rest_check_product_reviews_permissions( $context = 'read', $object_id = 0 ) { $permission = false; $contexts = array( 'read' => 'moderate_comments', 'create' => 'moderate_comments', 'edit' => 'moderate_comments', 'delete' => 'moderate_comments', 'batch' => 'moderate_comments', ); if ( isset( $contexts[ $context ] ) ) { $permission = current_user_can( $contexts[ $context ] ); } return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'product_review' ); } includes/class-wc-post-data.php 0000644 00000050404 15132754524 0012504 0 ustar 00 <?php /** * Post Data * * Standardises certain post data on save. * * @package WooCommerce\Classes\Data * @version 2.2.0 */ use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore; use Automattic\WooCommerce\Proxies\LegacyProxy; defined( 'ABSPATH' ) || exit; /** * Post data class. */ class WC_Post_Data { /** * Editing term. * * @var object */ private static $editing_term = null; /** * Hook in methods. */ public static function init() { add_filter( 'post_type_link', array( __CLASS__, 'variation_post_link' ), 10, 2 ); add_action( 'shutdown', array( __CLASS__, 'do_deferred_product_sync' ), 10 ); add_action( 'set_object_terms', array( __CLASS__, 'force_default_term' ), 10, 5 ); add_action( 'set_object_terms', array( __CLASS__, 'delete_product_query_transients' ) ); add_action( 'deleted_term_relationships', array( __CLASS__, 'delete_product_query_transients' ) ); add_action( 'woocommerce_product_set_stock_status', array( __CLASS__, 'delete_product_query_transients' ) ); add_action( 'woocommerce_product_set_visibility', array( __CLASS__, 'delete_product_query_transients' ) ); add_action( 'woocommerce_product_type_changed', array( __CLASS__, 'product_type_changed' ), 10, 3 ); add_action( 'edit_term', array( __CLASS__, 'edit_term' ), 10, 3 ); add_action( 'edited_term', array( __CLASS__, 'edited_term' ), 10, 3 ); add_filter( 'update_order_item_metadata', array( __CLASS__, 'update_order_item_metadata' ), 10, 5 ); add_filter( 'update_post_metadata', array( __CLASS__, 'update_post_metadata' ), 10, 5 ); add_filter( 'wp_insert_post_data', array( __CLASS__, 'wp_insert_post_data' ) ); add_filter( 'oembed_response_data', array( __CLASS__, 'filter_oembed_response_data' ), 10, 2 ); add_filter( 'wp_untrash_post_status', array( __CLASS__, 'wp_untrash_post_status' ), 10, 3 ); // Status transitions. add_action( 'transition_post_status', array( __CLASS__, 'transition_post_status' ), 10, 3 ); add_action( 'delete_post', array( __CLASS__, 'delete_post' ) ); add_action( 'wp_trash_post', array( __CLASS__, 'trash_post' ) ); add_action( 'untrashed_post', array( __CLASS__, 'untrash_post' ) ); add_action( 'before_delete_post', array( __CLASS__, 'before_delete_order' ) ); // Meta cache flushing. add_action( 'updated_post_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 ); add_action( 'updated_order_item_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 ); } /** * Link to parent products when getting permalink for variation. * * @param string $permalink Permalink. * @param WP_Post $post Post data. * * @return string */ public static function variation_post_link( $permalink, $post ) { if ( isset( $post->ID, $post->post_type ) && 'product_variation' === $post->post_type ) { $variation = wc_get_product( $post->ID ); if ( $variation && $variation->get_parent_id() ) { return $variation->get_permalink(); } } return $permalink; } /** * Sync products queued to sync. */ public static function do_deferred_product_sync() { global $wc_deferred_product_sync; if ( ! empty( $wc_deferred_product_sync ) ) { $wc_deferred_product_sync = wp_parse_id_list( $wc_deferred_product_sync ); array_walk( $wc_deferred_product_sync, array( __CLASS__, 'deferred_product_sync' ) ); } } /** * Sync a product. * * @param int $product_id Product ID. */ public static function deferred_product_sync( $product_id ) { $product = wc_get_product( $product_id ); if ( is_callable( array( $product, 'sync' ) ) ) { $product->sync( $product ); } } /** * When a post status changes. * * @param string $new_status New status. * @param string $old_status Old status. * @param WP_Post $post Post data. */ public static function transition_post_status( $new_status, $old_status, $post ) { if ( ( 'publish' === $new_status || 'publish' === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { self::delete_product_query_transients(); } } /** * Delete product view transients when needed e.g. when post status changes, or visibility/stock status is modified. */ public static function delete_product_query_transients() { WC_Cache_Helper::get_transient_version( 'product_query', true ); } /** * Handle type changes. * * @since 3.0.0 * * @param WC_Product $product Product data. * @param string $from Origin type. * @param string $to New type. */ public static function product_type_changed( $product, $from, $to ) { /** * Filter to prevent variations from being deleted while switching from a variable product type to a variable product type. * * @since 5.0.0 * * @param bool A boolean value of true will delete the variations. * @param WC_Product $product Product data. * @return string $from Origin type. * @param string $to New type. */ if ( apply_filters( 'woocommerce_delete_variations_on_product_type_change', 'variable' === $from && 'variable' !== $to, $product, $from, $to ) ) { // If the product is no longer variable, we should ensure all variations are removed. $data_store = WC_Data_Store::load( 'product-variable' ); $data_store->delete_variations( $product->get_id(), true ); } } /** * When editing a term, check for product attributes. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ public static function edit_term( $term_id, $tt_id, $taxonomy ) { if ( strpos( $taxonomy, 'pa_' ) === 0 ) { self::$editing_term = get_term_by( 'id', $term_id, $taxonomy ); } else { self::$editing_term = null; } } /** * When a term is edited, check for product attributes and update variations. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ public static function edited_term( $term_id, $tt_id, $taxonomy ) { if ( ! is_null( self::$editing_term ) && strpos( $taxonomy, 'pa_' ) === 0 ) { $edited_term = get_term_by( 'id', $term_id, $taxonomy ); if ( $edited_term->slug !== self::$editing_term->slug ) { global $wpdb; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %s WHERE meta_key = %s AND meta_value = %s;", $edited_term->slug, 'attribute_' . sanitize_title( $taxonomy ), self::$editing_term->slug ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE( meta_value, %s, %s ) WHERE meta_key = '_default_attributes'", serialize( self::$editing_term->taxonomy ) . serialize( self::$editing_term->slug ), serialize( $edited_term->taxonomy ) . serialize( $edited_term->slug ) ) ); } } else { self::$editing_term = null; } } /** * Ensure floats are correctly converted to strings based on PHP locale. * * @param null $check Whether to allow updating metadata for the given type. * @param int $object_id Object ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. Must be serializable if non-scalar. * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. * @return null|bool */ public static function update_order_item_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { if ( ! empty( $meta_value ) && is_float( $meta_value ) ) { // Convert float to string. $meta_value = wc_float_to_string( $meta_value ); // Update meta value with new string. update_metadata( 'order_item', $object_id, $meta_key, $meta_value, $prev_value ); return true; } return $check; } /** * Ensure floats are correctly converted to strings based on PHP locale. * * @param null $check Whether to allow updating metadata for the given type. * @param int $object_id Object ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. Must be serializable if non-scalar. * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. * @return null|bool */ public static function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { // Delete product cache if someone uses meta directly. if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { wp_cache_delete( 'product-' . $object_id, 'products' ); } if ( ! empty( $meta_value ) && is_float( $meta_value ) && ! registered_meta_key_exists( 'post', $meta_key ) && in_array( get_post_type( $object_id ), array_merge( wc_get_order_types(), array( 'shop_coupon', 'product', 'product_variation' ) ), true ) ) { // Convert float to string. $meta_value = wc_float_to_string( $meta_value ); // Update meta value with new string. update_metadata( 'post', $object_id, $meta_key, $meta_value, $prev_value ); return true; } return $check; } /** * Forces the order posts to have a title in a certain format (containing the date). * Forces certain product data based on the product's type, e.g. grouped products cannot have a parent. * * @param array $data An array of slashed post data. * @return array */ public static function wp_insert_post_data( $data ) { if ( 'shop_order' === $data['post_type'] && isset( $data['post_date'] ) ) { $order_title = 'Order'; if ( $data['post_date'] ) { $order_title .= ' – ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) ); } $data['post_title'] = $order_title; } elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { // WPCS: input var ok, CSRF ok. $product_type = wc_clean( wp_unslash( $_POST['product-type'] ) ); // WPCS: input var ok, CSRF ok. switch ( $product_type ) { case 'grouped': case 'variable': $data['post_parent'] = 0; break; } } elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) { $data['post_title'] = 'AUTO-DRAFT'; } elseif ( 'shop_coupon' === $data['post_type'] ) { // Coupons should never allow unfiltered HTML. $data['post_title'] = wp_filter_kses( $data['post_title'] ); } return $data; } /** * Change embed data for certain post types. * * @since 3.2.0 * @param array $data The response data. * @param WP_Post $post The post object. * @return array */ public static function filter_oembed_response_data( $data, $post ) { if ( in_array( $post->post_type, array( 'shop_order', 'shop_coupon' ), true ) ) { return array(); } return $data; } /** * Removes variations etc belonging to a deleted post, and clears transients. * * @param mixed $id ID of post being deleted. */ public static function delete_post( $id ) { $container = wc_get_container(); if ( ! $container->get( LegacyProxy::class )->call_function( 'current_user_can', 'delete_posts' ) || ! $id ) { return; } $post_type = self::get_post_type( $id ); switch ( $post_type ) { case 'product': $data_store = WC_Data_Store::load( 'product-variable' ); $data_store->delete_variations( $id, true ); $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); $parent_id = wp_get_post_parent_id( $id ); if ( $parent_id ) { wc_delete_product_transients( $parent_id ); } break; case 'product_variation': $data_store = WC_Data_Store::load( 'product' ); $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); wc_delete_product_transients( wp_get_post_parent_id( $id ) ); $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); break; case 'shop_order': global $wpdb; $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); if ( ! is_null( $refunds ) ) { foreach ( $refunds as $refund ) { wp_delete_post( $refund->ID, true ); } } break; } } /** * Trash post. * * @param mixed $id Post ID. */ public static function trash_post( $id ) { if ( ! $id ) { return; } $post_type = self::get_post_type( $id ); // If this is an order, trash any refunds too. if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { global $wpdb; $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); foreach ( $refunds as $refund ) { $wpdb->update( $wpdb->posts, array( 'post_status' => 'trash' ), array( 'ID' => $refund->ID ) ); } wc_delete_shop_order_transients( $id ); // If this is a product, trash children variations. } elseif ( 'product' === $post_type ) { $data_store = WC_Data_Store::load( 'product-variable' ); $data_store->delete_variations( $id, false ); wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); } elseif ( 'product_variation' === $post_type ) { wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); } } /** * Untrash post. * * @param mixed $id Post ID. */ public static function untrash_post( $id ) { if ( ! $id ) { return; } $post_type = self::get_post_type( $id ); if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { global $wpdb; $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); foreach ( $refunds as $refund ) { $wpdb->update( $wpdb->posts, array( 'post_status' => 'wc-completed' ), array( 'ID' => $refund->ID ) ); } wc_delete_shop_order_transients( $id ); } elseif ( 'product' === $post_type ) { $data_store = WC_Data_Store::load( 'product-variable' ); $data_store->untrash_variations( $id ); wc_product_force_unique_sku( $id ); wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id ); } elseif ( 'product_variation' === $post_type ) { wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id ); } } /** * Get the post type for a given post. * * @param int $id The post id. * @return string The post type. */ private static function get_post_type( $id ) { return wc_get_container()->get( LegacyProxy::class )->call_function( 'get_post_type', $id ); } /** * Before deleting an order, do some cleanup. * * @since 3.2.0 * @param int $order_id Order ID. */ public static function before_delete_order( $order_id ) { if ( in_array( get_post_type( $order_id ), wc_get_order_types(), true ) ) { // Clean up user. $order = wc_get_order( $order_id ); // Check for `get_customer_id`, since this may be e.g. a refund order (which doesn't implement it). $customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? $order->get_customer_id() : 0; if ( $customer_id > 0 && 'shop_order' === $order->get_type() ) { $customer = new WC_Customer( $customer_id ); $order_count = $customer->get_order_count(); $order_count --; if ( 0 === $order_count ) { $customer->set_is_paying_customer( false ); $customer->save(); } // Delete order count and last order meta. delete_user_meta( $customer_id, '_order_count' ); delete_user_meta( $customer_id, '_last_order' ); } // Clean up items. self::delete_order_items( $order_id ); self::delete_order_downloadable_permissions( $order_id ); } } /** * Remove item meta on permanent deletion. * * @param int $postid Post ID. */ public static function delete_order_items( $postid ) { global $wpdb; if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { do_action( 'woocommerce_delete_order_items', $postid ); $wpdb->query( " DELETE {$wpdb->prefix}woocommerce_order_items, {$wpdb->prefix}woocommerce_order_itemmeta FROM {$wpdb->prefix}woocommerce_order_items JOIN {$wpdb->prefix}woocommerce_order_itemmeta ON {$wpdb->prefix}woocommerce_order_items.order_item_id = {$wpdb->prefix}woocommerce_order_itemmeta.order_item_id WHERE {$wpdb->prefix}woocommerce_order_items.order_id = '{$postid}'; " ); // WPCS: unprepared SQL ok. do_action( 'woocommerce_deleted_order_items', $postid ); } } /** * Remove downloadable permissions on permanent order deletion. * * @param int $postid Post ID. */ public static function delete_order_downloadable_permissions( $postid ) { if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { do_action( 'woocommerce_delete_order_downloadable_permissions', $postid ); $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->delete_by_order_id( $postid ); do_action( 'woocommerce_deleted_order_downloadable_permissions', $postid ); } } /** * Flush meta cache for CRUD objects on direct update. * * @param int $meta_id Meta ID. * @param int $object_id Object ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value. */ public static function flush_object_meta_cache( $meta_id, $object_id, $meta_key, $meta_value ) { WC_Cache_Helper::invalidate_cache_group( 'object_' . $object_id ); } /** * Ensure default category gets set. * * @since 3.3.0 * @param int $object_id Product ID. * @param array $terms Terms array. * @param array $tt_ids Term ids array. * @param string $taxonomy Taxonomy name. * @param bool $append Are we appending or setting terms. */ public static function force_default_term( $object_id, $terms, $tt_ids, $taxonomy, $append ) { if ( ! $append && 'product_cat' === $taxonomy && empty( $tt_ids ) && 'product' === get_post_type( $object_id ) ) { $default_term = absint( get_option( 'default_product_cat', 0 ) ); $tt_ids = array_map( 'absint', $tt_ids ); if ( $default_term && ! in_array( $default_term, $tt_ids, true ) ) { wp_set_post_terms( $object_id, array( $default_term ), 'product_cat', true ); } } } /** * Ensure statuses are correctly reassigned when restoring orders and products. * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. * @param string $previous_status The status of the post at the point where it was trashed. * @return string */ public static function wp_untrash_post_status( $new_status, $post_id, $previous_status ) { $post_types = array( 'shop_order', 'shop_coupon', 'product', 'product_variation' ); if ( in_array( get_post_type( $post_id ), $post_types, true ) ) { $new_status = $previous_status; } return $new_status; } /** * When setting stock level, ensure the stock status is kept in sync. * * @param int $meta_id Meta ID. * @param int $object_id Object ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @deprecated 3.3 */ public static function sync_product_stock_status( $meta_id, $object_id, $meta_key, $meta_value ) {} /** * Update changed downloads. * * @deprecated 3.3.0 No action is necessary on changes to download paths since download_id is no longer based on file hash. * @param int $product_id Product ID. * @param int $variation_id Variation ID. Optional product variation identifier. * @param array $downloads Newly set files. */ public static function process_product_file_download_paths( $product_id, $variation_id, $downloads ) { wc_deprecated_function( __FUNCTION__, '3.3' ); } /** * Delete transients when terms are set. * * @deprecated 3.6 * @param int $object_id Object ID. * @param mixed $terms An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param mixed $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ public static function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { self::delete_product_query_transients(); } } } WC_Post_Data::init(); includes/traits/trait-wc-item-totals.php 0000644 00000004114 15132754524 0014373 0 ustar 00 <?php /** * This ongoing trait will have shared calculation logic between WC_Abstract_Order and WC_Cart_Totals classes. * * @package WooCommerce\Traits * @version 3.9.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Trait WC_Item_Totals. * * Right now this do not have much, but plan is to eventually move all shared calculation logic between Orders and Cart in this file. * * @since 3.9.0 */ trait WC_Item_Totals { /** * Line items to calculate. Define in child class. * * @since 3.9.0 * @param string $field Field name to calculate upon. * * @return array having `total`|`subtotal` property. */ abstract protected function get_values_for_total( $field ); /** * Return rounded total based on settings. Will be used by Cart and Orders. * * @since 3.9.0 * * @param array $values Values to round. Should be with precision. * * @return float|int Appropriately rounded value. */ public static function get_rounded_items_total( $values ) { return array_sum( array_map( array( self::class, 'round_item_subtotal' ), $values ) ); } /** * Apply rounding to item subtotal before summing. * * @since 3.9.0 * @param float $value Item subtotal value. * @return float */ public static function round_item_subtotal( $value ) { if ( ! self::round_at_subtotal() ) { $value = NumberUtil::round( $value ); } return $value; } /** * Should always round at subtotal? * * @since 3.9.0 * @return bool */ protected static function round_at_subtotal() { return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); } /** * Apply rounding to an array of taxes before summing. Rounds to store DP setting, ignoring precision. * * @since 3.2.6 * @param float $value Tax value. * @param bool $in_cents Whether precision of value is in cents. * @return float */ protected static function round_line_tax( $value, $in_cents = true ) { if ( ! self::round_at_subtotal() ) { $value = wc_round_tax_total( $value, $in_cents ? 0 : null ); } return $value; } } includes/class-wc-https.php 0000644 00000010510 15132754524 0011744 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_HTTPS class. * * @class WC_HTTPS * @version 2.2.0 * @package WooCommerce\Classes * @category Class * @author WooThemes */ class WC_HTTPS { /** * Hook in our HTTPS functions if we're on the frontend. This will ensure any links output to a page (when viewing via HTTPS) are also served over HTTPS. */ public static function init() { if ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) && ! is_admin() ) { // HTTPS urls with SSL on $filters = array( 'post_thumbnail_html', 'wp_get_attachment_image_attributes', 'wp_get_attachment_url', 'option_stylesheet_url', 'option_template_url', 'script_loader_src', 'style_loader_src', 'template_directory_uri', 'stylesheet_directory_uri', 'site_url', ); foreach ( $filters as $filter ) { add_filter( $filter, array( __CLASS__, 'force_https_url' ), 999 ); } add_filter( 'page_link', array( __CLASS__, 'force_https_page_link' ), 10, 2 ); add_action( 'template_redirect', array( __CLASS__, 'force_https_template_redirect' ) ); if ( 'yes' == get_option( 'woocommerce_unforce_ssl_checkout' ) ) { add_action( 'template_redirect', array( __CLASS__, 'unforce_https_template_redirect' ) ); } } add_action( 'http_api_curl', array( __CLASS__, 'http_api_curl' ), 10, 3 ); } /** * Force https for urls. * * @param mixed $content * @return string */ public static function force_https_url( $content ) { if ( is_ssl() ) { if ( is_array( $content ) ) { $content = array_map( 'WC_HTTPS::force_https_url', $content ); } else { $content = str_replace( 'http:', 'https:', $content ); } } return $content; } /** * Force a post link to be SSL if needed. * * @param string $link * @param int $page_id * * @return string */ public static function force_https_page_link( $link, $page_id ) { if ( in_array( $page_id, array( get_option( 'woocommerce_checkout_page_id' ), get_option( 'woocommerce_myaccount_page_id' ) ) ) ) { $link = str_replace( 'http:', 'https:', $link ); } elseif ( 'yes' === get_option( 'woocommerce_unforce_ssl_checkout' ) && ! wc_site_is_https() ) { $link = str_replace( 'https:', 'http:', $link ); } return $link; } /** * Template redirect - if we end up on a page ensure it has the correct http/https url. */ public static function force_https_template_redirect() { if ( ! is_ssl() && ( is_checkout() || is_account_page() || apply_filters( 'woocommerce_force_ssl_checkout', false ) ) ) { if ( 0 === strpos( $_SERVER['REQUEST_URI'], 'http' ) ) { wp_safe_redirect( preg_replace( '|^http://|', 'https://', $_SERVER['REQUEST_URI'] ) ); exit; } else { wp_safe_redirect( 'https://' . ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'] ) . $_SERVER['REQUEST_URI'] ); exit; } } } /** * Template redirect - if we end up on a page ensure it has the correct http/https url. */ public static function unforce_https_template_redirect() { if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { return; } if ( ! wc_site_is_https() && is_ssl() && $_SERVER['REQUEST_URI'] && ! is_checkout() && ! is_ajax() && ! is_account_page() && apply_filters( 'woocommerce_unforce_ssl_checkout', true ) ) { if ( 0 === strpos( $_SERVER['REQUEST_URI'], 'http' ) ) { wp_safe_redirect( preg_replace( '|^https://|', 'http://', $_SERVER['REQUEST_URI'] ) ); exit; } else { wp_safe_redirect( 'http://' . ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'] ) . $_SERVER['REQUEST_URI'] ); exit; } } } /** * Force posts to PayPal to use TLS v1.2. See: * https://core.trac.wordpress.org/ticket/36320 * https://core.trac.wordpress.org/ticket/34924#comment:13 * https://www.paypal-knowledge.com/infocenter/index?page=content&widgetview=true&id=FAQ1914&viewlocale=en_US * * @param string $handle * @param mixed $r * @param string $url */ public static function http_api_curl( $handle, $r, $url ) { if ( strstr( $url, 'https://' ) && ( strstr( $url, '.paypal.com/nvp' ) || strstr( $url, '.paypal.com/cgi-bin/webscr' ) ) ) { curl_setopt( $handle, CURLOPT_SSLVERSION, 6 ); } } } WC_HTTPS::init(); includes/class-wc-install.php 0000644 00000172146 15132754524 0012266 0 ustar 00 <?php /** * Installation related functions and actions. * * @package WooCommerce\Classes * @version 3.0.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper; defined( 'ABSPATH' ) || exit; /** * WC_Install Class. */ class WC_Install { /** * DB updates and callbacks that need to be run per version. * * @var array */ private static $db_updates = array( '2.0.0' => array( 'wc_update_200_file_paths', 'wc_update_200_permalinks', 'wc_update_200_subcat_display', 'wc_update_200_taxrates', 'wc_update_200_line_items', 'wc_update_200_images', 'wc_update_200_db_version', ), '2.0.9' => array( 'wc_update_209_brazillian_state', 'wc_update_209_db_version', ), '2.1.0' => array( 'wc_update_210_remove_pages', 'wc_update_210_file_paths', 'wc_update_210_db_version', ), '2.2.0' => array( 'wc_update_220_shipping', 'wc_update_220_order_status', 'wc_update_220_variations', 'wc_update_220_attributes', 'wc_update_220_db_version', ), '2.3.0' => array( 'wc_update_230_options', 'wc_update_230_db_version', ), '2.4.0' => array( 'wc_update_240_options', 'wc_update_240_shipping_methods', 'wc_update_240_api_keys', 'wc_update_240_refunds', 'wc_update_240_db_version', ), '2.4.1' => array( 'wc_update_241_variations', 'wc_update_241_db_version', ), '2.5.0' => array( 'wc_update_250_currency', 'wc_update_250_db_version', ), '2.6.0' => array( 'wc_update_260_options', 'wc_update_260_termmeta', 'wc_update_260_zones', 'wc_update_260_zone_methods', 'wc_update_260_refunds', 'wc_update_260_db_version', ), '3.0.0' => array( 'wc_update_300_grouped_products', 'wc_update_300_settings', 'wc_update_300_product_visibility', 'wc_update_300_db_version', ), '3.1.0' => array( 'wc_update_310_downloadable_products', 'wc_update_310_old_comments', 'wc_update_310_db_version', ), '3.1.2' => array( 'wc_update_312_shop_manager_capabilities', 'wc_update_312_db_version', ), '3.2.0' => array( 'wc_update_320_mexican_states', 'wc_update_320_db_version', ), '3.3.0' => array( 'wc_update_330_image_options', 'wc_update_330_webhooks', 'wc_update_330_product_stock_status', 'wc_update_330_set_default_product_cat', 'wc_update_330_clear_transients', 'wc_update_330_set_paypal_sandbox_credentials', 'wc_update_330_db_version', ), '3.4.0' => array( 'wc_update_340_states', 'wc_update_340_state', 'wc_update_340_last_active', 'wc_update_340_db_version', ), '3.4.3' => array( 'wc_update_343_cleanup_foreign_keys', 'wc_update_343_db_version', ), '3.4.4' => array( 'wc_update_344_recreate_roles', 'wc_update_344_db_version', ), '3.5.0' => array( 'wc_update_350_reviews_comment_type', 'wc_update_350_db_version', ), '3.5.2' => array( 'wc_update_352_drop_download_log_fk', ), '3.5.4' => array( 'wc_update_354_modify_shop_manager_caps', 'wc_update_354_db_version', ), '3.6.0' => array( 'wc_update_360_product_lookup_tables', 'wc_update_360_term_meta', 'wc_update_360_downloadable_product_permissions_index', 'wc_update_360_db_version', ), '3.7.0' => array( 'wc_update_370_tax_rate_classes', 'wc_update_370_mro_std_currency', 'wc_update_370_db_version', ), '3.9.0' => array( 'wc_update_390_move_maxmind_database', 'wc_update_390_change_geolocation_database_update_cron', 'wc_update_390_db_version', ), '4.0.0' => array( 'wc_update_product_lookup_tables', 'wc_update_400_increase_size_of_column', 'wc_update_400_reset_action_scheduler_migration_status', 'wc_update_400_db_version', ), '4.4.0' => array( 'wc_update_440_insert_attribute_terms_for_variable_products', 'wc_update_440_db_version', ), '4.5.0' => array( 'wc_update_450_sanitize_coupons_code', 'wc_update_450_db_version', ), '5.0.0' => array( 'wc_update_500_fix_product_review_count', 'wc_update_500_db_version', ), '5.6.0' => array( 'wc_update_560_create_refund_returns_page', 'wc_update_560_db_version', ), ); /** * Hook in tabs. */ public static function init() { add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 ); add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) ); add_action( 'admin_init', array( __CLASS__, 'add_admin_note_after_page_created' ) ); add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) ); add_action( 'admin_init', array( __CLASS__, 'install_actions' ) ); add_action( 'woocommerce_page_created', array( __CLASS__, 'page_created' ), 10, 2 ); add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) ); add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); } /** * Check WooCommerce version and run the updater is required. * * This check is done on all requests and runs if the versions do not match. */ public static function check_version() { if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && version_compare( get_option( 'woocommerce_version' ), WC()->version, '<' ) ) { self::install(); do_action( 'woocommerce_updated' ); } } /** * Performan manual database update when triggered by WooCommerce System Tools. * * @since 3.6.5 */ public static function manual_database_update() { $blog_id = get_current_blog_id(); add_action( 'wp_' . $blog_id . '_wc_updater_cron', array( __CLASS__, 'run_manual_database_update' ) ); } /** * Add WC Admin based db update notice. * * @since 4.0.0 */ public static function wc_admin_db_update_notice() { if ( WC()->is_wc_admin_active() && false !== get_option( 'woocommerce_admin_install_timestamp' ) ) { new WC_Notes_Run_Db_Update(); } } /** * Run manual database update. */ public static function run_manual_database_update() { self::update(); } /** * Run an update callback when triggered by ActionScheduler. * * @param string $update_callback Callback name. * * @since 3.6.0 */ public static function run_update_callback( $update_callback ) { include_once dirname( __FILE__ ) . '/wc-update-functions.php'; if ( is_callable( $update_callback ) ) { self::run_update_callback_start( $update_callback ); $result = (bool) call_user_func( $update_callback ); self::run_update_callback_end( $update_callback, $result ); } } /** * Triggered when a callback will run. * * @since 3.6.0 * @param string $callback Callback name. */ protected static function run_update_callback_start( $callback ) { wc_maybe_define_constant( 'WC_UPDATING', true ); } /** * Triggered when a callback has ran. * * @since 3.6.0 * @param string $callback Callback name. * @param bool $result Return value from callback. Non-false need to run again. */ protected static function run_update_callback_end( $callback, $result ) { if ( $result ) { WC()->queue()->add( 'woocommerce_run_update_callback', array( 'update_callback' => $callback, ), 'woocommerce-db-updates' ); } } /** * Install actions when a update button is clicked within the admin area. * * This function is hooked into admin_init to affect admin only. */ public static function install_actions() { if ( ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok. check_admin_referer( 'wc_db_update', 'wc_db_update_nonce' ); self::update(); WC_Admin_Notices::add_notice( 'update', true ); } } /** * Install WC. */ public static function install() { if ( ! is_blog_installed() ) { return; } // Check if we are not already running this routine. if ( 'yes' === get_transient( 'wc_installing' ) ) { return; } // If we made it till here nothing is running yet, lets set the transient now. set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 ); wc_maybe_define_constant( 'WC_INSTALLING', true ); WC()->wpdb_table_fix(); self::remove_admin_notices(); self::create_tables(); self::verify_base_tables(); self::create_options(); self::create_roles(); self::setup_environment(); self::create_terms(); self::create_cron_jobs(); self::create_files(); self::maybe_create_pages(); self::maybe_set_activation_transients(); self::set_paypal_standard_load_eligibility(); self::update_wc_version(); self::maybe_update_db_version(); delete_transient( 'wc_installing' ); do_action( 'woocommerce_flush_rewrite_rules' ); do_action( 'woocommerce_installed' ); } /** * Check if all the base tables are present. * * @param bool $modify_notice Whether to modify notice based on if all tables are present. * @param bool $execute Whether to execute get_schema queries as well. * * @return array List of querues. */ public static function verify_base_tables( $modify_notice = true, $execute = false ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; if ( $execute ) { self::create_tables(); } $queries = dbDelta( self::get_schema(), false ); $missing_tables = array(); foreach ( $queries as $table_name => $result ) { if ( "Created table $table_name" === $result ) { $missing_tables[] = $table_name; } } if ( 0 < count( $missing_tables ) ) { if ( $modify_notice ) { WC_Admin_Notices::add_notice( 'base_tables_missing' ); } update_option( 'woocommerce_schema_missing_tables', $missing_tables ); } else { if ( $modify_notice ) { WC_Admin_Notices::remove_notice( 'base_tables_missing' ); } update_option( 'woocommerce_schema_version', WC()->db_version ); delete_option( 'woocommerce_schema_missing_tables' ); } return $missing_tables; } /** * Reset any notices added to admin. * * @since 3.2.0 */ private static function remove_admin_notices() { include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php'; WC_Admin_Notices::remove_all_notices(); } /** * Setup WC environment - post types, taxonomies, endpoints. * * @since 3.2.0 */ private static function setup_environment() { WC_Post_types::register_post_types(); WC_Post_types::register_taxonomies(); WC()->query->init_query_vars(); WC()->query->add_endpoints(); WC_API::add_endpoint(); WC_Auth::add_endpoint(); } /** * Is this a brand new WC install? * * A brand new install has no version yet. Also treat empty installs as 'new'. * * @since 3.2.0 * @return boolean */ public static function is_new_install() { $product_count = array_sum( (array) wp_count_posts( 'product' ) ); return is_null( get_option( 'woocommerce_version', null ) ) || ( 0 === $product_count && -1 === wc_get_page_id( 'shop' ) ); } /** * Is a DB update needed? * * @since 3.2.0 * @return boolean */ public static function needs_db_update() { $current_db_version = get_option( 'woocommerce_db_version', null ); $updates = self::get_db_update_callbacks(); $update_versions = array_keys( $updates ); usort( $update_versions, 'version_compare' ); return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' ); } /** * See if we need to set redirect transients for activation or not. * * @since 4.6.0 */ private static function maybe_set_activation_transients() { if ( self::is_new_install() ) { set_transient( '_wc_activation_redirect', 1, 30 ); } } /** * See if we need to show or run database updates during install. * * @since 3.2.0 */ private static function maybe_update_db_version() { if ( self::needs_db_update() ) { if ( apply_filters( 'woocommerce_enable_auto_update_db', false ) ) { self::update(); } else { WC_Admin_Notices::add_notice( 'update', true ); } } else { self::update_db_version(); } } /** * Update WC version to current. */ private static function update_wc_version() { update_option( 'woocommerce_version', WC()->version ); } /** * Get list of DB update callbacks. * * @since 3.0.0 * @return array */ public static function get_db_update_callbacks() { return self::$db_updates; } /** * Push all needed DB updates to the queue for processing. */ private static function update() { $current_db_version = get_option( 'woocommerce_db_version' ); $loop = 0; foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { if ( version_compare( $current_db_version, $version, '<' ) ) { foreach ( $update_callbacks as $update_callback ) { WC()->queue()->schedule_single( time() + $loop, 'woocommerce_run_update_callback', array( 'update_callback' => $update_callback, ), 'woocommerce-db-updates' ); $loop++; } } } } /** * Update DB version to current. * * @param string|null $version New WooCommerce DB version or null. */ public static function update_db_version( $version = null ) { update_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version ); } /** * Add more cron schedules. * * @param array $schedules List of WP scheduled cron jobs. * * @return array */ public static function cron_schedules( $schedules ) { $schedules['monthly'] = array( 'interval' => 2635200, 'display' => __( 'Monthly', 'woocommerce' ), ); $schedules['fifteendays'] = array( 'interval' => 1296000, 'display' => __( 'Every 15 Days', 'woocommerce' ), ); return $schedules; } /** * Create cron jobs (clear them first). */ private static function create_cron_jobs() { wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' ); wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' ); wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' ); wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' ); wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); $ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+'; wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' ); $held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' ); if ( '' !== $held_duration ) { $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); } // Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds // so it doesn't occur in the same request. WooCommerce Admin also schedules // a daily cron that gets lost due to a race condition. WC_Privacy's background // processing instance updates the cron schedule from within a cron job. wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' ); wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' ); wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' ); wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); } /** * Create pages on installation. */ public static function maybe_create_pages() { if ( empty( get_option( 'woocommerce_db_version' ) ) ) { self::create_pages(); } } /** * Create pages that the plugin relies on, storing page IDs in variables. */ public static function create_pages() { include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php'; $pages = apply_filters( 'woocommerce_create_pages', array( 'shop' => array( 'name' => _x( 'shop', 'Page slug', 'woocommerce' ), 'title' => _x( 'Shop', 'Page title', 'woocommerce' ), 'content' => '', ), 'cart' => array( 'name' => _x( 'cart', 'Page slug', 'woocommerce' ), 'title' => _x( 'Cart', 'Page title', 'woocommerce' ), 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']<!-- /wp:shortcode -->', ), 'checkout' => array( 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']<!-- /wp:shortcode -->', ), 'myaccount' => array( 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), 'title' => _x( 'My account', 'Page title', 'woocommerce' ), 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']<!-- /wp:shortcode -->', ), 'refund_returns' => array( 'name' => _x( 'refund_returns', 'Page slug', 'woocommerce' ), 'title' => _x( 'Refund and Returns Policy', 'Page title', 'woocommerce' ), 'content' => self::get_refunds_return_policy_page_content(), 'post_status' => 'draft', ), ) ); foreach ( $pages as $key => $page ) { wc_create_page( esc_sql( $page['name'] ), 'woocommerce_' . $key . '_page_id', $page['title'], $page['content'], ! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '', ! empty( $page['post_status'] ) ? $page['post_status'] : 'publish' ); } } /** * Default options. * * Sets up the default options used on the settings page. */ private static function create_options() { // Include settings so that we can run through defaults. include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php'; $settings = WC_Admin_Settings::get_settings_pages(); foreach ( $settings as $section ) { if ( ! method_exists( $section, 'get_settings' ) ) { continue; } $subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) ); /** * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. * See the method documentation for an explanation. */ foreach ( $subsections as $subsection ) { foreach ( $section->get_settings( $subsection ) as $value ) { if ( isset( $value['default'] ) && isset( $value['id'] ) ) { $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); } } } } // Define other defaults if not in setting screens. add_option( 'woocommerce_single_image_width', '600', '', 'yes' ); add_option( 'woocommerce_thumbnail_image_width', '300', '', 'yes' ); add_option( 'woocommerce_checkout_highlight_required_fields', 'yes', '', 'yes' ); add_option( 'woocommerce_demo_store', 'no', '', 'no' ); if ( self::is_new_install() ) { // Define initial tax classes. WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) ); WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) ); } } /** * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. */ public static function create_terms() { $taxonomies = array( 'product_type' => array( 'simple', 'grouped', 'variable', 'external', ), 'product_visibility' => array( 'exclude-from-search', 'exclude-from-catalog', 'featured', 'outofstock', 'rated-1', 'rated-2', 'rated-3', 'rated-4', 'rated-5', ), ); foreach ( $taxonomies as $taxonomy => $terms ) { foreach ( $terms as $term ) { if ( ! get_term_by( 'name', $term, $taxonomy ) ) { // @codingStandardsIgnoreLine. wp_insert_term( $term, $taxonomy ); } } } $woocommerce_default_category = (int) get_option( 'default_product_cat', 0 ); if ( ! $woocommerce_default_category || ! term_exists( $woocommerce_default_category, 'product_cat' ) ) { $default_product_cat_id = 0; $default_product_cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) ); $default_product_cat = get_term_by( 'slug', $default_product_cat_slug, 'product_cat' ); // @codingStandardsIgnoreLine. if ( $default_product_cat ) { $default_product_cat_id = absint( $default_product_cat->term_taxonomy_id ); } else { $result = wp_insert_term( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ), 'product_cat', array( 'slug' => $default_product_cat_slug ) ); if ( ! is_wp_error( $result ) && ! empty( $result['term_taxonomy_id'] ) ) { $default_product_cat_id = absint( $result['term_taxonomy_id'] ); } } if ( $default_product_cat_id ) { update_option( 'default_product_cat', $default_product_cat_id ); } } } /** * Set up the database tables which the plugin needs to function. * WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database. * * This is called from `install` method and is executed in-sync when WC is installed or updated. This can also be called optionally from `verify_base_tables`. * * TODO: Add all crucial tables that we have created from workers in the past. * * Tables: * woocommerce_attribute_taxonomies - Table for storing attribute taxonomies - these are user defined * woocommerce_downloadable_product_permissions - Table for storing user and guest download permissions. * KEY(order_id, product_id, download_id) used for organizing downloads on the My Account page * woocommerce_order_items - Order line items are stored in a table to make them easily queryable for reports * woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data. * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. */ private static function create_tables() { global $wpdb; $wpdb->hide_errors(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; /** * Before updating with DBDELTA, remove any primary keys which could be * modified due to schema updates. */ if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) { if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); } } /** * Change wp_woocommerce_sessions schema to use a bigint auto increment field instead of char(32) field as * the primary key as it is not a good practice to use a char(32) field as the primary key of a table and as * there were reports of issues with this table (see https://github.com/woocommerce/woocommerce/issues/20912). * * This query needs to run before dbDelta() as this WP function is not able to handle primary key changes * (see https://github.com/woocommerce/woocommerce/issues/21534 and https://core.trac.wordpress.org/ticket/40357). */ if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_sessions'" ) ) { if ( ! $wpdb->get_var( "SHOW KEYS FROM {$wpdb->prefix}woocommerce_sessions WHERE Key_name = 'PRIMARY' AND Column_name = 'session_id'" ) ) { $wpdb->query( "ALTER TABLE `{$wpdb->prefix}woocommerce_sessions` DROP PRIMARY KEY, DROP KEY `session_id`, ADD PRIMARY KEY(`session_id`), ADD UNIQUE KEY(`session_key`)" ); } } dbDelta( self::get_schema() ); $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); if ( is_null( $index_exists ) ) { // Add an index to the field comment_type to improve the response time of the query // used by WC_Comments::wp_count_comments() to get the number of comments by type. $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); } // Get tables data types and check it matches before adding constraint. $download_log_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}wc_download_log WHERE Field = 'permission_id'", ARRAY_A ); $download_log_column_type = ''; if ( isset( $download_log_columns[0]['Type'] ) ) { $download_log_column_type = $download_log_columns[0]['Type']; } $download_permissions_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE Field = 'permission_id'", ARRAY_A ); $download_permissions_column_type = ''; if ( isset( $download_permissions_columns[0]['Type'] ) ) { $download_permissions_column_type = $download_permissions_columns[0]['Type']; } // Add constraint to download logs if the columns matches. if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) { $fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" ); if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) { $wpdb->query( "ALTER TABLE `{$wpdb->prefix}wc_download_log` ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id` FOREIGN KEY (`permission_id`) REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;" ); } } // Clear table caches. delete_transient( 'wc_attribute_taxonomies' ); } /** * Get Table schema. * * See https://github.com/woocommerce/woocommerce/wiki/Database-Description/ * * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. * * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping * indexes first causes too much load on some servers/larger DB. * * When adding or removing a table, make sure to update the list of tables in WC_Install::get_tables(). * * @return string */ private static function get_schema() { global $wpdb; $collate = ''; if ( $wpdb->has_cap( 'collation' ) ) { $collate = $wpdb->get_charset_collate(); } /* * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. */ $max_index_length = 191; $tables = " CREATE TABLE {$wpdb->prefix}woocommerce_sessions ( session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, session_key char(32) NOT NULL, session_value longtext NOT NULL, session_expiry BIGINT UNSIGNED NOT NULL, PRIMARY KEY (session_id), UNIQUE KEY session_key (session_key) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_api_keys ( key_id BIGINT UNSIGNED NOT NULL auto_increment, user_id BIGINT UNSIGNED NOT NULL, description varchar(200) NULL, permissions varchar(10) NOT NULL, consumer_key char(64) NOT NULL, consumer_secret char(43) NOT NULL, nonces longtext NULL, truncated_key char(7) NOT NULL, last_access datetime NULL default null, PRIMARY KEY (key_id), KEY consumer_key (consumer_key), KEY consumer_secret (consumer_secret) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( attribute_id BIGINT UNSIGNED NOT NULL auto_increment, attribute_name varchar(200) NOT NULL, attribute_label varchar(200) NULL, attribute_type varchar(20) NOT NULL, attribute_orderby varchar(20) NOT NULL, attribute_public int(1) NOT NULL DEFAULT 1, PRIMARY KEY (attribute_id), KEY attribute_name (attribute_name(20)) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( permission_id BIGINT UNSIGNED NOT NULL auto_increment, download_id varchar(36) NOT NULL, product_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, order_key varchar(200) NOT NULL, user_email varchar(200) NOT NULL, user_id BIGINT UNSIGNED NULL, downloads_remaining varchar(9) NULL, access_granted datetime NOT NULL default '0000-00-00 00:00:00', access_expires datetime NULL default null, download_count BIGINT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (permission_id), KEY download_order_key_product (product_id,order_id,order_key(16),download_id), KEY download_order_product (download_id,order_id,product_id), KEY order_id (order_id), KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( order_item_id BIGINT UNSIGNED NOT NULL auto_increment, order_item_name TEXT NOT NULL, order_item_type varchar(200) NOT NULL DEFAULT '', order_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (order_item_id), KEY order_id (order_id) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( meta_id BIGINT UNSIGNED NOT NULL auto_increment, order_item_id BIGINT UNSIGNED NOT NULL, meta_key varchar(255) default NULL, meta_value longtext NULL, PRIMARY KEY (meta_id), KEY order_item_id (order_item_id), KEY meta_key (meta_key(32)) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment, tax_rate_country varchar(2) NOT NULL DEFAULT '', tax_rate_state varchar(200) NOT NULL DEFAULT '', tax_rate varchar(8) NOT NULL DEFAULT '', tax_rate_name varchar(200) NOT NULL DEFAULT '', tax_rate_priority BIGINT UNSIGNED NOT NULL, tax_rate_compound int(1) NOT NULL DEFAULT 0, tax_rate_shipping int(1) NOT NULL DEFAULT 1, tax_rate_order BIGINT UNSIGNED NOT NULL, tax_rate_class varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (tax_rate_id), KEY tax_rate_country (tax_rate_country), KEY tax_rate_state (tax_rate_state(2)), KEY tax_rate_class (tax_rate_class(10)), KEY tax_rate_priority (tax_rate_priority) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( location_id BIGINT UNSIGNED NOT NULL auto_increment, location_code varchar(200) NOT NULL, tax_rate_id BIGINT UNSIGNED NOT NULL, location_type varchar(40) NOT NULL, PRIMARY KEY (location_id), KEY tax_rate_id (tax_rate_id), KEY location_type_code (location_type(10),location_code(20)) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones ( zone_id BIGINT UNSIGNED NOT NULL auto_increment, zone_name varchar(200) NOT NULL, zone_order BIGINT UNSIGNED NOT NULL, PRIMARY KEY (zone_id) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations ( location_id BIGINT UNSIGNED NOT NULL auto_increment, zone_id BIGINT UNSIGNED NOT NULL, location_code varchar(200) NOT NULL, location_type varchar(40) NOT NULL, PRIMARY KEY (location_id), KEY location_id (location_id), KEY location_type_code (location_type(10),location_code(20)) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( zone_id BIGINT UNSIGNED NOT NULL, instance_id BIGINT UNSIGNED NOT NULL auto_increment, method_id varchar(200) NOT NULL, method_order BIGINT UNSIGNED NOT NULL, is_enabled tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (instance_id) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( token_id BIGINT UNSIGNED NOT NULL auto_increment, gateway_id varchar(200) NOT NULL, token text NOT NULL, user_id BIGINT UNSIGNED NOT NULL DEFAULT '0', type varchar(200) NOT NULL, is_default tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (token_id), KEY user_id (user_id) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( meta_id BIGINT UNSIGNED NOT NULL auto_increment, payment_token_id BIGINT UNSIGNED NOT NULL, meta_key varchar(255) NULL, meta_value longtext NULL, PRIMARY KEY (meta_id), KEY payment_token_id (payment_token_id), KEY meta_key (meta_key(32)) ) $collate; CREATE TABLE {$wpdb->prefix}woocommerce_log ( log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, timestamp datetime NOT NULL, level smallint(4) NOT NULL, source varchar(200) NOT NULL, message longtext NOT NULL, context longtext NULL, PRIMARY KEY (log_id), KEY level (level) ) $collate; CREATE TABLE {$wpdb->prefix}wc_webhooks ( webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, status varchar(200) NOT NULL, name text NOT NULL, user_id BIGINT UNSIGNED NOT NULL, delivery_url text NOT NULL, secret text NOT NULL, topic varchar(200) NOT NULL, date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00', date_created_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', date_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00', date_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', api_version smallint(4) NOT NULL, failure_count smallint(10) NOT NULL DEFAULT '0', pending_delivery tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (webhook_id), KEY user_id (user_id) ) $collate; CREATE TABLE {$wpdb->prefix}wc_download_log ( download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, timestamp datetime NOT NULL, permission_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NULL, user_ip_address VARCHAR(100) NULL DEFAULT '', PRIMARY KEY (download_log_id), KEY permission_id (permission_id), KEY timestamp (timestamp) ) $collate; CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup ( `product_id` bigint(20) NOT NULL, `sku` varchar(100) NULL default '', `virtual` tinyint(1) NULL default 0, `downloadable` tinyint(1) NULL default 0, `min_price` decimal(19,4) NULL default NULL, `max_price` decimal(19,4) NULL default NULL, `onsale` tinyint(1) NULL default 0, `stock_quantity` double NULL default NULL, `stock_status` varchar(100) NULL default 'instock', `rating_count` bigint(20) NULL default 0, `average_rating` decimal(3,2) NULL default 0.00, `total_sales` bigint(20) NULL default 0, `tax_status` varchar(100) NULL default 'taxable', `tax_class` varchar(100) NULL default '', PRIMARY KEY (`product_id`), KEY `virtual` (`virtual`), KEY `downloadable` (`downloadable`), KEY `stock_status` (`stock_status`), KEY `stock_quantity` (`stock_quantity`), KEY `onsale` (`onsale`), KEY min_max_price (`min_price`, `max_price`) ) $collate; CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes ( tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment, name varchar(200) NOT NULL DEFAULT '', slug varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (tax_rate_class_id), UNIQUE KEY slug (slug($max_index_length)) ) $collate; CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `stock_quantity` double NOT NULL DEFAULT 0, `timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`order_id`, `product_id`) ) $collate; "; return $tables; } /** * Return a list of WooCommerce tables. Used to make sure all WC tables are dropped when uninstalling the plugin * in a single site or multi site environment. * * @return array WC tables. */ public static function get_tables() { global $wpdb; $tables = array( "{$wpdb->prefix}wc_download_log", "{$wpdb->prefix}wc_product_meta_lookup", "{$wpdb->prefix}wc_tax_rate_classes", "{$wpdb->prefix}wc_webhooks", "{$wpdb->prefix}woocommerce_api_keys", "{$wpdb->prefix}woocommerce_attribute_taxonomies", "{$wpdb->prefix}woocommerce_downloadable_product_permissions", "{$wpdb->prefix}woocommerce_log", "{$wpdb->prefix}woocommerce_order_itemmeta", "{$wpdb->prefix}woocommerce_order_items", "{$wpdb->prefix}woocommerce_payment_tokenmeta", "{$wpdb->prefix}woocommerce_payment_tokens", "{$wpdb->prefix}woocommerce_sessions", "{$wpdb->prefix}woocommerce_shipping_zone_locations", "{$wpdb->prefix}woocommerce_shipping_zone_methods", "{$wpdb->prefix}woocommerce_shipping_zones", "{$wpdb->prefix}woocommerce_tax_rate_locations", "{$wpdb->prefix}woocommerce_tax_rates", "{$wpdb->prefix}wc_reserved_stock", ); /** * Filter the list of known WooCommerce tables. * * If WooCommerce plugins need to add new tables, they can inject them here. * * @param array $tables An array of WooCommerce-specific database table names. */ $tables = apply_filters( 'woocommerce_install_get_tables', $tables ); return $tables; } /** * Drop WooCommerce tables. * * @return void */ public static function drop_tables() { global $wpdb; $tables = self::get_tables(); foreach ( $tables as $table ) { $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } /** * Uninstall tables when MU blog is deleted. * * @param array $tables List of tables that will be deleted by WP. * * @return string[] */ public static function wpmu_drop_tables( $tables ) { return array_merge( $tables, self::get_tables() ); } /** * Create roles and capabilities. */ public static function create_roles() { global $wp_roles; if ( ! class_exists( 'WP_Roles' ) ) { return; } if ( ! isset( $wp_roles ) ) { $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine } // Dummy gettext calls to get strings in the catalog. /* translators: user role */ _x( 'Customer', 'User role', 'woocommerce' ); /* translators: user role */ _x( 'Shop manager', 'User role', 'woocommerce' ); // Customer role. add_role( 'customer', 'Customer', array( 'read' => true, ) ); // Shop manager role. add_role( 'shop_manager', 'Shop manager', array( 'level_9' => true, 'level_8' => true, 'level_7' => true, 'level_6' => true, 'level_5' => true, 'level_4' => true, 'level_3' => true, 'level_2' => true, 'level_1' => true, 'level_0' => true, 'read' => true, 'read_private_pages' => true, 'read_private_posts' => true, 'edit_posts' => true, 'edit_pages' => true, 'edit_published_posts' => true, 'edit_published_pages' => true, 'edit_private_pages' => true, 'edit_private_posts' => true, 'edit_others_posts' => true, 'edit_others_pages' => true, 'publish_posts' => true, 'publish_pages' => true, 'delete_posts' => true, 'delete_pages' => true, 'delete_private_pages' => true, 'delete_private_posts' => true, 'delete_published_pages' => true, 'delete_published_posts' => true, 'delete_others_posts' => true, 'delete_others_pages' => true, 'manage_categories' => true, 'manage_links' => true, 'moderate_comments' => true, 'upload_files' => true, 'export' => true, 'import' => true, 'list_users' => true, 'edit_theme_options' => true, ) ); $capabilities = self::get_core_capabilities(); foreach ( $capabilities as $cap_group ) { foreach ( $cap_group as $cap ) { $wp_roles->add_cap( 'shop_manager', $cap ); $wp_roles->add_cap( 'administrator', $cap ); } } } /** * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset. * * @return array */ public static function get_core_capabilities() { $capabilities = array(); $capabilities['core'] = array( 'manage_woocommerce', 'view_woocommerce_reports', ); $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); foreach ( $capability_types as $capability_type ) { $capabilities[ $capability_type ] = array( // Post type. "edit_{$capability_type}", "read_{$capability_type}", "delete_{$capability_type}", "edit_{$capability_type}s", "edit_others_{$capability_type}s", "publish_{$capability_type}s", "read_private_{$capability_type}s", "delete_{$capability_type}s", "delete_private_{$capability_type}s", "delete_published_{$capability_type}s", "delete_others_{$capability_type}s", "edit_private_{$capability_type}s", "edit_published_{$capability_type}s", // Terms. "manage_{$capability_type}_terms", "edit_{$capability_type}_terms", "delete_{$capability_type}_terms", "assign_{$capability_type}_terms", ); } return $capabilities; } /** * Remove WooCommerce roles. */ public static function remove_roles() { global $wp_roles; if ( ! class_exists( 'WP_Roles' ) ) { return; } if ( ! isset( $wp_roles ) ) { $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine } $capabilities = self::get_core_capabilities(); foreach ( $capabilities as $cap_group ) { foreach ( $cap_group as $cap ) { $wp_roles->remove_cap( 'shop_manager', $cap ); $wp_roles->remove_cap( 'administrator', $cap ); } } remove_role( 'customer' ); remove_role( 'shop_manager' ); } /** * Create files/directories. */ private static function create_files() { // Bypass if filesystem is read-only and/or non-standard upload system is used. if ( apply_filters( 'woocommerce_install_skip_create_files', false ) ) { return; } // Install files and folders for uploading files and prevent hotlinking. $upload_dir = wp_get_upload_dir(); $download_method = get_option( 'woocommerce_file_download_method', 'force' ); $files = array( array( 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', 'file' => 'index.html', 'content' => '', ), array( 'base' => WC_LOG_DIR, 'file' => '.htaccess', 'content' => 'deny from all', ), array( 'base' => WC_LOG_DIR, 'file' => 'index.html', 'content' => '', ), array( 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', 'file' => '.htaccess', 'content' => 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all', ), ); foreach ( $files as $file ) { if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen if ( $file_handle ) { fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose } } } // Create attachment for placeholders. self::create_placeholder_image(); } /** * Create a placeholder image in the media library. * * @since 3.5.0 */ private static function create_placeholder_image() { $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); // Validate current setting if set. If set, return. if ( ! empty( $placeholder_image ) ) { if ( ! is_numeric( $placeholder_image ) ) { return; } elseif ( $placeholder_image && wp_attachment_is_image( $placeholder_image ) ) { return; } } $upload_dir = wp_upload_dir(); $source = WC()->plugin_path() . '/assets/images/placeholder-attachment.png'; $filename = $upload_dir['basedir'] . '/woocommerce-placeholder.png'; if ( ! file_exists( $filename ) ) { copy( $source, $filename ); // @codingStandardsIgnoreLine. } if ( ! file_exists( $filename ) ) { update_option( 'woocommerce_placeholder_image', 0 ); return; } $filetype = wp_check_filetype( basename( $filename ), null ); $attachment = array( 'guid' => $upload_dir['url'] . '/' . basename( $filename ), 'post_mime_type' => $filetype['type'], 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ), 'post_content' => '', 'post_status' => 'inherit', ); $attach_id = wp_insert_attachment( $attachment, $filename ); if ( is_wp_error( $attach_id ) ) { update_option( 'woocommerce_placeholder_image', 0 ); return; } update_option( 'woocommerce_placeholder_image', $attach_id ); // Make sure that this file is included, as wp_generate_attachment_metadata() depends on it. require_once ABSPATH . 'wp-admin/includes/image.php'; // Generate the metadata for the attachment, and update the database record. $attach_data = wp_generate_attachment_metadata( $attach_id, $filename ); wp_update_attachment_metadata( $attach_id, $attach_data ); } /** * Show action links on the plugin screen. * * @param mixed $links Plugin Action links. * * @return array */ public static function plugin_action_links( $links ) { $action_links = array( 'settings' => '<a href="' . admin_url( 'admin.php?page=wc-settings' ) . '" aria-label="' . esc_attr__( 'View WooCommerce settings', 'woocommerce' ) . '">' . esc_html__( 'Settings', 'woocommerce' ) . '</a>', ); return array_merge( $action_links, $links ); } /** * Show row meta on the plugin screen. * * @param mixed $links Plugin Row Meta. * @param mixed $file Plugin Base file. * * @return array */ public static function plugin_row_meta( $links, $file ) { if ( WC_PLUGIN_BASENAME !== $file ) { return $links; } $row_meta = array( 'docs' => '<a href="' . esc_url( apply_filters( 'woocommerce_docs_url', 'https://docs.woocommerce.com/documentation/plugins/woocommerce/' ) ) . '" aria-label="' . esc_attr__( 'View WooCommerce documentation', 'woocommerce' ) . '">' . esc_html__( 'Docs', 'woocommerce' ) . '</a>', 'apidocs' => '<a href="' . esc_url( apply_filters( 'woocommerce_apidocs_url', 'https://docs.woocommerce.com/wc-apidocs/' ) ) . '" aria-label="' . esc_attr__( 'View WooCommerce API docs', 'woocommerce' ) . '">' . esc_html__( 'API docs', 'woocommerce' ) . '</a>', 'support' => '<a href="' . esc_url( apply_filters( 'woocommerce_community_support_url', 'https://wordpress.org/support/plugin/woocommerce/' ) ) . '" aria-label="' . esc_attr__( 'Visit community forums', 'woocommerce' ) . '">' . esc_html__( 'Community support', 'woocommerce' ) . '</a>', ); if ( WCConnectionHelper::is_connected() ) { $row_meta['premium_support'] = '<a href="' . esc_url( apply_filters( 'woocommerce_support_url', 'https://woocommerce.com/my-account/create-a-ticket/' ) ) . '" aria-label="' . esc_attr__( 'Visit premium customer support', 'woocommerce' ) . '">' . esc_html__( 'Premium support', 'woocommerce' ) . '</a>'; } return array_merge( $links, $row_meta ); } /** * Get slug from path and associate it with the path. * * @param array $plugins Associative array of plugin files to paths. * @param string $key Plugin relative path. Example: woocommerce/woocommerce.php. */ private static function associate_plugin_file( $plugins, $key ) { $path = explode( '/', $key ); $filename = end( $path ); $plugins[ $filename ] = $key; return $plugins; } /** * Install a plugin from .org in the background via a cron job (used by * installer - opt in). * * @param string $plugin_to_install_id Plugin ID. * @param array $plugin_to_install Plugin information. * * @throws Exception If unable to proceed with plugin installation. * @since 2.6.0 */ public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { // Explicitly clear the event. $args = func_get_args(); if ( ! empty( $plugin_to_install['repo-slug'] ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php'; WP_Filesystem(); $skin = new Automatic_Upgrader_Skin(); $upgrader = new WP_Upgrader( $skin ); $installed_plugins = array_reduce( array_keys( get_plugins() ), array( __CLASS__, 'associate_plugin_file' ) ); if ( empty( $installed_plugins ) ) { $installed_plugins = array(); } $plugin_slug = $plugin_to_install['repo-slug']; $plugin_file = isset( $plugin_to_install['file'] ) ? $plugin_to_install['file'] : $plugin_slug . '.php'; $installed = false; $activate = false; // See if the plugin is installed already. if ( isset( $installed_plugins[ $plugin_file ] ) ) { $installed = true; $activate = ! is_plugin_active( $installed_plugins[ $plugin_file ] ); } // Install this thing! if ( ! $installed ) { // Suppress feedback. ob_start(); try { $plugin_information = plugins_api( 'plugin_information', array( 'slug' => $plugin_slug, 'fields' => array( 'short_description' => false, 'sections' => false, 'requires' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, 'author_profile' => false, 'author' => false, ), ) ); if ( is_wp_error( $plugin_information ) ) { throw new Exception( $plugin_information->get_error_message() ); } $package = $plugin_information->download_link; $download = $upgrader->download_package( $package ); if ( is_wp_error( $download ) ) { throw new Exception( $download->get_error_message() ); } $working_dir = $upgrader->unpack_package( $download, true ); if ( is_wp_error( $working_dir ) ) { throw new Exception( $working_dir->get_error_message() ); } $result = $upgrader->install_package( array( 'source' => $working_dir, 'destination' => WP_PLUGIN_DIR, 'clear_destination' => false, 'abort_if_destination_exists' => false, 'clear_working' => true, 'hook_extra' => array( 'type' => 'plugin', 'action' => 'install', ), ) ); if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } $activate = true; } catch ( Exception $e ) { WC_Admin_Notices::add_custom_notice( $plugin_to_install_id . '_install_error', sprintf( // translators: 1: plugin name, 2: error message, 3: URL to install plugin manually. __( '%1$s could not be installed (%2$s). <a href="%3$s">Please install it manually by clicking here.</a>', 'woocommerce' ), $plugin_to_install['name'], $e->getMessage(), esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) ) ) ); } // Discard feedback. ob_end_clean(); } wp_clean_plugins_cache(); // Activate this thing. if ( $activate ) { try { add_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ), 10, 2 ); $result = activate_plugin( $installed ? $installed_plugins[ $plugin_file ] : $plugin_slug . '/' . $plugin_file ); if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } } catch ( Exception $e ) { WC_Admin_Notices::add_custom_notice( $plugin_to_install_id . '_install_error', sprintf( // translators: 1: plugin name, 2: URL to WP plugin page. __( '%1$s was installed but could not be activated. <a href="%2$s">Please activate it manually by clicking here.</a>', 'woocommerce' ), $plugin_to_install['name'], admin_url( 'plugins.php' ) ) ); } } } } /** * Removes redirect added during MailChimp plugin's activation. * * @param string $option Option name. * @param string $value Option value. */ public static function remove_mailchimps_redirect( $option, $value ) { // Remove this action to prevent infinite looping. remove_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ) ); // Update redirect back to false. update_option( 'mailchimp_woocommerce_plugin_do_activation_redirect', false ); } /** * Install a theme from .org in the background via a cron job (used by installer - opt in). * * @param string $theme_slug Theme slug. * * @throws Exception If unable to proceed with theme installation. * @since 3.1.0 */ public static function theme_background_installer( $theme_slug ) { // Explicitly clear the event. $args = func_get_args(); if ( ! empty( $theme_slug ) ) { // Suppress feedback. ob_start(); try { $theme = wp_get_theme( $theme_slug ); if ( ! $theme->exists() ) { require_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/theme.php'; WP_Filesystem(); $skin = new Automatic_Upgrader_Skin(); $upgrader = new Theme_Upgrader( $skin ); $api = themes_api( 'theme_information', array( 'slug' => $theme_slug, 'fields' => array( 'sections' => false ), ) ); $result = $upgrader->install( $api->download_link ); if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } elseif ( is_wp_error( $skin->result ) ) { throw new Exception( $skin->result->get_error_message() ); } elseif ( is_null( $result ) ) { throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' ); } } switch_theme( $theme_slug ); } catch ( Exception $e ) { WC_Admin_Notices::add_custom_notice( $theme_slug . '_install_error', sprintf( // translators: 1: theme slug, 2: error message, 3: URL to install theme manually. __( '%1$s could not be installed (%2$s). <a href="%3$s">Please install it manually by clicking here.</a>', 'woocommerce' ), $theme_slug, $e->getMessage(), esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) ) ) ); } // Discard feedback. ob_end_clean(); } } /** * Sets whether PayPal Standard will be loaded on install. * * @since 5.5.0 */ private static function set_paypal_standard_load_eligibility() { // Initiating the payment gateways sets the flag. if ( class_exists( 'WC_Gateway_Paypal' ) ) { ( new WC_Gateway_Paypal() )->should_load(); } } /** * Gets the content of the sample refunds and return policy page. * * @since 5.6.0 * @return HTML The content for the page */ private static function get_refunds_return_policy_page_content() { return <<<EOT <!-- wp:paragraph --> <p><b>This is a sample page.</b></p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <h3>Overview</h3> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Our refund and returns policy lasts 30 days. If 30 days have passed since your purchase, we can’t offer you a full refund or exchange.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>To be eligible for a return, your item must be unused and in the same condition that you received it. It must also be in the original packaging.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Several types of goods are exempt from being returned. Perishable goods such as food, flowers, newspapers or magazines cannot be returned. We also do not accept products that are intimate or sanitary goods, hazardous materials, or flammable liquids or gases.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Additional non-returnable items:</p> <!-- /wp:paragraph --> <!-- wp:list --> <ul> <li>Gift cards</li> <li>Downloadable software products</li> <li>Some health and personal care items</li> </ul> <!-- /wp:list --> <!-- wp:paragraph --> <p>To complete your return, we require a receipt or proof of purchase.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Please do not send your purchase back to the manufacturer.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>There are certain situations where only partial refunds are granted:</p> <!-- /wp:paragraph --> <!-- wp:list --> <ul> <li>Book with obvious signs of use</li> <li>CD, DVD, VHS tape, software, video game, cassette tape, or vinyl record that has been opened.</li> <li>Any item not in its original condition, is damaged or missing parts for reasons not due to our error.</li> <li>Any item that is returned more than 30 days after delivery</li> </ul> <!-- /wp:list --> <!-- wp:paragraph --> <h2>Refunds</h2> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Once your return is received and inspected, we will send you an email to notify you that we have received your returned item. We will also notify you of the approval or rejection of your refund.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If you are approved, then your refund will be processed, and a credit will automatically be applied to your credit card or original method of payment, within a certain amount of days.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <b>Late or missing refunds</b> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If you haven’t received a refund yet, first check your bank account again.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Then contact your credit card company, it may take some time before your refund is officially posted.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Next contact your bank. There is often some processing time before a refund is posted.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If you’ve done all of this and you still have not received your refund yet, please contact us at {email address}.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <b>Sale items</b> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Only regular priced items may be refunded. Sale items cannot be refunded.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <h2>Exchanges</h2> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>We only replace items if they are defective or damaged. If you need to exchange it for the same item, send us an email at {email address} and send your item to: {physical address}.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <h2>Gifts</h2> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If the item was marked as a gift when purchased and shipped directly to you, you’ll receive a gift credit for the value of your return. Once the returned item is received, a gift certificate will be mailed to you.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If the item wasn’t marked as a gift when purchased, or the gift giver had the order shipped to themselves to give to you later, we will send a refund to the gift giver and they will find out about your return.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <h2>Shipping returns</h2> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>To return your product, you should mail your product to: {physical address}.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>You will be responsible for paying for your own shipping costs for returning your item. Shipping costs are non-refundable. If you receive a refund, the cost of return shipping will be deducted from your refund.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Depending on where you live, the time it may take for your exchanged product to reach you may vary.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>If you are returning more expensive items, you may consider using a trackable shipping service or purchasing shipping insurance. We don’t guarantee that we will receive your returned item.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <h2>Need help?</h2> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Contact us at {email} for questions related to refunds and returns.</p> <!-- /wp:paragraph --> EOT; } /** * Adds an admin inbox note after a page has been created to notify * user. For example to take action to edit the page such as the * Refund and returns page. * * @since 5.6.0 * @return void */ public static function add_admin_note_after_page_created() { if ( ! WC()->is_wc_admin_active() ) { return; } $page_id = get_option( 'woocommerce_refund_returns_page_created', null ); if ( null === $page_id ) { return; } WC_Notes_Refund_Returns::possibly_add_note( $page_id ); } /** * When pages are created, we might want to take some action. * In this case we want to set an option when refund and returns * page is created. * * @since 5.6.0 * @param int $page_id ID of the page. * @param array $page_data The data of the page created. * @return void */ public static function page_created( $page_id, $page_data ) { if ( 'refund_returns' === $page_data['post_name'] ) { delete_option( 'woocommerce_refund_returns_page_created' ); add_option( 'woocommerce_refund_returns_page_created', $page_id, '', false ); } } } WC_Install::init(); includes/wc-conditional-functions.php 0000644 00000027464 15132754524 0014030 0 ustar 00 <?php /** * WooCommerce Conditional Functions * * Functions for determining the current query/page. * * @package WooCommerce\Functions * @version 2.3.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Is_woocommerce - Returns true if on a page which uses WooCommerce templates (cart and checkout are standard pages with shortcodes and thus are not included). * * @return bool */ function is_woocommerce() { return apply_filters( 'is_woocommerce', is_shop() || is_product_taxonomy() || is_product() ); } if ( ! function_exists( 'is_shop' ) ) { /** * Is_shop - Returns true when viewing the product type archive (shop). * * @return bool */ function is_shop() { return ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ); } } if ( ! function_exists( 'is_product_taxonomy' ) ) { /** * Is_product_taxonomy - Returns true when viewing a product taxonomy archive. * * @return bool */ function is_product_taxonomy() { return is_tax( get_object_taxonomies( 'product' ) ); } } if ( ! function_exists( 'is_product_category' ) ) { /** * Is_product_category - Returns true when viewing a product category. * * @param string $term (default: '') The term slug your checking for. Leave blank to return true on any. * @return bool */ function is_product_category( $term = '' ) { return is_tax( 'product_cat', $term ); } } if ( ! function_exists( 'is_product_tag' ) ) { /** * Is_product_tag - Returns true when viewing a product tag. * * @param string $term (default: '') The term slug your checking for. Leave blank to return true on any. * @return bool */ function is_product_tag( $term = '' ) { return is_tax( 'product_tag', $term ); } } if ( ! function_exists( 'is_product' ) ) { /** * Is_product - Returns true when viewing a single product. * * @return bool */ function is_product() { return is_singular( array( 'product' ) ); } } if ( ! function_exists( 'is_cart' ) ) { /** * Is_cart - Returns true when viewing the cart page. * * @return bool */ function is_cart() { $page_id = wc_get_page_id( 'cart' ); return ( $page_id && is_page( $page_id ) ) || Constants::is_defined( 'WOOCOMMERCE_CART' ) || wc_post_content_has_shortcode( 'woocommerce_cart' ); } } if ( ! function_exists( 'is_checkout' ) ) { /** * Is_checkout - Returns true when viewing the checkout page. * * @return bool */ function is_checkout() { $page_id = wc_get_page_id( 'checkout' ); return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_checkout' ) || apply_filters( 'woocommerce_is_checkout', false ) || Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ); } } if ( ! function_exists( 'is_checkout_pay_page' ) ) { /** * Is_checkout_pay - Returns true when viewing the checkout's pay page. * * @return bool */ function is_checkout_pay_page() { global $wp; return is_checkout() && ! empty( $wp->query_vars['order-pay'] ); } } if ( ! function_exists( 'is_wc_endpoint_url' ) ) { /** * Is_wc_endpoint_url - Check if an endpoint is showing. * * @param string|false $endpoint Whether endpoint. * @return bool */ function is_wc_endpoint_url( $endpoint = false ) { global $wp; $wc_endpoints = WC()->query->get_query_vars(); if ( false !== $endpoint ) { if ( ! isset( $wc_endpoints[ $endpoint ] ) ) { return false; } else { $endpoint_var = $wc_endpoints[ $endpoint ]; } return isset( $wp->query_vars[ $endpoint_var ] ); } else { foreach ( $wc_endpoints as $key => $value ) { if ( isset( $wp->query_vars[ $key ] ) ) { return true; } } return false; } } } if ( ! function_exists( 'is_account_page' ) ) { /** * Is_account_page - Returns true when viewing an account page. * * @return bool */ function is_account_page() { $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_my_account' ) || apply_filters( 'woocommerce_is_account_page', false ); } } if ( ! function_exists( 'is_view_order_page' ) ) { /** * Is_view_order_page - Returns true when on the view order page. * * @return bool */ function is_view_order_page() { global $wp; $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['view-order'] ) ); } } if ( ! function_exists( 'is_edit_account_page' ) ) { /** * Check for edit account page. * Returns true when viewing the edit account page. * * @since 2.5.1 * @return bool */ function is_edit_account_page() { global $wp; $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['edit-account'] ) ); } } if ( ! function_exists( 'is_order_received_page' ) ) { /** * Is_order_received_page - Returns true when viewing the order received page. * * @return bool */ function is_order_received_page() { global $wp; $page_id = wc_get_page_id( 'checkout' ); return apply_filters( 'woocommerce_is_order_received_page', ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['order-received'] ) ) ); } } if ( ! function_exists( 'is_add_payment_method_page' ) ) { /** * Is_add_payment_method_page - Returns true when viewing the add payment method page. * * @return bool */ function is_add_payment_method_page() { global $wp; $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) || isset( $wp->query_vars['add-payment-method'] ) ) ); } } if ( ! function_exists( 'is_lost_password_page' ) ) { /** * Is_lost_password_page - Returns true when viewing the lost password page. * * @return bool */ function is_lost_password_page() { global $wp; $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['lost-password'] ) ); } } if ( ! function_exists( 'is_ajax' ) ) { /** * Is_ajax - Returns true when the page is loaded via ajax. * * @return bool */ function is_ajax() { return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : Constants::is_defined( 'DOING_AJAX' ); } } if ( ! function_exists( 'is_store_notice_showing' ) ) { /** * Is_store_notice_showing - Returns true when store notice is active. * * @return bool */ function is_store_notice_showing() { return 'no' !== get_option( 'woocommerce_demo_store', 'no' ); } } if ( ! function_exists( 'is_filtered' ) ) { /** * Is_filtered - Returns true when filtering products using layered nav or price sliders. * * @return bool */ function is_filtered() { return apply_filters( 'woocommerce_is_filtered', ( count( WC_Query::get_layered_nav_chosen_attributes() ) > 0 || isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) || isset( $_GET['rating_filter'] ) ) ); // WPCS: CSRF ok. } } if ( ! function_exists( 'taxonomy_is_product_attribute' ) ) { /** * Returns true when the passed taxonomy name is a product attribute. * * @uses $wc_product_attributes global which stores taxonomy names upon registration * @param string $name of the attribute. * @return bool */ function taxonomy_is_product_attribute( $name ) { global $wc_product_attributes; return taxonomy_exists( $name ) && array_key_exists( $name, (array) $wc_product_attributes ); } } if ( ! function_exists( 'meta_is_product_attribute' ) ) { /** * Returns true when the passed meta name is a product attribute. * * @param string $name of the attribute. * @param string $value of the attribute. * @param int $product_id to check for attribute. * @return bool */ function meta_is_product_attribute( $name, $value, $product_id ) { $product = wc_get_product( $product_id ); if ( $product && method_exists( $product, 'get_variation_attributes' ) ) { $variation_attributes = $product->get_variation_attributes(); $attributes = $product->get_attributes(); return ( in_array( $name, array_keys( $attributes ), true ) && in_array( $value, $variation_attributes[ $attributes[ $name ]['name'] ], true ) ); } else { return false; } } } if ( ! function_exists( 'wc_tax_enabled' ) ) { /** * Are store-wide taxes enabled? * * @return bool */ function wc_tax_enabled() { return apply_filters( 'wc_tax_enabled', get_option( 'woocommerce_calc_taxes' ) === 'yes' ); } } if ( ! function_exists( 'wc_shipping_enabled' ) ) { /** * Is shipping enabled? * * @return bool */ function wc_shipping_enabled() { return apply_filters( 'wc_shipping_enabled', get_option( 'woocommerce_ship_to_countries' ) !== 'disabled' ); } } if ( ! function_exists( 'wc_prices_include_tax' ) ) { /** * Are prices inclusive of tax? * * @return bool */ function wc_prices_include_tax() { return wc_tax_enabled() && apply_filters( 'woocommerce_prices_include_tax', get_option( 'woocommerce_prices_include_tax' ) === 'yes' ); } } /** * Simple check for validating a URL, it must start with http:// or https://. * and pass FILTER_VALIDATE_URL validation. * * @param string $url to check. * @return bool */ function wc_is_valid_url( $url ) { // Must start with http:// or https://. if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) { return false; } // Must pass validation. if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return false; } return true; } /** * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'. * * @since 2.4.13 * @return bool */ function wc_site_is_https() { return false !== strstr( get_option( 'home' ), 'https:' ); } /** * Check if the checkout is configured for https. Look at options, WP HTTPS plugin, or the permalink itself. * * @since 2.5.0 * @return bool */ function wc_checkout_is_https() { return wc_site_is_https() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || class_exists( 'WordPressHTTPS' ) || strstr( wc_get_page_permalink( 'checkout' ), 'https:' ); } /** * Checks whether the content passed contains a specific short code. * * @param string $tag Shortcode tag to check. * @return bool */ function wc_post_content_has_shortcode( $tag = '' ) { global $post; return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag ); } /** * Check if reviews are enabled. * * @since 3.6.0 * @return bool */ function wc_reviews_enabled() { return 'yes' === get_option( 'woocommerce_enable_reviews' ); } /** * Check if reviews ratings are enabled. * * @since 3.6.0 * @return bool */ function wc_review_ratings_enabled() { return wc_reviews_enabled() && 'yes' === get_option( 'woocommerce_enable_review_rating' ); } /** * Check if review ratings are required. * * @since 3.6.0 * @return bool */ function wc_review_ratings_required() { return 'yes' === get_option( 'woocommerce_review_rating_required' ); } /** * Check if a CSV file is valid. * * @since 3.6.5 * @param string $file File name. * @param bool $check_path If should check for the path. * @return bool */ function wc_is_file_valid_csv( $file, $check_path = true ) { /** * Filter check for CSV file path. * * @since 3.6.4 * @param bool $check_import_file_path If requires file path check. Defaults to true. */ $check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true ); if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) { return false; } /** * Filter CSV valid file types. * * @since 3.6.5 * @param array $valid_filetypes List of valid file types. */ $valid_filetypes = apply_filters( 'woocommerce_csv_import_valid_filetypes', array( 'csv' => 'text/csv', 'txt' => 'text/plain', ) ); $filetype = wp_check_filetype( $file, $valid_filetypes ); if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { return true; } return false; } includes/class-wc-validation.php 0000644 00000013527 15132754524 0012747 0 ustar 00 <?php /** * General user data validation methods * * @package WooCommerce\Classes * @version 2.4.0 */ defined( 'ABSPATH' ) || exit; /** * Validation class. */ class WC_Validation { /** * Validates an email using WordPress native is_email function. * * @param string $email Email address to validate. * @return bool */ public static function is_email( $email ) { return is_email( $email ); } /** * Validates a phone number using a regular expression. * * @param string $phone Phone number to validate. * @return bool */ public static function is_phone( $phone ) { if ( 0 < strlen( trim( preg_replace( '/[\s\#0-9_\-\+\/\(\)\.]/', '', $phone ) ) ) ) { return false; } return true; } /** * Checks for a valid postcode. * * @param string $postcode Postcode to validate. * @param string $country Country to validate the postcode for. * @return bool */ public static function is_postcode( $postcode, $country ) { if ( strlen( trim( preg_replace( '/[\s\-A-Za-z0-9]/', '', $postcode ) ) ) > 0 ) { return false; } switch ( $country ) { case 'AT': $valid = (bool) preg_match( '/^([0-9]{4})$/', $postcode ); break; case 'BA': $valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode ); break; case 'BE': $valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode ); break; case 'BR': $valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode ); break; case 'CH': $valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode ); break; case 'DE': $valid = (bool) preg_match( '/^([0]{1}[1-9]{1}|[1-9]{1}[0-9]{1})[0-9]{3}$/', $postcode ); break; case 'ES': case 'FR': case 'IT': $valid = (bool) preg_match( '/^([0-9]{5})$/i', $postcode ); break; case 'GB': $valid = self::is_gb_postcode( $postcode ); break; case 'HU': $valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode ); break; case 'IE': $valid = (bool) preg_match( '/([AC-FHKNPRTV-Y]\d{2}|D6W)[0-9AC-FHKNPRTV-Y]{4}/', wc_normalize_postcode( $postcode ) ); break; case 'IN': $valid = (bool) preg_match( '/^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/', $postcode ); break; case 'JP': $valid = (bool) preg_match( '/^([0-9]{3})([-]?)([0-9]{4})$/', $postcode ); break; case 'PT': $valid = (bool) preg_match( '/^([0-9]{4})([-])([0-9]{3})$/', $postcode ); break; case 'PR': case 'US': $valid = (bool) preg_match( '/^([0-9]{5})(-[0-9]{4})?$/i', $postcode ); break; case 'CA': // CA Postal codes cannot contain D,F,I,O,Q,U and cannot start with W or Z. https://en.wikipedia.org/wiki/Postal_codes_in_Canada#Number_of_possible_postal_codes. $valid = (bool) preg_match( '/^([ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ])([\ ])?(\d[ABCEGHJKLMNPRSTVWXYZ]\d)$/i', $postcode ); break; case 'PL': $valid = (bool) preg_match( '/^([0-9]{2})([-])([0-9]{3})$/', $postcode ); break; case 'CZ': case 'SK': $valid = (bool) preg_match( '/^([0-9]{3})(\s?)([0-9]{2})$/', $postcode ); break; case 'NL': $valid = (bool) preg_match( '/^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i', $postcode ); break; case 'SI': $valid = (bool) preg_match( '/^([1-9][0-9]{3})$/', $postcode ); break; case 'LI': $valid = (bool) preg_match( '/^(94[8-9][0-9])$/', $postcode ); break; default: $valid = true; break; } return apply_filters( 'woocommerce_validate_postcode', $valid, $postcode, $country ); } /** * Check if is a GB postcode. * * @param string $to_check A postcode. * @return bool */ public static function is_gb_postcode( $to_check ) { // Permitted letters depend upon their position in the postcode. // https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation. $alpha1 = '[abcdefghijklmnoprstuwyz]'; // Character 1. $alpha2 = '[abcdefghklmnopqrstuvwxy]'; // Character 2. $alpha3 = '[abcdefghjkpstuw]'; // Character 3 == ABCDEFGHJKPSTUW. $alpha4 = '[abehmnprvwxy]'; // Character 4 == ABEHMNPRVWXY. $alpha5 = '[abdefghjlnpqrstuwxyz]'; // Character 5 != CIKMOV. $pcexp = array(); // Expression for postcodes: AN NAA, ANN NAA, AAN NAA, and AANN NAA. $pcexp[0] = '/^(' . $alpha1 . '{1}' . $alpha2 . '{0,1}[0-9]{1,2})([0-9]{1}' . $alpha5 . '{2})$/'; // Expression for postcodes: ANA NAA. $pcexp[1] = '/^(' . $alpha1 . '{1}[0-9]{1}' . $alpha3 . '{1})([0-9]{1}' . $alpha5 . '{2})$/'; // Expression for postcodes: AANA NAA. $pcexp[2] = '/^(' . $alpha1 . '{1}' . $alpha2 . '[0-9]{1}' . $alpha4 . ')([0-9]{1}' . $alpha5 . '{2})$/'; // Exception for the special postcode GIR 0AA. $pcexp[3] = '/^(gir)(0aa)$/'; // Standard BFPO numbers. $pcexp[4] = '/^(bfpo)([0-9]{1,4})$/'; // c/o BFPO numbers. $pcexp[5] = '/^(bfpo)(c\/o[0-9]{1,3})$/'; // Load up the string to check, converting into lowercase and removing spaces. $postcode = strtolower( $to_check ); $postcode = str_replace( ' ', '', $postcode ); // Assume we are not going to find a valid postcode. $valid = false; // Check the string against the six types of postcodes. foreach ( $pcexp as $regexp ) { if ( preg_match( $regexp, $postcode, $matches ) ) { // Remember that we have found that the code is valid and break from loop. $valid = true; break; } } return $valid; } /** * Format the postcode according to the country and length of the postcode. * * @param string $postcode Postcode to format. * @param string $country Country to format the postcode for. * @return string Formatted postcode. */ public static function format_postcode( $postcode, $country ) { return wc_format_postcode( $postcode, $country ); } /** * Format a given phone number. * * @param mixed $tel Phone number to format. * @return string */ public static function format_phone( $tel ) { return wc_format_phone_number( $tel ); } } includes/class-wc-deprecated-action-hooks.php 0000644 00000015602 15132754524 0015305 0 ustar 00 <?php /** * Deprecated action hooks * * @package WooCommerce\Abstracts * @since 3.0.0 * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Handles deprecation notices and triggering of legacy action hooks. */ class WC_Deprecated_Action_Hooks extends WC_Deprecated_Hooks { /** * Array of deprecated hooks we need to handle. Format of 'new' => 'old'. * * @var array */ protected $deprecated_hooks = array( 'woocommerce_new_order_item' => array( 'woocommerce_order_add_shipping', 'woocommerce_order_add_coupon', 'woocommerce_order_add_tax', 'woocommerce_order_add_fee', 'woocommerce_add_shipping_order_item', 'woocommerce_add_order_item_meta', 'woocommerce_add_order_fee_meta', ), 'woocommerce_update_order_item' => array( 'woocommerce_order_edit_product', 'woocommerce_order_update_coupon', 'woocommerce_order_update_shipping', 'woocommerce_order_update_fee', 'woocommerce_order_update_tax', ), 'woocommerce_new_payment_token' => 'woocommerce_payment_token_created', 'woocommerce_new_product_variation' => 'woocommerce_create_product_variation', 'woocommerce_order_details_after_order_table_items' => 'woocommerce_order_items_table', 'woocommerce_settings_advanced_page_options' => array( 'woocommerce_settings_checkout_page_options', 'woocommerce_settings_account_page_options', ), 'woocommerce_settings_advanced_page_options_end' => array( 'woocommerce_settings_checkout_page_options_end', 'woocommerce_settings_account_page_options_end', ), 'woocommerce_settings_advanced_page_options_after' => array( 'woocommerce_settings_checkout_page_options_after', 'woocommerce_settings_account_page_options_after', ), ); /** * Array of versions on each hook has been deprecated. * * @var array */ protected $deprecated_version = array( 'woocommerce_order_add_shipping' => '3.0.0', 'woocommerce_order_add_coupon' => '3.0.0', 'woocommerce_order_add_tax' => '3.0.0', 'woocommerce_order_add_fee' => '3.0.0', 'woocommerce_add_shipping_order_item' => '3.0.0', 'woocommerce_add_order_item_meta' => '3.0.0', 'woocommerce_add_order_fee_meta' => '3.0.0', 'woocommerce_order_edit_product' => '3.0.0', 'woocommerce_order_update_coupon' => '3.0.0', 'woocommerce_order_update_shipping' => '3.0.0', 'woocommerce_order_update_fee' => '3.0.0', 'woocommerce_order_update_tax' => '3.0.0', 'woocommerce_payment_token_created' => '3.0.0', 'woocommerce_create_product_variation' => '3.0.0', 'woocommerce_order_items_table' => '3.0.0', 'woocommerce_settings_checkout_page_options' => '3.4.0', 'woocommerce_settings_account_page_options' => '3.4.0', 'woocommerce_settings_checkout_page_options_end' => '3.4.0', 'woocommerce_settings_account_page_options_end' => '3.4.0', 'woocommerce_settings_checkout_page_options_after' => '3.4.0', 'woocommerce_settings_account_page_options_after' => '3.4.0', ); /** * Hook into the new hook so we can handle deprecated hooks once fired. * * @param string $hook_name Hook name. */ public function hook_in( $hook_name ) { add_action( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); } /** * If the old hook is in-use, trigger it. * * @param string $new_hook New hook name. * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @param mixed $return_value Returned value. * @return mixed */ public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { if ( has_action( $old_hook ) ) { $this->display_notice( $old_hook, $new_hook ); $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); } return $return_value; } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed */ protected function trigger_hook( $old_hook, $new_callback_args ) { switch ( $old_hook ) { case 'woocommerce_order_add_shipping': case 'woocommerce_order_add_fee': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Shipping' ) || is_a( $item, 'WC_Order_Item_Fee' ) ) { do_action( $old_hook, $order_id, $item_id, $item ); } break; case 'woocommerce_order_add_coupon': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { do_action( $old_hook, $order_id, $item_id, $item->get_code(), $item->get_discount(), $item->get_discount_tax() ); } break; case 'woocommerce_order_add_tax': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Tax' ) ) { do_action( $old_hook, $order_id, $item_id, $item->get_rate_id(), $item->get_tax_total(), $item->get_shipping_tax_total() ); } break; case 'woocommerce_add_shipping_order_item': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { do_action( $old_hook, $order_id, $item_id, $item->legacy_package_key ); } break; case 'woocommerce_add_order_item_meta': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $item_id, $item->legacy_values, $item->legacy_cart_item_key ); } break; case 'woocommerce_add_order_fee_meta': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Fee' ) ) { do_action( $old_hook, $order_id, $item_id, $item->legacy_fee, $item->legacy_fee_key ); } break; case 'woocommerce_order_edit_product': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $order_id, $item_id, $item, $item->get_product() ); } break; case 'woocommerce_order_update_coupon': case 'woocommerce_order_update_shipping': case 'woocommerce_order_update_fee': case 'woocommerce_order_update_tax': if ( ! is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $order_id, $item_id, $item ); } break; default: do_action_ref_array( $old_hook, $new_callback_args ); break; } } } includes/wc-coupon-functions.php 0000644 00000005265 15132754524 0013023 0 ustar 00 <?php /** * WooCommerce Coupons Functions * * Functions for coupon specific things. * * @package WooCommerce\Functions * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Get coupon types. * * @return array */ function wc_get_coupon_types() { return (array) apply_filters( 'woocommerce_coupon_discount_types', array( 'percent' => __( 'Percentage discount', 'woocommerce' ), 'fixed_cart' => __( 'Fixed cart discount', 'woocommerce' ), 'fixed_product' => __( 'Fixed product discount', 'woocommerce' ), ) ); } /** * Get a coupon type's name. * * @param string $type Coupon type. * @return string */ function wc_get_coupon_type( $type = '' ) { $types = wc_get_coupon_types(); return isset( $types[ $type ] ) ? $types[ $type ] : ''; } /** * Coupon types that apply to individual products. Controls which validation rules will apply. * * @since 2.5.0 * @return array */ function wc_get_product_coupon_types() { return (array) apply_filters( 'woocommerce_product_coupon_types', array( 'fixed_product', 'percent' ) ); } /** * Coupon types that apply to the cart as a whole. Controls which validation rules will apply. * * @since 2.5.0 * @return array */ function wc_get_cart_coupon_types() { return (array) apply_filters( 'woocommerce_cart_coupon_types', array( 'fixed_cart' ) ); } /** * Check if coupons are enabled. * Filterable. * * @since 2.5.0 * * @return bool */ function wc_coupons_enabled() { return apply_filters( 'woocommerce_coupons_enabled', 'yes' === get_option( 'woocommerce_enable_coupons' ) ); } /** * Get coupon code by ID. * * @since 3.0.0 * @param int $id Coupon ID. * @return string */ function wc_get_coupon_code_by_id( $id ) { $data_store = WC_Data_Store::load( 'coupon' ); return empty( $id ) ? '' : (string) $data_store->get_code_by_id( $id ); } /** * Get coupon ID by code. * * @since 3.0.0 * @param string $code Coupon code. * @param int $exclude Used to exclude an ID from the check if you're checking existence. * @return int */ function wc_get_coupon_id_by_code( $code, $exclude = 0 ) { if ( empty( $code ) ) { return 0; } $data_store = WC_Data_Store::load( 'coupon' ); $ids = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' ); if ( false === $ids ) { $ids = $data_store->get_ids_by_code( $code ); if ( $ids ) { wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $ids, 'coupons' ); } } $ids = array_diff( array_filter( array_map( 'absint', (array) $ids ) ), array( $exclude ) ); return apply_filters( 'woocommerce_get_coupon_id_from_code', absint( current( $ids ) ), $code, $exclude ); } includes/class-wc-product-query.php 0000644 00000004267 15132754524 0013441 0 ustar 00 <?php /** * Class for parameter-based Product querying * * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query * * @package WooCommerce\Classes * @version 3.2.0 * @since 3.2.0 */ defined( 'ABSPATH' ) || exit; /** * Product query class. */ class WC_Product_Query extends WC_Object_Query { /** * Valid query vars for products. * * @return array */ protected function get_default_query_vars() { return array_merge( parent::get_default_query_vars(), array( 'status' => array( 'draft', 'pending', 'private', 'publish' ), 'type' => array_merge( array_keys( wc_get_product_types() ) ), 'limit' => get_option( 'posts_per_page' ), 'include' => array(), 'date_created' => '', 'date_modified' => '', 'featured' => '', 'visibility' => '', 'sku' => '', 'price' => '', 'regular_price' => '', 'sale_price' => '', 'date_on_sale_from' => '', 'date_on_sale_to' => '', 'total_sales' => '', 'tax_status' => '', 'tax_class' => '', 'manage_stock' => '', 'stock_quantity' => '', 'stock_status' => '', 'backorders' => '', 'low_stock_amount' => '', 'sold_individually' => '', 'weight' => '', 'length' => '', 'width' => '', 'height' => '', 'reviews_allowed' => '', 'virtual' => '', 'downloadable' => '', 'category' => array(), 'tag' => array(), 'shipping_class' => array(), 'download_limit' => '', 'download_expiry' => '', 'average_rating' => '', 'review_count' => '', ) ); } /** * Get products matching the current query vars. * * @return array|object of WC_Product objects */ public function get_products() { $args = apply_filters( 'woocommerce_product_object_query_args', $this->get_query_vars() ); $results = WC_Data_Store::load( 'product' )->query( $args ); return apply_filters( 'woocommerce_product_object_query', $results, $args ); } } includes/class-wc-auth.php 0000644 00000027342 15132754524 0011556 0 ustar 00 <?php /** * WooCommerce Auth * * Handles wc-auth endpoint requests. * * @package WooCommerce\RestApi * @since 2.4.0 */ defined( 'ABSPATH' ) || exit; /** * Auth class. */ class WC_Auth { /** * Version. * * @var int */ const VERSION = 1; /** * Setup class. * * @since 2.4.0 */ public function __construct() { // Add query vars. add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); // Register auth endpoint. add_action( 'init', array( __CLASS__, 'add_endpoint' ), 0 ); // Handle auth requests. add_action( 'parse_request', array( $this, 'handle_auth_requests' ), 0 ); } /** * Add query vars. * * @since 2.4.0 * @param array $vars Query variables. * @return string[] */ public function add_query_vars( $vars ) { $vars[] = 'wc-auth-version'; $vars[] = 'wc-auth-route'; return $vars; } /** * Add auth endpoint. * * @since 2.4.0 */ public static function add_endpoint() { add_rewrite_rule( '^wc-auth/v([1]{1})/(.*)?', 'index.php?wc-auth-version=$matches[1]&wc-auth-route=$matches[2]', 'top' ); } /** * Get scope name. * * @since 2.4.0 * @param string $scope Permission scope. * @return string */ protected function get_i18n_scope( $scope ) { $permissions = array( 'read' => __( 'Read', 'woocommerce' ), 'write' => __( 'Write', 'woocommerce' ), 'read_write' => __( 'Read/Write', 'woocommerce' ), ); return $permissions[ $scope ]; } /** * Return a list of permissions a scope allows. * * @since 2.4.0 * @param string $scope Permission scope. * @return array */ protected function get_permissions_in_scope( $scope ) { $permissions = array(); switch ( $scope ) { case 'read': $permissions[] = __( 'View coupons', 'woocommerce' ); $permissions[] = __( 'View customers', 'woocommerce' ); $permissions[] = __( 'View orders and sales reports', 'woocommerce' ); $permissions[] = __( 'View products', 'woocommerce' ); break; case 'write': $permissions[] = __( 'Create webhooks', 'woocommerce' ); $permissions[] = __( 'Create coupons', 'woocommerce' ); $permissions[] = __( 'Create customers', 'woocommerce' ); $permissions[] = __( 'Create orders', 'woocommerce' ); $permissions[] = __( 'Create products', 'woocommerce' ); break; case 'read_write': $permissions[] = __( 'Create webhooks', 'woocommerce' ); $permissions[] = __( 'View and manage coupons', 'woocommerce' ); $permissions[] = __( 'View and manage customers', 'woocommerce' ); $permissions[] = __( 'View and manage orders and sales reports', 'woocommerce' ); $permissions[] = __( 'View and manage products', 'woocommerce' ); break; } return apply_filters( 'woocommerce_api_permissions_in_scope', $permissions, $scope ); } /** * Build auth urls. * * @since 2.4.0 * @param array $data Data to build URL. * @param string $endpoint Endpoint. * @return string */ protected function build_url( $data, $endpoint ) { $url = wc_get_endpoint_url( 'wc-auth/v' . self::VERSION, $endpoint, home_url( '/' ) ); return add_query_arg( array( 'app_name' => wc_clean( $data['app_name'] ), 'user_id' => wc_clean( $data['user_id'] ), 'return_url' => rawurlencode( $this->get_formatted_url( $data['return_url'] ) ), 'callback_url' => rawurlencode( $this->get_formatted_url( $data['callback_url'] ) ), 'scope' => wc_clean( $data['scope'] ), ), $url ); } /** * Decode and format a URL. * * @param string $url URL. * @return string */ protected function get_formatted_url( $url ) { $url = urldecode( $url ); if ( ! strstr( $url, '://' ) ) { $url = 'https://' . $url; } return $url; } /** * Make validation. * * @since 2.4.0 * @throws Exception When validate fails. */ protected function make_validation() { $data = array(); $params = array( 'app_name', 'user_id', 'return_url', 'callback_url', 'scope', ); foreach ( $params as $param ) { if ( empty( $_REQUEST[ $param ] ) ) { // WPCS: input var ok, CSRF ok. /* translators: %s: parameter */ throw new Exception( sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param ) ); } $data[ $param ] = wp_unslash( $_REQUEST[ $param ] ); // WPCS: input var ok, CSRF ok, sanitization ok. } if ( ! in_array( $data['scope'], array( 'read', 'write', 'read_write' ), true ) ) { /* translators: %s: scope */ throw new Exception( sprintf( __( 'Invalid scope %s', 'woocommerce' ), wc_clean( $data['scope'] ) ) ); } foreach ( array( 'return_url', 'callback_url' ) as $param ) { $param = $this->get_formatted_url( $data[ $param ] ); if ( false === filter_var( $param, FILTER_VALIDATE_URL ) ) { /* translators: %s: url */ throw new Exception( sprintf( __( 'The %s is not a valid URL', 'woocommerce' ), $param ) ); } } $callback_url = $this->get_formatted_url( $data['callback_url'] ); if ( 0 !== stripos( $callback_url, 'https://' ) ) { throw new Exception( __( 'The callback_url needs to be over SSL', 'woocommerce' ) ); } } /** * Create keys. * * @since 2.4.0 * * @param string $app_name App name. * @param string $app_user_id User ID. * @param string $scope Scope. * * @return array */ protected function create_keys( $app_name, $app_user_id, $scope ) { global $wpdb; $description = sprintf( /* translators: 1: app name 2: scope 3: date 4: time */ __( '%1$s - API %2$s (created on %3$s at %4$s).', 'woocommerce' ), wc_clean( $app_name ), $this->get_i18n_scope( $scope ), date_i18n( wc_date_format() ), date_i18n( wc_time_format() ) ); $user = wp_get_current_user(); // Created API keys. $permissions = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read'; $consumer_key = 'ck_' . wc_rand_hash(); $consumer_secret = 'cs_' . wc_rand_hash(); $wpdb->insert( $wpdb->prefix . 'woocommerce_api_keys', array( 'user_id' => $user->ID, 'description' => $description, 'permissions' => $permissions, 'consumer_key' => wc_api_hash( $consumer_key ), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr( $consumer_key, -7 ), ), array( '%d', '%s', '%s', '%s', '%s', '%s', ) ); return array( 'key_id' => $wpdb->insert_id, 'user_id' => $app_user_id, 'consumer_key' => $consumer_key, 'consumer_secret' => $consumer_secret, 'key_permissions' => $permissions, ); } /** * Post consumer data. * * @since 2.4.0 * * @throws Exception When validation fails. * @param array $consumer_data Consumer data. * @param string $url URL. * @return bool */ protected function post_consumer_data( $consumer_data, $url ) { $params = array( 'body' => wp_json_encode( $consumer_data ), 'timeout' => 60, 'headers' => array( 'Content-Type' => 'application/json;charset=' . get_bloginfo( 'charset' ), ), ); $response = wp_safe_remote_post( esc_url_raw( $url ), $params ); if ( is_wp_error( $response ) ) { throw new Exception( $response->get_error_message() ); } elseif ( 200 !== intval( $response['response']['code'] ) ) { throw new Exception( __( 'An error occurred in the request and at the time were unable to send the consumer data', 'woocommerce' ) ); } return true; } /** * Handle auth requests. * * @since 2.4.0 * @throws Exception When auth_endpoint validation fails. */ public function handle_auth_requests() { global $wp; if ( ! empty( $_GET['wc-auth-version'] ) ) { // WPCS: input var ok, CSRF ok. $wp->query_vars['wc-auth-version'] = wc_clean( wp_unslash( $_GET['wc-auth-version'] ) ); // WPCS: input var ok, CSRF ok. } if ( ! empty( $_GET['wc-auth-route'] ) ) { // WPCS: input var ok, CSRF ok. $wp->query_vars['wc-auth-route'] = wc_clean( wp_unslash( $_GET['wc-auth-route'] ) ); // WPCS: input var ok, CSRF ok. } // wc-auth endpoint requests. if ( ! empty( $wp->query_vars['wc-auth-version'] ) && ! empty( $wp->query_vars['wc-auth-route'] ) ) { $this->auth_endpoint( $wp->query_vars['wc-auth-route'] ); } } /** * Auth endpoint. * * @since 2.4.0 * @throws Exception When validation fails. * @param string $route Route. */ protected function auth_endpoint( $route ) { ob_start(); $consumer_data = array(); try { $route = strtolower( wc_clean( $route ) ); $this->make_validation(); $data = wp_unslash( $_REQUEST ); // WPCS: input var ok, CSRF ok. // Login endpoint. if ( 'login' === $route && ! is_user_logged_in() ) { wc_get_template( 'auth/form-login.php', array( 'app_name' => wc_clean( $data['app_name'] ), 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $data['user_id'] ), ), $this->get_formatted_url( $data['return_url'] ) ), 'redirect_url' => $this->build_url( $data, 'authorize' ), ) ); exit; } elseif ( 'login' === $route && is_user_logged_in() ) { // Redirect with user is logged in. wp_redirect( esc_url_raw( $this->build_url( $data, 'authorize' ) ) ); exit; } elseif ( 'authorize' === $route && ! is_user_logged_in() ) { // Redirect with user is not logged in and trying to access the authorize endpoint. wp_redirect( esc_url_raw( $this->build_url( $data, 'login' ) ) ); exit; } elseif ( 'authorize' === $route && current_user_can( 'manage_woocommerce' ) ) { // Authorize endpoint. wc_get_template( 'auth/form-grant-access.php', array( 'app_name' => wc_clean( $data['app_name'] ), 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $data['user_id'] ), ), $this->get_formatted_url( $data['return_url'] ) ), 'scope' => $this->get_i18n_scope( wc_clean( $data['scope'] ) ), 'permissions' => $this->get_permissions_in_scope( wc_clean( $data['scope'] ) ), 'granted_url' => wp_nonce_url( $this->build_url( $data, 'access_granted' ), 'wc_auth_grant_access', 'wc_auth_nonce' ), 'logout_url' => wp_logout_url( $this->build_url( $data, 'login' ) ), 'user' => wp_get_current_user(), ) ); exit; } elseif ( 'access_granted' === $route && current_user_can( 'manage_woocommerce' ) ) { // Granted access endpoint. if ( ! isset( $_GET['wc_auth_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['wc_auth_nonce'] ) ), 'wc_auth_grant_access' ) ) { // WPCS: input var ok. throw new Exception( __( 'Invalid nonce verification', 'woocommerce' ) ); } $consumer_data = $this->create_keys( $data['app_name'], $data['user_id'], $data['scope'] ); $response = $this->post_consumer_data( $consumer_data, $this->get_formatted_url( $data['callback_url'] ) ); if ( $response ) { wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $data['user_id'] ), ), $this->get_formatted_url( $data['return_url'] ) ) ) ); exit; } } else { throw new Exception( __( 'You do not have permission to access this page', 'woocommerce' ) ); } } catch ( Exception $e ) { $this->maybe_delete_key( $consumer_data ); /* translators: %s: error message */ wp_die( sprintf( esc_html__( 'Error: %s.', 'woocommerce' ), esc_html( $e->getMessage() ) ), esc_html__( 'Access denied', 'woocommerce' ), array( 'response' => 401 ) ); } } /** * Maybe delete key. * * @since 2.4.0 * * @param array $key Key. */ private function maybe_delete_key( $key ) { global $wpdb; if ( isset( $key['key_id'] ) ) { $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key['key_id'] ), array( '%d' ) ); } } } new WC_Auth(); includes/class-wc-form-handler.php 0000644 00000127752 15132754524 0013201 0 ustar 00 <?php /** * Handle frontend forms. * * @package WooCommerce\Classes\ */ defined( 'ABSPATH' ) || exit; /** * WC_Form_Handler class. */ class WC_Form_Handler { /** * Hook in methods. */ public static function init() { add_action( 'template_redirect', array( __CLASS__, 'redirect_reset_password_link' ) ); add_action( 'template_redirect', array( __CLASS__, 'save_address' ) ); add_action( 'template_redirect', array( __CLASS__, 'save_account_details' ) ); add_action( 'wp_loaded', array( __CLASS__, 'checkout_action' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'process_login' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'process_registration' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'process_lost_password' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'process_reset_password' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'cancel_order' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'update_cart_action' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'add_to_cart_action' ), 20 ); // May need $wp global to access query vars. add_action( 'wp', array( __CLASS__, 'pay_action' ), 20 ); add_action( 'wp', array( __CLASS__, 'add_payment_method_action' ), 20 ); add_action( 'wp', array( __CLASS__, 'delete_payment_method_action' ), 20 ); add_action( 'wp', array( __CLASS__, 'set_default_payment_method_action' ), 20 ); } /** * Remove key and user ID (or user login, as a fallback) from query string, set cookie, and redirect to account page to show the form. */ public static function redirect_reset_password_link() { if ( is_account_page() && isset( $_GET['key'] ) && ( isset( $_GET['id'] ) || isset( $_GET['login'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // If available, get $user_id from query string parameter for fallback purposes. if ( isset( $_GET['login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $user = get_user_by( 'login', sanitize_user( wp_unslash( $_GET['login'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $user_id = $user ? $user->ID : 0; } else { $user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } // If the reset token is not for the current user, ignore the reset request (don't redirect). $logged_in_user_id = get_current_user_id(); if ( $logged_in_user_id && $logged_in_user_id !== $user_id ) { wc_add_notice( __( 'This password reset key is for a different user account. Please log out and try again.', 'woocommerce' ), 'error' ); return; } $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; $value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore WC_Shortcode_My_Account::set_reset_password_cookie( $value ); wp_safe_redirect( add_query_arg( array( 'show-reset-form' => 'true', 'action' => $action, ), wc_lostpassword_url() ) ); exit; } } /** * Save and and update a billing or shipping address if the * form was submitted through the user account page. */ public static function save_address() { global $wp; $nonce_value = wc_get_var( $_REQUEST['woocommerce-edit-address-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-edit_address' ) ) { return; } if ( empty( $_POST['action'] ) || 'edit_address' !== $_POST['action'] ) { return; } wc_nocache_headers(); $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return; } $customer = new WC_Customer( $user_id ); if ( ! $customer ) { return; } $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; if ( ! isset( $_POST[ $load_address . '_country' ] ) ) { return; } $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' ); foreach ( $address as $key => $field ) { if ( ! isset( $field['type'] ) ) { $field['type'] = 'text'; } // Get Value. if ( 'checkbox' === $field['type'] ) { $value = (int) isset( $_POST[ $key ] ); } else { $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; } // Hook to allow modification of value. $value = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $value ); // Validation: Required fields. if ( ! empty( $field['required'] ) && empty( $value ) ) { /* translators: %s: Field name. */ wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error', array( 'id' => $key ) ); } if ( ! empty( $value ) ) { // Validation and formatting rules. if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { foreach ( $field['validate'] as $rule ) { switch ( $rule ) { case 'postcode': $country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ); $value = wc_format_postcode( $value, $country ); if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) { switch ( $country ) { case 'IE': $postcode_validation_notice = __( 'Please enter a valid Eircode.', 'woocommerce' ); break; default: $postcode_validation_notice = __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ); } wc_add_notice( $postcode_validation_notice, 'error' ); } break; case 'phone': if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) { /* translators: %s: Phone number. */ wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ), 'error' ); } break; case 'email': $value = strtolower( $value ); if ( ! is_email( $value ) ) { /* translators: %s: Email address. */ wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ), 'error' ); } break; } } } } try { // Set prop in customer object. if ( is_callable( array( $customer, "set_$key" ) ) ) { $customer->{"set_$key"}( $value ); } else { $customer->update_meta_data( $key, $value ); } } catch ( WC_Data_Exception $e ) { // Set notices. Ignore invalid billing email, since is already validated. if ( 'customer_invalid_billing_email' !== $e->getErrorCode() ) { wc_add_notice( $e->getMessage(), 'error' ); } } } /** * Hook: woocommerce_after_save_address_validation. * * Allow developers to add custom validation logic and throw an error to prevent save. * * @param int $user_id User ID being saved. * @param string $load_address Type of address e.g. billing or shipping. * @param array $address The address fields. * @param WC_Customer $customer The customer object being saved. @since 3.6.0 */ do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer ); if ( 0 < wc_notice_count( 'error' ) ) { return; } $customer->save(); wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) ); do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); exit; } /** * Save the password/account details and redirect back to the my account page. */ public static function save_account_details() { $nonce_value = wc_get_var( $_REQUEST['save-account-details-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'save_account_details' ) ) { return; } if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] ) { return; } wc_nocache_headers(); $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return; } $account_first_name = ! empty( $_POST['account_first_name'] ) ? wc_clean( wp_unslash( $_POST['account_first_name'] ) ) : ''; $account_last_name = ! empty( $_POST['account_last_name'] ) ? wc_clean( wp_unslash( $_POST['account_last_name'] ) ) : ''; $account_display_name = ! empty( $_POST['account_display_name'] ) ? wc_clean( wp_unslash( $_POST['account_display_name'] ) ) : ''; $account_email = ! empty( $_POST['account_email'] ) ? wc_clean( wp_unslash( $_POST['account_email'] ) ) : ''; $pass_cur = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $pass1 = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $pass2 = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $save_pass = true; // Current user data. $current_user = get_user_by( 'id', $user_id ); $current_first_name = $current_user->first_name; $current_last_name = $current_user->last_name; $current_email = $current_user->user_email; // New user data. $user = new stdClass(); $user->ID = $user_id; $user->first_name = $account_first_name; $user->last_name = $account_last_name; $user->display_name = $account_display_name; // Prevent display name to be changed to email. if ( is_email( $account_display_name ) ) { wc_add_notice( __( 'Display name cannot be changed to email address due to privacy concern.', 'woocommerce' ), 'error' ); } // Handle required fields. $required_fields = apply_filters( 'woocommerce_save_account_details_required_fields', array( 'account_first_name' => __( 'First name', 'woocommerce' ), 'account_last_name' => __( 'Last name', 'woocommerce' ), 'account_display_name' => __( 'Display name', 'woocommerce' ), 'account_email' => __( 'Email address', 'woocommerce' ), ) ); foreach ( $required_fields as $field_key => $field_name ) { if ( empty( $_POST[ $field_key ] ) ) { /* translators: %s: Field name. */ wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_name ) . '</strong>' ), 'error', array( 'id' => $field_key ) ); } } if ( $account_email ) { $account_email = sanitize_email( $account_email ); if ( ! is_email( $account_email ) ) { wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); } $user->user_email = $account_email; } if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) { wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' ); $save_pass = false; } elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) { wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' ); $save_pass = false; } elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) { wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' ); $save_pass = false; } elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) { wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' ); $save_pass = false; } elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); $save_pass = false; } if ( $pass1 && $save_pass ) { $user->user_pass = $pass1; } // Allow plugins to return their own errors. $errors = new WP_Error(); do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) ); if ( $errors->get_error_messages() ) { foreach ( $errors->get_error_messages() as $error ) { wc_add_notice( $error, 'error' ); } } if ( wc_notice_count( 'error' ) === 0 ) { wp_update_user( $user ); // Update customer object to keep data in sync. $customer = new WC_Customer( $user->ID ); if ( $customer ) { // Keep billing data in sync if data changed. if ( is_email( $user->user_email ) && $current_email !== $user->user_email ) { $customer->set_billing_email( $user->user_email ); } if ( $current_first_name !== $user->first_name ) { $customer->set_billing_first_name( $user->first_name ); } if ( $current_last_name !== $user->last_name ) { $customer->set_billing_last_name( $user->last_name ); } $customer->save(); } wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) ); do_action( 'woocommerce_save_account_details', $user->ID ); wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } /** * Process the checkout form. */ public static function checkout_action() { if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing wc_nocache_headers(); if ( WC()->cart->is_empty() ) { wp_safe_redirect( wc_get_cart_url() ); exit; } wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); WC()->checkout()->process_checkout(); } } /** * Process the pay form. * * @throws Exception On payment error. */ public static function pay_action() { global $wp; if ( isset( $_POST['woocommerce_pay'], $_GET['key'] ) ) { wc_nocache_headers(); $nonce_value = wc_get_var( $_REQUEST['woocommerce-pay-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-pay' ) ) { return; } ob_start(); // Pay for existing order. $order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $order_id = absint( $wp->query_vars['order-pay'] ); $order = wc_get_order( $order_id ); if ( $order_id === $order->get_id() && hash_equals( $order->get_order_key(), $order_key ) && $order->needs_payment() ) { do_action( 'woocommerce_before_pay_action', $order ); WC()->customer->set_props( array( 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, 'billing_city' => $order->get_billing_city() ? $order->get_billing_city() : null, ) ); WC()->customer->save(); if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) { wc_add_notice( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), 'error' ); return; } // Update payment method. if ( $order->needs_payment() ) { try { $payment_method_id = isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : false; if ( ! $payment_method_id ) { throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); } $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); $payment_method = isset( $available_gateways[ $payment_method_id ] ) ? $available_gateways[ $payment_method_id ] : false; if ( ! $payment_method ) { throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); } $order->set_payment_method( $payment_method ); $order->save(); $payment_method->validate_fields(); if ( 0 === wc_notice_count( 'error' ) ) { $result = $payment_method->process_payment( $order_id ); // Redirect to success/confirmation/payment page. if ( isset( $result['result'] ) && 'success' === $result['result'] ) { $result['order_id'] = $order_id; $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit; } } } catch ( Exception $e ) { wc_add_notice( $e->getMessage(), 'error' ); } } else { // No payment was required for order. $order->payment_complete(); wp_safe_redirect( $order->get_checkout_order_received_url() ); exit; } do_action( 'woocommerce_after_pay_action', $order ); } } } /** * Process the add payment method form. */ public static function add_payment_method_action() { if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'] ) ) { wc_nocache_headers(); $nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) { return; } if ( ! apply_filters( 'woocommerce_add_payment_method_form_is_valid', true ) ) { return; } // Test rate limit. $current_user_id = get_current_user_id(); $rate_limit_id = 'add_payment_method_' . $current_user_id; $delay = (int) apply_filters( 'woocommerce_payment_gateway_add_payment_method_delay', 20 ); if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) { wc_add_notice( sprintf( /* translators: %d number of seconds */ _n( 'You cannot add a new payment method so soon after the previous one. Please wait for %d second.', 'You cannot add a new payment method so soon after the previous one. Please wait for %d seconds.', $delay, 'woocommerce' ), $delay ), 'error' ); return; } WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ); ob_start(); $payment_method_id = wc_clean( wp_unslash( $_POST['payment_method'] ) ); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( isset( $available_gateways[ $payment_method_id ] ) ) { $gateway = $available_gateways[ $payment_method_id ]; if ( ! $gateway->supports( 'add_payment_method' ) && ! $gateway->supports( 'tokenization' ) ) { wc_add_notice( __( 'Invalid payment gateway.', 'woocommerce' ), 'error' ); return; } $gateway->validate_fields(); if ( wc_notice_count( 'error' ) > 0 ) { return; } $result = $gateway->add_payment_method(); if ( 'success' === $result['result'] ) { wc_add_notice( __( 'Payment method successfully added.', 'woocommerce' ) ); } if ( 'failure' === $result['result'] ) { wc_add_notice( __( 'Unable to add payment method to your account.', 'woocommerce' ), 'error' ); } if ( ! empty( $result['redirect'] ) ) { wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit(); } } } } /** * Process the delete payment method form. */ public static function delete_payment_method_action() { global $wp; if ( isset( $wp->query_vars['delete-payment-method'] ) ) { wc_nocache_headers(); $token_id = absint( $wp->query_vars['delete-payment-method'] ); $token = WC_Payment_Tokens::get( $token_id ); if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'delete-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); } else { WC_Payment_Tokens::delete( $token_id ); wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); } wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); exit(); } } /** * Process the delete payment method form. */ public static function set_default_payment_method_action() { global $wp; if ( isset( $wp->query_vars['set-default-payment-method'] ) ) { wc_nocache_headers(); $token_id = absint( $wp->query_vars['set-default-payment-method'] ); $token = WC_Payment_Tokens::get( $token_id ); if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'set-default-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); } else { WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) ); wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); } wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); exit(); } } /** * Remove from cart/update. */ public static function update_cart_action() { if ( ! ( isset( $_REQUEST['apply_coupon'] ) || isset( $_REQUEST['remove_coupon'] ) || isset( $_REQUEST['remove_item'] ) || isset( $_REQUEST['undo_item'] ) || isset( $_REQUEST['update_cart'] ) || isset( $_REQUEST['proceed'] ) ) ) { return; } wc_nocache_headers(); $nonce_value = wc_get_var( $_REQUEST['woocommerce-cart-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) { WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( isset( $_GET['remove_coupon'] ) ) { WC()->cart->remove_coupon( wc_format_coupon_code( urldecode( wp_unslash( $_GET['remove_coupon'] ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( ! empty( $_GET['remove_item'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { $cart_item_key = sanitize_text_field( wp_unslash( $_GET['remove_item'] ) ); $cart_item = WC()->cart->get_cart_item( $cart_item_key ); if ( $cart_item ) { WC()->cart->remove_cart_item( $cart_item_key ); $product = wc_get_product( $cart_item['product_id'] ); /* translators: %s: Item name. */ $item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item ); // Don't show undo link if removed item is out of stock. if ( $product && $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) { /* Translators: %s Product title. */ $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); $removed_notice .= ' <a href="' . esc_url( wc_get_cart_undo_url( $cart_item_key ) ) . '" class="restore-item">' . __( 'Undo?', 'woocommerce' ) . '</a>'; } else { /* Translators: %s Product title. */ $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); } wc_add_notice( $removed_notice, apply_filters( 'woocommerce_cart_item_removed_notice_type', 'success' ) ); } $referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url(); wp_safe_redirect( $referer ); exit; } elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { // Undo Cart Item. $cart_item_key = sanitize_text_field( wp_unslash( $_GET['undo_item'] ) ); WC()->cart->restore_cart_item( $cart_item_key ); $referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url(); wp_safe_redirect( $referer ); exit; } // Update Cart - checks apply_coupon too because they are in the same form. if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { $cart_updated = false; $cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { $_product = $values['data']; // Skip product if no updated quantity was posted. if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { continue; } // Sanitize. $quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( '/[^0-9\.]/', '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key ); if ( '' === $quantity || $quantity === $values['quantity'] ) { continue; } // Update cart validation. $passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity ); // is_sold_individually. if ( $_product->is_sold_individually() && $quantity > 1 ) { /* Translators: %s Product title. */ wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' ); $passed_validation = false; } if ( $passed_validation ) { WC()->cart->set_quantity( $cart_item_key, $quantity, false ); $cart_updated = true; } } } // Trigger action - let 3rd parties update the cart if they need to and update the $cart_updated variable. $cart_updated = apply_filters( 'woocommerce_update_cart_action_cart_updated', $cart_updated ); if ( $cart_updated ) { WC()->cart->calculate_totals(); } if ( ! empty( $_POST['proceed'] ) ) { wp_safe_redirect( wc_get_checkout_url() ); exit; } elseif ( $cart_updated ) { wc_add_notice( __( 'Cart updated.', 'woocommerce' ), apply_filters( 'woocommerce_cart_updated_notice_type', 'success' ) ); $referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) ); wp_safe_redirect( $referer ); exit; } } } /** * Place a previous order again. * * @deprecated 3.5.0 Logic moved to cart session handling. */ public static function order_again() { wc_deprecated_function( 'WC_Form_Handler::order_again', '3.5', 'This method should not be called manually.' ); } /** * Cancel a pending order. */ public static function cancel_order() { if ( isset( $_GET['cancel_order'] ) && isset( $_GET['order'] ) && isset( $_GET['order_id'] ) && ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ) { wc_nocache_headers(); $order_key = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $order_id = absint( $_GET['order_id'] ); $order = wc_get_order( $order_id ); $user_can_cancel = current_user_can( 'cancel_order', $order_id ); $order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ) ); $redirect = isset( $_GET['redirect'] ) ? wp_unslash( $_GET['redirect'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { // Cancel the order + restore stock. WC()->session->set( 'order_awaiting_payment', false ); $order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) ); wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) ); do_action( 'woocommerce_cancelled_order', $order->get_id() ); } elseif ( $user_can_cancel && ! $order_can_cancel ) { wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' ); } else { wc_add_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); } if ( $redirect ) { wp_safe_redirect( $redirect ); exit; } } } /** * Add to cart action. * * Checks for a valid request, does validation (via hooks) and then redirects if valid. * * @param bool $url (default: false) URL to redirect to. */ public static function add_to_cart_action( $url = false ) { if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return; } wc_nocache_headers(); $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( wp_unslash( $_REQUEST['add-to-cart'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $was_added_to_cart = false; $adding_to_cart = wc_get_product( $product_id ); if ( ! $adding_to_cart ) { return; } $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart ); if ( 'variable' === $add_to_cart_handler || 'variation' === $add_to_cart_handler ) { $was_added_to_cart = self::add_to_cart_handler_variable( $product_id ); } elseif ( 'grouped' === $add_to_cart_handler ) { $was_added_to_cart = self::add_to_cart_handler_grouped( $product_id ); } elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) { do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); // Custom handler. } else { $was_added_to_cart = self::add_to_cart_handler_simple( $product_id ); } // If we added the product to the cart we can now optionally do a redirect. if ( $was_added_to_cart && 0 === wc_notice_count( 'error' ) ) { $url = apply_filters( 'woocommerce_add_to_cart_redirect', $url, $adding_to_cart ); if ( $url ) { wp_safe_redirect( $url ); exit; } elseif ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { wp_safe_redirect( wc_get_cart_url() ); exit; } } } /** * Handle adding simple products to the cart. * * @since 2.4.6 Split from add_to_cart_action. * @param int $product_id Product ID to add to the cart. * @return bool success or not */ private static function add_to_cart_handler_simple( $product_id ) { $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) ) { wc_add_to_cart_message( array( $product_id => $quantity ), true ); return true; } return false; } /** * Handle adding grouped products to the cart. * * @since 2.4.6 Split from add_to_cart_action. * @param int $product_id Product ID to add to the cart. * @return bool success or not */ private static function add_to_cart_handler_grouped( $product_id ) { $was_added_to_cart = false; $added_to_cart = array(); $items = isset( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ? wp_unslash( $_REQUEST['quantity'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $items ) ) { $quantity_set = false; foreach ( $items as $item => $quantity ) { $quantity = wc_stock_amount( $quantity ); if ( $quantity <= 0 ) { continue; } $quantity_set = true; // Add to cart validation. $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); // Suppress total recalculation until finished. remove_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); if ( $passed_validation && false !== WC()->cart->add_to_cart( $item, $quantity ) ) { $was_added_to_cart = true; $added_to_cart[ $item ] = $quantity; } add_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); } if ( ! $was_added_to_cart && ! $quantity_set ) { wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); } elseif ( $was_added_to_cart ) { wc_add_to_cart_message( $added_to_cart ); WC()->cart->calculate_totals(); return true; } } elseif ( $product_id ) { /* Link on product archives */ wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); } return false; } /** * Handle adding variable products to the cart. * * @since 2.4.6 Split from add_to_cart_action. * @throws Exception If add to cart fails. * @param int $product_id Product ID to add to the cart. * @return bool success or not */ private static function add_to_cart_handler_variable( $product_id ) { $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $variations = array(); $product = wc_get_product( $product_id ); foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'attribute_' !== substr( $key, 0, 10 ) ) { continue; } $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value ); } $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); if ( ! $passed_validation ) { return false; } // Prevent parent variable product from being added to cart. if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) { /* translators: 1: product link, 2: product name */ wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' ); return false; } if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) { wc_add_to_cart_message( array( $product_id => $quantity ), true ); return true; } return false; } /** * Process the login form. * * @throws Exception On login error. */ public static function process_login() { // The global form-login.php template used `_wpnonce` in template versions < 3.3.0. $nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) { try { $creds = array( 'user_login' => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash 'remember' => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ); $validation_error = new WP_Error(); $validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] ); if ( $validation_error->get_error_code() ) { throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $validation_error->get_error_message() ); } if ( empty( $creds['user_login'] ) ) { throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . __( 'Username is required.', 'woocommerce' ) ); } // On multisite, ensure user exists on current site, if not add them before allowing login. if ( is_multisite() ) { $user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] ); if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' ); } } // Perform the login. $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() ); if ( is_wp_error( $user ) ) { throw new Exception( $user->get_error_message() ); } else { if ( ! empty( $_POST['redirect'] ) ) { $redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( wc_get_raw_referer() ) { $redirect = wc_get_raw_referer(); } else { $redirect = wc_get_page_permalink( 'myaccount' ); } wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore exit; } } catch ( Exception $e ) { wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' ); do_action( 'woocommerce_login_failed' ); } } } /** * Handle lost password form. */ public static function process_lost_password() { if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { $nonce_value = wc_get_var( $_REQUEST['woocommerce-lost-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'lost_password' ) ) { return; } $success = WC_Shortcode_My_Account::retrieve_password(); // If successful, redirect to my account with query arg set. if ( $success ) { wp_safe_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) ); exit; } } } /** * Handle reset password form. */ public static function process_reset_password() { $nonce_value = wc_get_var( $_REQUEST['woocommerce-reset-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'reset_password' ) ) { return; } $posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login' ); foreach ( $posted_fields as $field ) { if ( ! isset( $_POST[ $field ] ) ) { return; } if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) { // Don't unslash password fields // @see https://github.com/woocommerce/woocommerce/issues/23922. $posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } else { $posted_fields[ $field ] = wp_unslash( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } } $user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] ); if ( $user instanceof WP_User ) { if ( empty( $posted_fields['password_1'] ) ) { wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); } if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) { wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); } $errors = new WP_Error(); do_action( 'validate_password_reset', $errors, $user ); wc_add_wp_error_notices( $errors ); if ( 0 === wc_notice_count( 'error' ) ) { WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] ); do_action( 'woocommerce_customer_reset_password', $user ); wp_safe_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) ); exit; } } } /** * Process the registration form. * * @throws Exception On registration error. */ public static function process_registration() { $nonce_value = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? wp_unslash( $_POST['woocommerce-register-nonce'] ) : $nonce_value; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( isset( $_POST['register'], $_POST['email'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) { $username = 'no' === get_option( 'woocommerce_registration_generate_username' ) && isset( $_POST['username'] ) ? wp_unslash( $_POST['username'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $password = 'no' === get_option( 'woocommerce_registration_generate_password' ) && isset( $_POST['password'] ) ? $_POST['password'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized try { $validation_error = new WP_Error(); $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email ); $validation_errors = $validation_error->get_error_messages(); if ( 1 === count( $validation_errors ) ) { throw new Exception( $validation_error->get_error_message() ); } elseif ( $validation_errors ) { foreach ( $validation_errors as $message ) { wc_add_notice( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $message, 'error' ); } throw new Exception(); } $new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password ); if ( is_wp_error( $new_customer ) ) { throw new Exception( $new_customer->get_error_message() ); } if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ) { wc_add_notice( __( 'Your account was created successfully and a password has been sent to your email address.', 'woocommerce' ) ); } else { wc_add_notice( __( 'Your account was created successfully. Your login details have been sent to your email address.', 'woocommerce' ) ); } // Only redirect after a forced login - otherwise output a success notice. if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) { wc_set_customer_auth_cookie( $new_customer ); if ( ! empty( $_POST['redirect'] ) ) { $redirect = wp_sanitize_redirect( wp_unslash( $_POST['redirect'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( wc_get_raw_referer() ) { $redirect = wc_get_raw_referer(); } else { $redirect = wc_get_page_permalink( 'myaccount' ); } wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ), wc_get_page_permalink( 'myaccount' ) ) ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit; } } catch ( Exception $e ) { if ( $e->getMessage() ) { wc_add_notice( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $e->getMessage(), 'error' ); } } } } } WC_Form_Handler::init(); includes/class-wc-comments.php 0000644 00000035771 15132754524 0012447 0 ustar 00 <?php /** * Comments * * Handle comments (reviews and order notes). * * @package WooCommerce\Classes\Products * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Comments class. */ class WC_Comments { /** * Hook in methods. */ public static function init() { // Rating posts. add_filter( 'comments_open', array( __CLASS__, 'comments_open' ), 10, 2 ); add_filter( 'preprocess_comment', array( __CLASS__, 'check_comment_rating' ), 0 ); add_action( 'comment_post', array( __CLASS__, 'add_comment_rating' ), 1 ); add_action( 'comment_moderation_recipients', array( __CLASS__, 'comment_moderation_recipients' ), 10, 2 ); // Clear transients. add_action( 'wp_update_comment_count', array( __CLASS__, 'clear_transients' ) ); // Secure order notes. add_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 ); add_filter( 'comment_feed_where', array( __CLASS__, 'exclude_order_comments_from_feed_where' ) ); // Secure webhook comments. add_filter( 'comments_clauses', array( __CLASS__, 'exclude_webhook_comments' ), 10, 1 ); add_filter( 'comment_feed_where', array( __CLASS__, 'exclude_webhook_comments_from_feed_where' ) ); // Count comments. add_filter( 'wp_count_comments', array( __CLASS__, 'wp_count_comments' ), 10, 2 ); // Delete comments count cache whenever there is a new comment or a comment status changes. add_action( 'wp_insert_comment', array( __CLASS__, 'delete_comments_count_cache' ) ); add_action( 'wp_set_comment_status', array( __CLASS__, 'delete_comments_count_cache' ) ); // Support avatars for `review` comment type. add_filter( 'get_avatar_comment_types', array( __CLASS__, 'add_avatar_for_review_comment_type' ) ); // Review of verified purchase. add_action( 'comment_post', array( __CLASS__, 'add_comment_purchase_verification' ) ); // Set comment type. add_action( 'preprocess_comment', array( __CLASS__, 'update_comment_type' ), 1 ); // Validate product reviews if requires verified owners. add_action( 'pre_comment_on_post', array( __CLASS__, 'validate_product_review_verified_owners' ) ); } /** * See if comments are open. * * @since 3.1.0 * @param bool $open Whether the current post is open for comments. * @param int $post_id Post ID. * @return bool */ public static function comments_open( $open, $post_id ) { if ( 'product' === get_post_type( $post_id ) && ! post_type_supports( 'product', 'comments' ) ) { $open = false; } return $open; } /** * Exclude order comments from queries and RSS. * * This code should exclude shop_order comments from queries. Some queries (like the recent comments widget on the dashboard) are hardcoded. * and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and. * shop managers can view orders anyway. * * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 ); * * @param array $clauses A compacted array of comment query clauses. * @return array */ public static function exclude_order_comments( $clauses ) { $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'order_note' "; return $clauses; } /** * Exclude order comments from feed. * * @deprecated 3.1 * @param mixed $join Deprecated. */ public static function exclude_order_comments_from_feed_join( $join ) { wc_deprecated_function( 'WC_Comments::exclude_order_comments_from_feed_join', '3.1' ); } /** * Exclude order comments from queries and RSS. * * @param string $where The WHERE clause of the query. * @return string */ public static function exclude_order_comments_from_feed_where( $where ) { return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'order_note' "; } /** * Exclude webhook comments from queries and RSS. * * @since 2.2 * @param array $clauses A compacted array of comment query clauses. * @return array */ public static function exclude_webhook_comments( $clauses ) { $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; return $clauses; } /** * Exclude webhooks comments from feed. * * @deprecated 3.1 * @param mixed $join Deprecated. */ public static function exclude_webhook_comments_from_feed_join( $join ) { wc_deprecated_function( 'WC_Comments::exclude_webhook_comments_from_feed_join', '3.1' ); } /** * Exclude webhook comments from queries and RSS. * * @since 2.1 * @param string $where The WHERE clause of the query. * @return string */ public static function exclude_webhook_comments_from_feed_where( $where ) { return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; } /** * Validate the comment ratings. * * @param array $comment_data Comment data. * @return array */ public static function check_comment_rating( $comment_data ) { // If posting a comment (not trackback etc) and not logged in. if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok. wp_die( esc_html__( 'Please rate the product.', 'woocommerce' ) ); exit; } return $comment_data; } /** * Rating field for comments. * * @param int $comment_id Comment ID. */ public static function add_comment_rating( $comment_id ) { if ( isset( $_POST['rating'], $_POST['comment_post_ID'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok. if ( ! $_POST['rating'] || $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok, CSRF ok, sanitization ok. return; } add_comment_meta( $comment_id, 'rating', intval( $_POST['rating'] ), true ); // WPCS: input var ok, CSRF ok. $post_id = isset( $_POST['comment_post_ID'] ) ? absint( $_POST['comment_post_ID'] ) : 0; // WPCS: input var ok, CSRF ok. if ( $post_id ) { self::clear_transients( $post_id ); } } } /** * Modify recipient of review email. * * @param array $emails Emails. * @param int $comment_id Comment ID. * @return array */ public static function comment_moderation_recipients( $emails, $comment_id ) { $comment = get_comment( $comment_id ); if ( $comment && 'product' === get_post_type( $comment->comment_post_ID ) ) { $emails = array( get_option( 'admin_email' ) ); } return $emails; } /** * Ensure product average rating and review count is kept up to date. * * @param int $post_id Post ID. */ public static function clear_transients( $post_id ) { if ( 'product' === get_post_type( $post_id ) ) { $product = wc_get_product( $post_id ); $product->set_rating_counts( self::get_rating_counts_for_product( $product ) ); $product->set_average_rating( self::get_average_rating_for_product( $product ) ); $product->set_review_count( self::get_review_count_for_product( $product ) ); $product->save(); } } /** * Delete comments count cache whenever there is * new comment or the status of a comment changes. Cache * will be regenerated next time WC_Comments::wp_count_comments() * is called. */ public static function delete_comments_count_cache() { delete_transient( 'wc_count_comments' ); } /** * Remove order notes and webhook delivery logs from wp_count_comments(). * * @since 2.2 * @param object $stats Comment stats. * @param int $post_id Post ID. * @return object */ public static function wp_count_comments( $stats, $post_id ) { global $wpdb; if ( 0 === $post_id ) { $stats = get_transient( 'wc_count_comments' ); if ( ! $stats ) { $stats = array( 'total_comments' => 0, 'all' => 0, ); $count = $wpdb->get_results( " SELECT comment_approved, COUNT(*) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN ('action_log', 'order_note', 'webhook_delivery') GROUP BY comment_approved ", ARRAY_A ); $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed', ); foreach ( (array) $count as $row ) { // Don't count post-trashed toward totals. if ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash', 'spam' ), true ) ) { $stats['all'] += $row['num_comments']; $stats['total_comments'] += $row['num_comments']; } elseif ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash' ), true ) ) { $stats['total_comments'] += $row['num_comments']; } if ( isset( $approved[ $row['comment_approved'] ] ) ) { $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; } } foreach ( $approved as $key ) { if ( empty( $stats[ $key ] ) ) { $stats[ $key ] = 0; } } $stats = (object) $stats; set_transient( 'wc_count_comments', $stats ); } } return $stats; } /** * Make sure WP displays avatars for comments with the `review` type. * * @since 2.3 * @param array $comment_types Comment types. * @return array */ public static function add_avatar_for_review_comment_type( $comment_types ) { return array_merge( $comment_types, array( 'review' ) ); } /** * Determine if a review is from a verified owner at submission. * * @param int $comment_id Comment ID. * @return bool */ public static function add_comment_purchase_verification( $comment_id ) { $comment = get_comment( $comment_id ); $verified = false; if ( 'product' === get_post_type( $comment->comment_post_ID ) ) { $verified = wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $comment->comment_post_ID ); add_comment_meta( $comment_id, 'verified', (int) $verified, true ); } return $verified; } /** * Get product rating for a product. Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return float */ public static function get_average_rating_for_product( &$product ) { global $wpdb; $count = $product->get_rating_count(); if ( $count ) { $ratings = $wpdb->get_var( $wpdb->prepare( " SELECT SUM(meta_value) FROM $wpdb->commentmeta LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID WHERE meta_key = 'rating' AND comment_post_ID = %d AND comment_approved = '1' AND meta_value > 0 ", $product->get_id() ) ); $average = number_format( $ratings / $count, 2, '.', '' ); } else { $average = 0; } return $average; } /** * Utility function for getting review counts for multiple products in one query. This is not cached. * * @since 5.0.0 * * @param array $product_ids Array of product IDs. * * @return array */ public static function get_review_counts_for_product_ids( $product_ids ) { global $wpdb; if ( empty( $product_ids ) ) { return array(); } $product_id_string_placeholder = substr( str_repeat( ',%s', count( $product_ids ) ), 1 ); $review_counts = $wpdb->get_results( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in IN query. $wpdb->prepare( " SELECT comment_post_ID as product_id, COUNT( comment_post_ID ) as review_count FROM $wpdb->comments WHERE comment_parent = 0 AND comment_post_ID IN ( $product_id_string_placeholder ) AND comment_approved = '1' AND comment_type in ( 'review', '', 'comment' ) GROUP BY product_id ", $product_ids ), // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared. ARRAY_A ); // Convert to key value pairs. $counts = array_replace( array_fill_keys( $product_ids, 0 ), array_column( $review_counts, 'review_count', 'product_id' ) ); return $counts; } /** * Get product review count for a product (not replies). Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return int */ public static function get_review_count_for_product( &$product ) { $counts = self::get_review_counts_for_product_ids( array( $product->get_id() ) ); return $counts[ $product->get_id() ]; } /** * Get product rating count for a product. Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return int[] */ public static function get_rating_counts_for_product( &$product ) { global $wpdb; $counts = array(); $raw_counts = $wpdb->get_results( $wpdb->prepare( " SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID WHERE meta_key = 'rating' AND comment_post_ID = %d AND comment_approved = '1' AND meta_value > 0 GROUP BY meta_value ", $product->get_id() ) ); foreach ( $raw_counts as $count ) { $counts[ $count->meta_value ] = absint( $count->meta_value_count ); // WPCS: slow query ok. } return $counts; } /** * Update comment type of product reviews. * * @since 3.5.0 * @param array $comment_data Comment data. * @return array */ public static function update_comment_type( $comment_data ) { if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok. $comment_data['comment_type'] = 'review'; } return $comment_data; } /** * Validate product reviews if requires a verified owner. * * @param int $comment_post_id Post ID. */ public static function validate_product_review_verified_owners( $comment_post_id ) { // Only validate if option is enabled. if ( 'yes' !== get_option( 'woocommerce_review_rating_verification_required' ) ) { return; } // Validate only products. if ( 'product' !== get_post_type( $comment_post_id ) ) { return; } // Skip if is a verified owner. if ( wc_customer_bought_product( '', get_current_user_id(), $comment_post_id ) ) { return; } wp_die( esc_html__( 'Only logged in customers who have purchased this product may leave a review.', 'woocommerce' ), esc_html__( 'Reviews can only be left by "verified owners"', 'woocommerce' ), array( 'code' => 403, ) ); } /** * Determines if a comment is of the default type. * * Prior to WordPress 5.5, '' was the default comment type. * As of 5.5, the default type is 'comment'. * * @since 4.3.0 * @param string $comment_type Comment type. * @return bool */ private static function is_default_comment_type( $comment_type ) { return ( '' === $comment_type || 'comment' === $comment_type ); } } WC_Comments::init(); includes/class-wc-post-types.php 0000644 00000070144 15132754524 0012742 0 ustar 00 <?php /** * Post Types * * Registers post types and taxonomies. * * @package WooCommerce\Classes\Products * @version 2.5.0 */ defined( 'ABSPATH' ) || exit; /** * Post types Class. */ class WC_Post_Types { /** * Hook in methods. */ public static function init() { add_action( 'init', array( __CLASS__, 'register_taxonomies' ), 5 ); add_action( 'init', array( __CLASS__, 'register_post_types' ), 5 ); add_action( 'init', array( __CLASS__, 'register_post_status' ), 9 ); add_action( 'init', array( __CLASS__, 'support_jetpack_omnisearch' ) ); add_filter( 'term_updated_messages', array( __CLASS__, 'updated_term_messages' ) ); add_filter( 'rest_api_allowed_post_types', array( __CLASS__, 'rest_api_allowed_post_types' ) ); add_action( 'woocommerce_after_register_post_type', array( __CLASS__, 'maybe_flush_rewrite_rules' ) ); add_action( 'woocommerce_flush_rewrite_rules', array( __CLASS__, 'flush_rewrite_rules' ) ); add_filter( 'gutenberg_can_edit_post_type', array( __CLASS__, 'gutenberg_can_edit_post_type' ), 10, 2 ); add_filter( 'use_block_editor_for_post_type', array( __CLASS__, 'gutenberg_can_edit_post_type' ), 10, 2 ); } /** * Register core taxonomies. */ public static function register_taxonomies() { if ( ! is_blog_installed() ) { return; } if ( taxonomy_exists( 'product_type' ) ) { return; } do_action( 'woocommerce_register_taxonomy' ); $permalinks = wc_get_permalink_structure(); register_taxonomy( 'product_type', apply_filters( 'woocommerce_taxonomy_objects_product_type', array( 'product' ) ), apply_filters( 'woocommerce_taxonomy_args_product_type', array( 'hierarchical' => false, 'show_ui' => false, 'show_in_nav_menus' => false, 'query_var' => is_admin(), 'rewrite' => false, 'public' => false, 'label' => _x( 'Product type', 'Taxonomy name', 'woocommerce' ), ) ) ); register_taxonomy( 'product_visibility', apply_filters( 'woocommerce_taxonomy_objects_product_visibility', array( 'product', 'product_variation' ) ), apply_filters( 'woocommerce_taxonomy_args_product_visibility', array( 'hierarchical' => false, 'show_ui' => false, 'show_in_nav_menus' => false, 'query_var' => is_admin(), 'rewrite' => false, 'public' => false, 'label' => _x( 'Product visibility', 'Taxonomy name', 'woocommerce' ), ) ) ); register_taxonomy( 'product_cat', apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ), apply_filters( 'woocommerce_taxonomy_args_product_cat', array( 'hierarchical' => true, 'update_count_callback' => '_wc_term_recount', 'label' => __( 'Categories', 'woocommerce' ), 'labels' => array( 'name' => __( 'Product categories', 'woocommerce' ), 'singular_name' => __( 'Category', 'woocommerce' ), 'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce' ), 'search_items' => __( 'Search categories', 'woocommerce' ), 'all_items' => __( 'All categories', 'woocommerce' ), 'parent_item' => __( 'Parent category', 'woocommerce' ), 'parent_item_colon' => __( 'Parent category:', 'woocommerce' ), 'edit_item' => __( 'Edit category', 'woocommerce' ), 'update_item' => __( 'Update category', 'woocommerce' ), 'add_new_item' => __( 'Add new category', 'woocommerce' ), 'new_item_name' => __( 'New category name', 'woocommerce' ), 'not_found' => __( 'No categories found', 'woocommerce' ), 'item_link' => __( 'Product Category Link', 'woocommerce' ), 'item_link_description' => __( 'A link to a product category.', 'woocommerce' ), ), 'show_in_rest' => true, 'show_ui' => true, 'query_var' => true, 'capabilities' => array( 'manage_terms' => 'manage_product_terms', 'edit_terms' => 'edit_product_terms', 'delete_terms' => 'delete_product_terms', 'assign_terms' => 'assign_product_terms', ), 'rewrite' => array( 'slug' => $permalinks['category_rewrite_slug'], 'with_front' => false, 'hierarchical' => true, ), ) ) ); register_taxonomy( 'product_tag', apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ), apply_filters( 'woocommerce_taxonomy_args_product_tag', array( 'hierarchical' => false, 'update_count_callback' => '_wc_term_recount', 'label' => __( 'Product tags', 'woocommerce' ), 'labels' => array( 'name' => __( 'Product tags', 'woocommerce' ), 'singular_name' => __( 'Tag', 'woocommerce' ), 'menu_name' => _x( 'Tags', 'Admin menu name', 'woocommerce' ), 'search_items' => __( 'Search tags', 'woocommerce' ), 'all_items' => __( 'All tags', 'woocommerce' ), 'edit_item' => __( 'Edit tag', 'woocommerce' ), 'update_item' => __( 'Update tag', 'woocommerce' ), 'add_new_item' => __( 'Add new tag', 'woocommerce' ), 'new_item_name' => __( 'New tag name', 'woocommerce' ), 'popular_items' => __( 'Popular tags', 'woocommerce' ), 'separate_items_with_commas' => __( 'Separate tags with commas', 'woocommerce' ), 'add_or_remove_items' => __( 'Add or remove tags', 'woocommerce' ), 'choose_from_most_used' => __( 'Choose from the most used tags', 'woocommerce' ), 'not_found' => __( 'No tags found', 'woocommerce' ), 'item_link' => __( 'Product Tag Link', 'woocommerce' ), 'item_link_description' => __( 'A link to a product tag.', 'woocommerce' ), ), 'show_in_rest' => true, 'show_ui' => true, 'query_var' => true, 'capabilities' => array( 'manage_terms' => 'manage_product_terms', 'edit_terms' => 'edit_product_terms', 'delete_terms' => 'delete_product_terms', 'assign_terms' => 'assign_product_terms', ), 'rewrite' => array( 'slug' => $permalinks['tag_rewrite_slug'], 'with_front' => false, ), ) ) ); register_taxonomy( 'product_shipping_class', apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array( 'product', 'product_variation' ) ), apply_filters( 'woocommerce_taxonomy_args_product_shipping_class', array( 'hierarchical' => false, 'update_count_callback' => '_update_post_term_count', 'label' => __( 'Shipping classes', 'woocommerce' ), 'labels' => array( 'name' => __( 'Product shipping classes', 'woocommerce' ), 'singular_name' => __( 'Shipping class', 'woocommerce' ), 'menu_name' => _x( 'Shipping classes', 'Admin menu name', 'woocommerce' ), 'search_items' => __( 'Search shipping classes', 'woocommerce' ), 'all_items' => __( 'All shipping classes', 'woocommerce' ), 'parent_item' => __( 'Parent shipping class', 'woocommerce' ), 'parent_item_colon' => __( 'Parent shipping class:', 'woocommerce' ), 'edit_item' => __( 'Edit shipping class', 'woocommerce' ), 'update_item' => __( 'Update shipping class', 'woocommerce' ), 'add_new_item' => __( 'Add new shipping class', 'woocommerce' ), 'new_item_name' => __( 'New shipping class Name', 'woocommerce' ), ), 'show_ui' => false, 'show_in_quick_edit' => false, 'show_in_nav_menus' => false, 'query_var' => is_admin(), 'capabilities' => array( 'manage_terms' => 'manage_product_terms', 'edit_terms' => 'edit_product_terms', 'delete_terms' => 'delete_product_terms', 'assign_terms' => 'assign_product_terms', ), 'rewrite' => false, ) ) ); global $wc_product_attributes; $wc_product_attributes = array(); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( $attribute_taxonomies ) { foreach ( $attribute_taxonomies as $tax ) { $name = wc_attribute_taxonomy_name( $tax->attribute_name ); if ( $name ) { $tax->attribute_public = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 ); $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; $wc_product_attributes[ $name ] = $tax; $taxonomy_data = array( 'hierarchical' => false, 'update_count_callback' => '_update_post_term_count', 'labels' => array( /* translators: %s: attribute name */ 'name' => sprintf( _x( 'Product %s', 'Product Attribute', 'woocommerce' ), $label ), 'singular_name' => $label, /* translators: %s: attribute name */ 'search_items' => sprintf( __( 'Search %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'all_items' => sprintf( __( 'All %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'parent_item' => sprintf( __( 'Parent %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'edit_item' => sprintf( __( 'Edit %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'update_item' => sprintf( __( 'Update %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'add_new_item' => sprintf( __( 'Add new %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'new_item_name' => sprintf( __( 'New %s', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'not_found' => sprintf( __( 'No "%s" found', 'woocommerce' ), $label ), /* translators: %s: attribute name */ 'back_to_items' => sprintf( __( '← Back to "%s" attributes', 'woocommerce' ), $label ), ), 'show_ui' => true, 'show_in_quick_edit' => false, 'show_in_menu' => false, 'meta_box_cb' => false, 'query_var' => 1 === $tax->attribute_public, 'rewrite' => false, 'sort' => false, 'public' => 1 === $tax->attribute_public, 'show_in_nav_menus' => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ), 'capabilities' => array( 'manage_terms' => 'manage_product_terms', 'edit_terms' => 'edit_product_terms', 'delete_terms' => 'delete_product_terms', 'assign_terms' => 'assign_product_terms', ), ); if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) { $taxonomy_data['rewrite'] = array( 'slug' => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . urldecode( sanitize_title( $tax->attribute_name ) ), 'with_front' => false, 'hierarchical' => true, ); } register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) ); } } } do_action( 'woocommerce_after_register_taxonomy' ); } /** * Register core post types. */ public static function register_post_types() { if ( ! is_blog_installed() || post_type_exists( 'product' ) ) { return; } do_action( 'woocommerce_register_post_type' ); $permalinks = wc_get_permalink_structure(); $supports = array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields', 'publicize', 'wpcom-markdown' ); if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) { $supports[] = 'comments'; } $shop_page_id = wc_get_page_id( 'shop' ); if ( current_theme_supports( 'woocommerce' ) ) { $has_archive = $shop_page_id && get_post( $shop_page_id ) ? urldecode( get_page_uri( $shop_page_id ) ) : 'shop'; } else { $has_archive = false; } // If theme support changes, we may need to flush permalinks since some are changed based on this flag. $theme_support = current_theme_supports( 'woocommerce' ) ? 'yes' : 'no'; if ( get_option( 'current_theme_supports_woocommerce' ) !== $theme_support && update_option( 'current_theme_supports_woocommerce', $theme_support ) ) { update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' ); } register_post_type( 'product', apply_filters( 'woocommerce_register_post_type_product', array( 'labels' => array( 'name' => __( 'Products', 'woocommerce' ), 'singular_name' => __( 'Product', 'woocommerce' ), 'all_items' => __( 'All Products', 'woocommerce' ), 'menu_name' => _x( 'Products', 'Admin menu name', 'woocommerce' ), 'add_new' => __( 'Add New', 'woocommerce' ), 'add_new_item' => __( 'Add new product', 'woocommerce' ), 'edit' => __( 'Edit', 'woocommerce' ), 'edit_item' => __( 'Edit product', 'woocommerce' ), 'new_item' => __( 'New product', 'woocommerce' ), 'view_item' => __( 'View product', 'woocommerce' ), 'view_items' => __( 'View products', 'woocommerce' ), 'search_items' => __( 'Search products', 'woocommerce' ), 'not_found' => __( 'No products found', 'woocommerce' ), 'not_found_in_trash' => __( 'No products found in trash', 'woocommerce' ), 'parent' => __( 'Parent product', 'woocommerce' ), 'featured_image' => __( 'Product image', 'woocommerce' ), 'set_featured_image' => __( 'Set product image', 'woocommerce' ), 'remove_featured_image' => __( 'Remove product image', 'woocommerce' ), 'use_featured_image' => __( 'Use as product image', 'woocommerce' ), 'insert_into_item' => __( 'Insert into product', 'woocommerce' ), 'uploaded_to_this_item' => __( 'Uploaded to this product', 'woocommerce' ), 'filter_items_list' => __( 'Filter products', 'woocommerce' ), 'items_list_navigation' => __( 'Products navigation', 'woocommerce' ), 'items_list' => __( 'Products list', 'woocommerce' ), 'item_link' => __( 'Product Link', 'woocommerce' ), 'item_link_description' => __( 'A link to a product.', 'woocommerce' ), ), 'description' => __( 'This is where you can browse products in this store.', 'woocommerce' ), 'public' => true, 'show_ui' => true, 'menu_icon' => 'dashicons-archive', 'capability_type' => 'product', 'map_meta_cap' => true, 'publicly_queryable' => true, 'exclude_from_search' => false, 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! 'rewrite' => $permalinks['product_rewrite_slug'] ? array( 'slug' => $permalinks['product_rewrite_slug'], 'with_front' => false, 'feeds' => true, ) : false, 'query_var' => true, 'supports' => $supports, 'has_archive' => $has_archive, 'show_in_nav_menus' => true, 'show_in_rest' => true, ) ) ); register_post_type( 'product_variation', apply_filters( 'woocommerce_register_post_type_product_variation', array( 'label' => __( 'Variations', 'woocommerce' ), 'public' => false, 'hierarchical' => false, 'supports' => false, 'capability_type' => 'product', 'rewrite' => false, ) ) ); wc_register_order_type( 'shop_order', apply_filters( 'woocommerce_register_post_type_shop_order', array( 'labels' => array( 'name' => __( 'Orders', 'woocommerce' ), 'singular_name' => _x( 'Order', 'shop_order post type singular name', 'woocommerce' ), 'add_new' => __( 'Add order', 'woocommerce' ), 'add_new_item' => __( 'Add new order', 'woocommerce' ), 'edit' => __( 'Edit', 'woocommerce' ), 'edit_item' => __( 'Edit order', 'woocommerce' ), 'new_item' => __( 'New order', 'woocommerce' ), 'view_item' => __( 'View order', 'woocommerce' ), 'search_items' => __( 'Search orders', 'woocommerce' ), 'not_found' => __( 'No orders found', 'woocommerce' ), 'not_found_in_trash' => __( 'No orders found in trash', 'woocommerce' ), 'parent' => __( 'Parent orders', 'woocommerce' ), 'menu_name' => _x( 'Orders', 'Admin menu name', 'woocommerce' ), 'filter_items_list' => __( 'Filter orders', 'woocommerce' ), 'items_list_navigation' => __( 'Orders navigation', 'woocommerce' ), 'items_list' => __( 'Orders list', 'woocommerce' ), ), 'description' => __( 'This is where store orders are stored.', 'woocommerce' ), 'public' => false, 'show_ui' => true, 'capability_type' => 'shop_order', 'map_meta_cap' => true, 'publicly_queryable' => false, 'exclude_from_search' => true, 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, 'hierarchical' => false, 'show_in_nav_menus' => false, 'rewrite' => false, 'query_var' => false, 'supports' => array( 'title', 'comments', 'custom-fields' ), 'has_archive' => false, ) ) ); wc_register_order_type( 'shop_order_refund', apply_filters( 'woocommerce_register_post_type_shop_order_refund', array( 'label' => __( 'Refunds', 'woocommerce' ), 'capability_type' => 'shop_order', 'public' => false, 'hierarchical' => false, 'supports' => false, 'exclude_from_orders_screen' => false, 'add_order_meta_boxes' => false, 'exclude_from_order_count' => true, 'exclude_from_order_views' => false, 'exclude_from_order_reports' => false, 'exclude_from_order_sales_reports' => true, 'class_name' => 'WC_Order_Refund', 'rewrite' => false, ) ) ); if ( 'yes' === get_option( 'woocommerce_enable_coupons' ) ) { register_post_type( 'shop_coupon', apply_filters( 'woocommerce_register_post_type_shop_coupon', array( 'labels' => array( 'name' => __( 'Coupons', 'woocommerce' ), 'singular_name' => __( 'Coupon', 'woocommerce' ), 'menu_name' => _x( 'Coupons', 'Admin menu name', 'woocommerce' ), 'add_new' => __( 'Add coupon', 'woocommerce' ), 'add_new_item' => __( 'Add new coupon', 'woocommerce' ), 'edit' => __( 'Edit', 'woocommerce' ), 'edit_item' => __( 'Edit coupon', 'woocommerce' ), 'new_item' => __( 'New coupon', 'woocommerce' ), 'view_item' => __( 'View coupon', 'woocommerce' ), 'search_items' => __( 'Search coupons', 'woocommerce' ), 'not_found' => __( 'No coupons found', 'woocommerce' ), 'not_found_in_trash' => __( 'No coupons found in trash', 'woocommerce' ), 'parent' => __( 'Parent coupon', 'woocommerce' ), 'filter_items_list' => __( 'Filter coupons', 'woocommerce' ), 'items_list_navigation' => __( 'Coupons navigation', 'woocommerce' ), 'items_list' => __( 'Coupons list', 'woocommerce' ), ), 'description' => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ), 'public' => false, 'show_ui' => true, 'capability_type' => 'shop_coupon', 'map_meta_cap' => true, 'publicly_queryable' => false, 'exclude_from_search' => true, 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'supports' => array( 'title' ), 'show_in_nav_menus' => false, 'show_in_admin_bar' => true, ) ) ); } do_action( 'woocommerce_after_register_post_type' ); } /** * Customize taxonomies update messages. * * @param array $messages The list of available messages. * @since 4.4.0 * @return bool */ public static function updated_term_messages( $messages ) { $messages['product_cat'] = array( 0 => '', 1 => __( 'Category added.', 'woocommerce' ), 2 => __( 'Category deleted.', 'woocommerce' ), 3 => __( 'Category updated.', 'woocommerce' ), 4 => __( 'Category not added.', 'woocommerce' ), 5 => __( 'Category not updated.', 'woocommerce' ), 6 => __( 'Categories deleted.', 'woocommerce' ), ); $messages['product_tag'] = array( 0 => '', 1 => __( 'Tag added.', 'woocommerce' ), 2 => __( 'Tag deleted.', 'woocommerce' ), 3 => __( 'Tag updated.', 'woocommerce' ), 4 => __( 'Tag not added.', 'woocommerce' ), 5 => __( 'Tag not updated.', 'woocommerce' ), 6 => __( 'Tags deleted.', 'woocommerce' ), ); $wc_product_attributes = array(); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( $attribute_taxonomies ) { foreach ( $attribute_taxonomies as $tax ) { $name = wc_attribute_taxonomy_name( $tax->attribute_name ); if ( $name ) { $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; $messages[ $name ] = array( 0 => '', /* translators: %s: taxonomy label */ 1 => sprintf( _x( '%s added', 'taxonomy term messages', 'woocommerce' ), $label ), /* translators: %s: taxonomy label */ 2 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), /* translators: %s: taxonomy label */ 3 => sprintf( _x( '%s updated', 'taxonomy term messages', 'woocommerce' ), $label ), /* translators: %s: taxonomy label */ 4 => sprintf( _x( '%s not added', 'taxonomy term messages', 'woocommerce' ), $label ), /* translators: %s: taxonomy label */ 5 => sprintf( _x( '%s not updated', 'taxonomy term messages', 'woocommerce' ), $label ), /* translators: %s: taxonomy label */ 6 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), ); } } } return $messages; } /** * Register our custom post statuses, used for order status. */ public static function register_post_status() { $order_statuses = apply_filters( 'woocommerce_register_shop_order_post_statuses', array( 'wc-pending' => array( 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Pending payment <span class="count">(%s)</span>', 'Pending payment <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-processing' => array( 'label' => _x( 'Processing', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Processing <span class="count">(%s)</span>', 'Processing <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-on-hold' => array( 'label' => _x( 'On hold', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'On hold <span class="count">(%s)</span>', 'On hold <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-completed' => array( 'label' => _x( 'Completed', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-cancelled' => array( 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Cancelled <span class="count">(%s)</span>', 'Cancelled <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-refunded' => array( 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Refunded <span class="count">(%s)</span>', 'Refunded <span class="count">(%s)</span>', 'woocommerce' ), ), 'wc-failed' => array( 'label' => _x( 'Failed', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'woocommerce' ), ), ) ); foreach ( $order_statuses as $order_status => $values ) { register_post_status( $order_status, $values ); } } /** * Flush rules if the event is queued. * * @since 3.3.0 */ public static function maybe_flush_rewrite_rules() { if ( 'yes' === get_option( 'woocommerce_queue_flush_rewrite_rules' ) ) { update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' ); self::flush_rewrite_rules(); } } /** * Flush rewrite rules. */ public static function flush_rewrite_rules() { flush_rewrite_rules(); } /** * Disable Gutenberg for products. * * @param bool $can_edit Whether the post type can be edited or not. * @param string $post_type The post type being checked. * @return bool */ public static function gutenberg_can_edit_post_type( $can_edit, $post_type ) { return 'product' === $post_type ? false : $can_edit; } /** * Add Product Support to Jetpack Omnisearch. */ public static function support_jetpack_omnisearch() { if ( class_exists( 'Jetpack_Omnisearch_Posts' ) ) { new Jetpack_Omnisearch_Posts( 'product' ); } } /** * Added product for Jetpack related posts. * * @param array $post_types Post types. * @return array */ public static function rest_api_allowed_post_types( $post_types ) { $post_types[] = 'product'; return $post_types; } } WC_Post_types::init(); includes/wc-order-functions.php 0000644 00000104353 15132754524 0012631 0 ustar 00 <?php /** * WooCommerce Order Functions * * Functions for order specific things. * * @package WooCommerce\Functions * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * Standard way of retrieving orders based on certain parameters. * * This function should be used for order retrieval so that when we move to * custom tables, functions still work. * * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query * * @since 2.6.0 * @param array $args Array of args (above). * @return WC_Order[]|stdClass Number of pages and an array of order objects if * paginate is true, or just an array of values. */ function wc_get_orders( $args ) { $map_legacy = array( 'numberposts' => 'limit', 'post_type' => 'type', 'post_status' => 'status', 'post_parent' => 'parent', 'author' => 'customer', 'email' => 'billing_email', 'posts_per_page' => 'limit', 'paged' => 'page', ); foreach ( $map_legacy as $from => $to ) { if ( isset( $args[ $from ] ) ) { $args[ $to ] = $args[ $from ]; } } // Map legacy date args to modern date args. $date_before = false; $date_after = false; if ( ! empty( $args['date_before'] ) ) { $datetime = wc_string_to_datetime( $args['date_before'] ); $date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); } if ( ! empty( $args['date_after'] ) ) { $datetime = wc_string_to_datetime( $args['date_after'] ); $date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); } if ( $date_before && $date_after ) { $args['date_created'] = $date_after . '...' . $date_before; } elseif ( $date_before ) { $args['date_created'] = '<' . $date_before; } elseif ( $date_after ) { $args['date_created'] = '>' . $date_after; } $query = new WC_Order_Query( $args ); return $query->get_orders(); } /** * Main function for returning orders, uses the WC_Order_Factory class. * * @since 2.2 * * @param mixed $the_order Post object or post ID of the order. * * @return bool|WC_Order|WC_Order_Refund */ function wc_get_order( $the_order = false ) { if ( ! did_action( 'woocommerce_after_register_post_type' ) ) { wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' ); return false; } return WC()->order_factory->get_order( $the_order ); } /** * Get all order statuses. * * @since 2.2 * @used-by WC_Order::set_status * @return array */ function wc_get_order_statuses() { $order_statuses = array( 'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ), 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), ); return apply_filters( 'wc_order_statuses', $order_statuses ); } /** * See if a string is an order status. * * @param string $maybe_status Status, including any wc- prefix. * @return bool */ function wc_is_order_status( $maybe_status ) { $order_statuses = wc_get_order_statuses(); return isset( $order_statuses[ $maybe_status ] ); } /** * Get list of statuses which are consider 'paid'. * * @since 3.0.0 * @return array */ function wc_get_is_paid_statuses() { return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ); } /** * Get list of statuses which are consider 'pending payment'. * * @since 3.6.0 * @return array */ function wc_get_is_pending_statuses() { return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) ); } /** * Get the nice name for an order status. * * @since 2.2 * @param string $status Status. * @return string */ function wc_get_order_status_name( $status ) { $statuses = wc_get_order_statuses(); $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; return $status; } /** * Generate an order key with prefix. * * @since 3.5.4 * @param string $key Order key without a prefix. By default generates a 13 digit secret. * @return string The order key. */ function wc_generate_order_key( $key = '' ) { if ( '' === $key ) { $key = wp_generate_password( 13, false ); } return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key ); } /** * Finds an Order ID based on an order key. * * @param string $order_key An order key has generated by. * @return int The ID of an order, or 0 if the order could not be found. */ function wc_get_order_id_by_order_key( $order_key ) { $data_store = WC_Data_Store::load( 'order' ); return $data_store->get_order_id_by_order_key( $order_key ); } /** * Get all registered order types. * * @since 2.2 * @param string $for Optionally define what you are getting order types for so * only relevant types are returned. * e.g. for 'order-meta-boxes', 'order-count'. * @return array */ function wc_get_order_types( $for = '' ) { global $wc_order_types; if ( ! is_array( $wc_order_types ) ) { $wc_order_types = array(); } $order_types = array(); switch ( $for ) { case 'order-count': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_count'] ) { $order_types[] = $type; } } break; case 'order-meta-boxes': foreach ( $wc_order_types as $type => $args ) { if ( $args['add_order_meta_boxes'] ) { $order_types[] = $type; } } break; case 'view-orders': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_views'] ) { $order_types[] = $type; } } break; case 'reports': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_reports'] ) { $order_types[] = $type; } } break; case 'sales-reports': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_sales_reports'] ) { $order_types[] = $type; } } break; case 'order-webhooks': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_webhooks'] ) { $order_types[] = $type; } } break; default: $order_types = array_keys( $wc_order_types ); break; } return apply_filters( 'wc_order_types', $order_types, $for ); } /** * Get an order type by post type name. * * @param string $type Post type name. * @return bool|array Details about the order type. */ function wc_get_order_type( $type ) { global $wc_order_types; if ( isset( $wc_order_types[ $type ] ) ) { return $wc_order_types[ $type ]; } return false; } /** * Register order type. Do not use before init. * * Wrapper for register post type, as well as a method of telling WC which. * post types are types of orders, and having them treated as such. * * $args are passed to register_post_type, but there are a few specific to this function: * - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main. * orders screen. * - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes. * - exclude_from_order_count (bool) Whether or not this order type is excluded from counts. * - exclude_from_order_views (bool) Whether or not this order type is visible by customers when. * viewing orders e.g. on the my account page. * - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports. * - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports. * * @since 2.2 * @see register_post_type for $args used in that function * @param string $type Post type. (max. 20 characters, can not contain capital letters or spaces). * @param array $args An array of arguments. * @return bool Success or failure */ function wc_register_order_type( $type, $args = array() ) { if ( post_type_exists( $type ) ) { return false; } global $wc_order_types; if ( ! is_array( $wc_order_types ) ) { $wc_order_types = array(); } // Register as a post type. if ( is_wp_error( register_post_type( $type, $args ) ) ) { return false; } // Register for WC usage. $order_type_args = array( 'exclude_from_orders_screen' => false, 'add_order_meta_boxes' => true, 'exclude_from_order_count' => false, 'exclude_from_order_views' => false, 'exclude_from_order_webhooks' => false, 'exclude_from_order_reports' => false, 'exclude_from_order_sales_reports' => false, 'class_name' => 'WC_Order', ); $args = array_intersect_key( $args, $order_type_args ); $args = wp_parse_args( $args, $order_type_args ); $wc_order_types[ $type ] = $args; return true; } /** * Return the count of processing orders. * * @return int */ function wc_processing_order_count() { return wc_orders_count( 'processing' ); } /** * Return the orders count of a specific order status. * * @param string $status Status. * @return int */ function wc_orders_count( $status ) { $count = 0; $status = 'wc-' . $status; $order_statuses = array_keys( wc_get_order_statuses() ); if ( ! in_array( $status, $order_statuses, true ) ) { return 0; } $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status; $cached_count = wp_cache_get( $cache_key, 'counts' ); if ( false !== $cached_count ) { return $cached_count; } foreach ( wc_get_order_types( 'order-count' ) as $type ) { $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); if ( $data_store ) { $count += $data_store->get_order_count( $status ); } } wp_cache_set( $cache_key, $count, 'counts' ); return $count; } /** * Grant downloadable product access to the file identified by $download_id. * * @param string $download_id File identifier. * @param int|WC_Product $product Product instance or ID. * @param WC_Order $order Order data. * @param int $qty Quantity purchased. * @param WC_Order_Item $item Item of the order. * @return int|bool insert id or false on failure. */ function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) { if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } $download = new WC_Customer_Download(); $download->set_download_id( $download_id ); $download->set_product_id( $product->get_id() ); $download->set_user_id( $order->get_customer_id() ); $download->set_order_id( $order->get_id() ); $download->set_user_email( $order->get_billing_email() ); $download->set_order_key( $order->get_order_key() ); $download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty ); $download->set_access_granted( time() ); $download->set_download_count( 0 ); $expiry = $product->get_download_expiry(); if ( $expiry > 0 ) { $from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true ); $download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) ); } $download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item ); return $download->save(); } /** * Order Status completed - give downloadable product access to customer. * * @param int $order_id Order ID. * @param bool $force Force downloadable permissions. */ function wc_downloadable_product_permissions( $order_id, $force = false ) { $order = wc_get_order( $order_id ); if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) { return; } if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) { return; } if ( count( $order->get_items() ) > 0 ) { foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( $product && $product->exists() && $product->is_downloadable() ) { $downloads = $product->get_downloads(); foreach ( array_keys( $downloads ) as $download_id ) { wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item ); } } } } $order->get_data_store()->set_download_permissions_granted( $order, true ); do_action( 'woocommerce_grant_product_download_permissions', $order_id ); } add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' ); add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' ); /** * Clear all transients cache for order data. * * @param int|WC_Order $order Order instance or ID. */ function wc_delete_shop_order_transients( $order = 0 ) { if ( is_numeric( $order ) ) { $order = wc_get_order( $order ); } $reports = WC_Admin_Reports::get_reports(); $transients_to_clear = array( 'wc_admin_report', ); foreach ( $reports as $report_group ) { foreach ( $report_group['reports'] as $report_key => $report ) { $transients_to_clear[] = 'wc_report_' . $report_key; } } foreach ( $transients_to_clear as $transient ) { delete_transient( $transient ); } // Clear customer's order related caches. if ( is_a( $order, 'WC_Order' ) ) { $order_id = $order->get_id(); delete_user_meta( $order->get_customer_id(), '_money_spent' ); delete_user_meta( $order->get_customer_id(), '_order_count' ); delete_user_meta( $order->get_customer_id(), '_last_order' ); } else { $order_id = 0; } // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'orders', true ); // Do the same for regular cache. WC_Cache_Helper::invalidate_cache_group( 'orders' ); do_action( 'woocommerce_delete_shop_order_transients', $order_id ); } /** * See if we only ship to billing addresses. * * @return bool */ function wc_ship_to_billing_address_only() { return 'billing_only' === get_option( 'woocommerce_ship_to_destination' ); } /** * Create a new order refund programmatically. * * Returns a new refund object on success which can then be used to add additional data. * * @since 2.2 * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead. * @param array $args New refund arguments. * @return WC_Order_Refund|WP_Error */ function wc_create_refund( $args = array() ) { $default_args = array( 'amount' => 0, 'reason' => null, 'order_id' => 0, 'refund_id' => 0, 'line_items' => array(), 'refund_payment' => false, 'restock_items' => false, ); try { $args = wp_parse_args( $args, $default_args ); $order = wc_get_order( $args['order_id'] ); if ( ! $order ) { throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) ); } $remaining_refund_amount = $order->get_remaining_refund_amount(); $remaining_refund_items = $order->get_remaining_refund_items(); $refund_item_count = 0; $refund = new WC_Order_Refund( $args['refund_id'] ); if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) { throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) ); } $refund->set_currency( $order->get_currency() ); $refund->set_amount( $args['amount'] ); $refund->set_parent_id( absint( $args['order_id'] ) ); $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 ); $refund->set_prices_include_tax( $order->get_prices_include_tax() ); if ( ! is_null( $args['reason'] ) ) { $refund->set_reason( $args['reason'] ); } // Negative line items. if ( count( $args['line_items'] ) > 0 ) { $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) ); foreach ( $items as $item_id => $item ) { if ( ! isset( $args['line_items'][ $item_id ] ) ) { continue; } $qty = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0; $refund_total = $args['line_items'][ $item_id ]['refund_total']; $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array(); if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) { continue; } $class = get_class( $item ); $refunded_item = new $class( $item ); $refunded_item->set_id( 0 ); $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true ); $refunded_item->set_total( wc_format_refund_total( $refund_total ) ); $refunded_item->set_taxes( array( 'total' => array_map( 'wc_format_refund_total', $refund_tax ), 'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ), ) ); if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) { $refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) ); } if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) { $refunded_item->set_quantity( $qty * -1 ); } $refund->add_item( $refunded_item ); $refund_item_count += $qty; } } $refund->update_taxes(); $refund->calculate_totals( false ); $refund->set_total( $args['amount'] * -1 ); // this should remain after update_taxes(), as this will save the order, and write the current date to the db // so we must wait until the order is persisted to set the date. if ( isset( $args['date_created'] ) ) { $refund->set_date_created( $args['date_created'] ); } /** * Action hook to adjust refund before save. * * @since 3.0.0 */ do_action( 'woocommerce_create_refund', $refund, $args ); if ( $refund->save() ) { if ( $args['refund_payment'] ) { $result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() ); if ( is_wp_error( $result ) ) { $refund->delete(); return $result; } $refund->set_refunded_payment( true ); $refund->save(); } if ( $args['restock_items'] ) { wc_restock_refunded_items( $order, $args['line_items'] ); } // Trigger notification emails. if ( ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ) ) { do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() ); } else { do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() ); $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() ); if ( $parent_status ) { $order->update_status( $parent_status ); } } } do_action( 'woocommerce_refund_created', $refund->get_id(), $args ); do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() ); } catch ( Exception $e ) { if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) { wp_delete_post( $refund->get_id(), true ); } return new WP_Error( 'error', $e->getMessage() ); } return $refund; } /** * Try to refund the payment for an order via the gateway. * * @since 3.0.0 * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead. * @param WC_Order $order Order instance. * @param string $amount Amount to refund. * @param string $reason Refund reason. * @return bool|WP_Error */ function wc_refund_payment( $order, $amount, $reason = '' ) { try { if ( ! is_a( $order, 'WC_Order' ) ) { throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); } $gateway_controller = WC_Payment_Gateways::instance(); $all_gateways = $gateway_controller->payment_gateways(); $payment_method = $order->get_payment_method(); $gateway = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false; if ( ! $gateway ) { throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) ); } if ( ! $gateway->supports( 'refunds' ) ) { throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) ); } $result = $gateway->process_refund( $order->get_id(), $amount, $reason ); if ( ! $result ) { throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) ); } if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } return true; } catch ( Exception $e ) { return new WP_Error( 'error', $e->getMessage() ); } } /** * Restock items during refund. * * @since 3.0.0 * @param WC_Order $order Order instance. * @param array $refunded_line_items Refunded items list. */ function wc_restock_refunded_items( $order, $refunded_line_items ) { if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) { return; } $line_items = $order->get_items(); foreach ( $line_items as $item_id => $item ) { if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) { continue; } $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); $restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true ); $qty_to_refund = $refunded_line_items[ $item_id ]['qty']; if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) { continue; } $old_stock = $product->get_stock_quantity(); $new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' ); // Update _reduced_stock meta to track changes. $item_stock_reduced = $item_stock_reduced - $qty_to_refund; if ( 0 < $item_stock_reduced ) { // Keeps track of total running tally of reduced stock. $item->update_meta_data( '_reduced_stock', $item_stock_reduced ); // Keeps track of only refunded items that needs restock. $item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items ); } else { $item->delete_meta_data( '_reduced_stock' ); $item->delete_meta_data( '_restock_refunded_items' ); } /* translators: 1: product ID 2: old stock level 3: new stock level */ $order->add_order_note( sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ) ); $item->save(); do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product ); } } /** * Get tax class by tax id. * * @since 2.2 * @param int $tax_id Tax ID. * @return string */ function wc_get_tax_class_by_tax_id( $tax_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) ); } /** * Get payment gateway class by order data. * * @since 2.2 * @param int|WC_Order $order Order instance. * @return WC_Payment_Gateway|bool */ function wc_get_payment_gateway_by_order( $order ) { if ( WC()->payment_gateways() ) { $payment_gateways = WC()->payment_gateways()->payment_gateways(); } else { $payment_gateways = array(); } if ( ! is_object( $order ) ) { $order_id = absint( $order ); $order = wc_get_order( $order_id ); } return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false; } /** * When refunding an order, create a refund line item if the partial refunds do not match order total. * * This is manual; no gateway refund will be performed. * * @since 2.4 * @param int $order_id Order ID. */ function wc_order_fully_refunded( $order_id ) { $order = wc_get_order( $order_id ); $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() ); if ( ! $max_refund ) { return; } // Create the refund object. wc_switch_to_site_locale(); wc_create_refund( array( 'amount' => $max_refund, 'reason' => __( 'Order fully refunded.', 'woocommerce' ), 'order_id' => $order_id, 'line_items' => array(), ) ); wc_restore_locale(); $order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) ); } add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' ); /** * Search orders. * * @since 2.6.0 * @param string $term Term to search. * @return array List of orders ID. */ function wc_order_search( $term ) { $data_store = WC_Data_Store::load( 'order' ); return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) ); } /** * Update total sales amount for each product within a paid order. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_update_total_sales_counts( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) { return; } if ( count( $order->get_items() ) > 0 ) { foreach ( $order->get_items() as $item ) { $product_id = $item->get_product_id(); if ( $product_id ) { $data_store = WC_Data_Store::load( 'product' ); $data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' ); } } } $order->get_data_store()->set_recorded_sales( $order, true ); /** * Called when sales for an order are recorded * * @param int $order_id order id */ do_action( 'woocommerce_recorded_sales', $order_id ); } add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' ); /** * Update used coupon amount for each coupon within an order. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_update_coupon_usage_counts( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); if ( $order->has_status( 'cancelled' ) && $has_recorded ) { $action = 'reduce'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { $action = 'increase'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); } elseif ( $order->has_status( 'cancelled' ) ) { $order->get_data_store()->release_held_coupons( $order, true ); return; } else { return; } if ( count( $order->get_coupon_codes() ) > 0 ) { foreach ( $order->get_coupon_codes() as $code ) { if ( ! $code ) { continue; } $coupon = new WC_Coupon( $code ); $used_by = $order->get_user_id(); if ( ! $used_by ) { $used_by = $order->get_billing_email(); } switch ( $action ) { case 'reduce': $coupon->decrease_usage_count( $used_by ); break; case 'increase': $coupon->increase_usage_count( $used_by, $order ); break; } } $order->get_data_store()->release_held_coupons( $order, true ); } } add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); /** * Cancel all unpaid orders after held duration to prevent stock lock for those products. */ function wc_cancel_unpaid_orders() { $held_duration = get_option( 'woocommerce_hold_stock_minutes' ); // Re-schedule the event before cancelling orders // this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry. wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { return; } $data_store = WC_Data_Store::load( 'order' ); $unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) ); if ( $unpaid_orders ) { foreach ( $unpaid_orders as $unpaid_order ) { $order = wc_get_order( $unpaid_order ); if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) { $order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) ); } } } } add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' ); /** * Sanitize order id removing unwanted characters. * * E.g Users can sometimes try to track an order id using # with no success. * This function will fix this. * * @since 3.1.0 * @param int $order_id Order ID. */ function wc_sanitize_order_id( $order_id ) { return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT ); } add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' ); /** * Get an order note. * * @since 3.2.0 * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). * @return stdClass|null Object with order note details or null when does not exists. */ function wc_get_order_note( $data ) { if ( is_numeric( $data ) ) { $data = get_comment( $data ); } if ( ! is_a( $data, 'WP_Comment' ) ) { return null; } return (object) apply_filters( 'woocommerce_get_order_note', array( 'id' => (int) $data->comment_ID, 'date_created' => wc_string_to_datetime( $data->comment_date ), 'content' => $data->comment_content, 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), 'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author, ), $data ); } /** * Get order notes. * * @since 3.2.0 * @param array $args Query arguments { * Array of query parameters. * * @type string $limit Maximum number of notes to retrieve. * Default empty (no limit). * @type int $order_id Limit results to those affiliated with a given order ID. * Default 0. * @type array $order__in Array of order IDs to include affiliated notes for. * Default empty. * @type array $order__not_in Array of order IDs to exclude affiliated notes for. * Default empty. * @type string $orderby Define how should sort notes. * Accepts 'date_created', 'date_created_gmt' or 'id'. * Default: 'id'. * @type string $order How to order retrieved notes. * Accepts 'ASC' or 'DESC'. * Default: 'DESC'. * @type string $type Define what type of note should retrieve. * Accepts 'customer', 'internal' or empty for both. * Default empty. * } * @return stdClass[] Array of stdClass objects with order notes details. */ function wc_get_order_notes( $args ) { $key_mapping = array( 'limit' => 'number', 'order_id' => 'post_id', 'order__in' => 'post__in', 'order__not_in' => 'post__not_in', ); foreach ( $key_mapping as $query_key => $db_key ) { if ( isset( $args[ $query_key ] ) ) { $args[ $db_key ] = $args[ $query_key ]; unset( $args[ $query_key ] ); } } // Define orderby. $orderby_mapping = array( 'date_created' => 'comment_date', 'date_created_gmt' => 'comment_date_gmt', 'id' => 'comment_ID', ); $args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID'; // Set WooCommerce order type. if ( isset( $args['type'] ) && 'customer' === $args['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'value' => 1, 'compare' => '=', ), ); } elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'compare' => 'NOT EXISTS', ), ); } // Set correct comment type. $args['type'] = 'order_note'; // Always approved. $args['status'] = 'approve'; // Does not support 'count' or 'fields'. unset( $args['count'], $args['fields'] ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); return array_filter( array_map( 'wc_get_order_note', $notes ) ); } /** * Create an order note. * * @since 3.2.0 * @param int $order_id Order ID. * @param string $note Note to add. * @param bool $is_customer_note If is a costumer note. * @param bool $added_by_user If note is create by an user. * @return int|WP_Error Integer when created or WP_Error when found an error. */ function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) ); } return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user ); } /** * Delete an order note. * * @since 3.2.0 * @param int $note_id Order note. * @return bool True on success, false on failure. */ function wc_delete_order_note( $note_id ) { return wp_delete_comment( $note_id, true ); } includes/wc-order-item-functions.php 0000644 00000012041 15132754524 0013555 0 ustar 00 <?php /** * WooCommerce Order Item Functions * * Functions for order specific things. * * @package WooCommerce\Functions * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * Add a item to an order (for example a line item). * * @param int $order_id Order ID. * @param array $item_array Items list. * * @throws Exception When `WC_Data_Store::load` validation fails. * @return int|bool Item ID or false */ function wc_add_order_item( $order_id, $item_array ) { $order_id = absint( $order_id ); if ( ! $order_id ) { return false; } $defaults = array( 'order_item_name' => '', 'order_item_type' => 'line_item', ); $item_array = wp_parse_args( $item_array, $defaults ); $data_store = WC_Data_Store::load( 'order-item' ); $item_id = $data_store->add_order_item( $order_id, $item_array ); $item = WC_Order_Factory::get_order_item( $item_id ); do_action( 'woocommerce_new_order_item', $item_id, $item, $order_id ); return $item_id; } /** * Update an item for an order. * * @since 2.2 * @param int $item_id Item ID. * @param array $args Either `order_item_type` or `order_item_name`. * * @throws Exception When `WC_Data_Store::load` validation fails. * @return bool True if successfully updated, false otherwise. */ function wc_update_order_item( $item_id, $args ) { $data_store = WC_Data_Store::load( 'order-item' ); $update = $data_store->update_order_item( $item_id, $args ); if ( false === $update ) { return false; } do_action( 'woocommerce_update_order_item', $item_id, $args ); return true; } /** * Delete an item from the order it belongs to based on item id. * * @param int $item_id Item ID. * * @throws Exception When `WC_Data_Store::load` validation fails. * @return bool */ function wc_delete_order_item( $item_id ) { $item_id = absint( $item_id ); if ( ! $item_id ) { return false; } $data_store = WC_Data_Store::load( 'order-item' ); do_action( 'woocommerce_before_delete_order_item', $item_id ); $data_store->delete_order_item( $item_id ); do_action( 'woocommerce_delete_order_item', $item_id ); return true; } /** * WooCommerce Order Item Meta API - Update term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value. * @param string $prev_value Previous value (default: ''). * * @throws Exception When `WC_Data_Store::load` validation fails. * @return bool */ function wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { $data_store = WC_Data_Store::load( 'order-item' ); if ( $data_store->update_metadata( $item_id, $meta_key, $meta_value, $prev_value ) ) { WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. return true; } return false; } /** * WooCommerce Order Item Meta API - Add term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value. * @param bool $unique If meta data should be unique (default: false). * * @throws Exception When `WC_Data_Store::load` validation fails. * @return int New row ID or 0. */ function wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { $data_store = WC_Data_Store::load( 'order-item' ); $meta_id = $data_store->add_metadata( $item_id, $meta_key, $meta_value, $unique ); if ( $meta_id ) { WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. return $meta_id; } return 0; } /** * WooCommerce Order Item Meta API - Delete term meta. * * @param int $item_id Item ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value (default: ''). * @param bool $delete_all Delete all meta data, defaults to `false`. * * @throws Exception When `WC_Data_Store::load` validation fails. * @return bool */ function wc_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { $data_store = WC_Data_Store::load( 'order-item' ); if ( $data_store->delete_metadata( $item_id, $meta_key, $meta_value, $delete_all ) ) { WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. return true; } return false; } /** * WooCommerce Order Item Meta API - Get term meta. * * @param int $item_id Item ID. * @param string $key Meta key. * @param bool $single Whether to return a single value. (default: true). * * @throws Exception When `WC_Data_Store::load` validation fails. * @return mixed */ function wc_get_order_item_meta( $item_id, $key, $single = true ) { $data_store = WC_Data_Store::load( 'order-item' ); return $data_store->get_metadata( $item_id, $key, $single ); } /** * Get order ID by order item ID. * * @param int $item_id Item ID. * * @throws Exception When `WC_Data_Store::load` validation fails. * @return int */ function wc_get_order_id_by_order_item_id( $item_id ) { $data_store = WC_Data_Store::load( 'order-item' ); return $data_store->get_order_id_by_order_item_id( $item_id ); } includes/log-handlers/class-wc-log-handler-file.php 0000644 00000027021 15132754524 0016277 0 ustar 00 <?php /** * Class WC_Log_Handler_File file. * * @package WooCommerce\Log Handlers */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Handles log entries by writing to a file. * * @class WC_Log_Handler_File * @version 1.0.0 * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_File extends WC_Log_Handler { /** * Stores open file handles. * * @var array */ protected $handles = array(); /** * File size limit for log files in bytes. * * @var int */ protected $log_size_limit; /** * Cache logs that could not be written. * * If a log is written too early in the request, pluggable functions may be unavailable. These * logs will be cached and written on 'plugins_loaded' action. * * @var array */ protected $cached_logs = array(); /** * Constructor for the logger. * * @param int $log_size_limit Optional. Size limit for log files. Default 5mb. */ public function __construct( $log_size_limit = null ) { if ( null === $log_size_limit ) { $log_size_limit = 5 * 1024 * 1024; } $this->log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit ); add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) ); } /** * Destructor. * * Cleans up open file handles. */ public function __destruct() { foreach ( $this->handles as $handle ) { if ( is_resource( $handle ) ) { fclose( $handle ); // @codingStandardsIgnoreLine. } } } /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context { * Additional information for log handlers. * * @type string $source Optional. Determines log file to write to. Default 'log'. * @type bool $_legacy Optional. Default false. True to use outdated log format * originally used in deprecated WC_Logger::add calls. * } * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ) { if ( isset( $context['source'] ) && $context['source'] ) { $handle = $context['source']; } else { $handle = 'log'; } $entry = self::format_entry( $timestamp, $level, $message, $context ); return $this->add( $entry, $handle ); } /** * Builds a log entry text from timestamp, level and message. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. * * @return string Formatted log entry. */ protected static function format_entry( $timestamp, $level, $message, $context ) { if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) { if ( isset( $context['source'] ) && $context['source'] ) { $handle = $context['source']; } else { $handle = 'log'; } $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); $time = date_i18n( 'm-d-Y @ H:i:s' ); $entry = "{$time} - {$message}"; } else { $entry = parent::format_entry( $timestamp, $level, $message, $context ); } return $entry; } /** * Open log file for writing. * * @param string $handle Log handle. * @param string $mode Optional. File mode. Default 'a'. * @return bool Success. */ protected function open( $handle, $mode = 'a' ) { if ( $this->is_open( $handle ) ) { return true; } $file = self::get_log_file_path( $handle ); if ( $file ) { if ( ! file_exists( $file ) ) { $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine. if ( is_resource( $temphandle ) ) { @fclose( $temphandle ); // @codingStandardsIgnoreLine. if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) { @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine. } } } $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine. if ( $resource ) { $this->handles[ $handle ] = $resource; return true; } } return false; } /** * Check if a handle is open. * * @param string $handle Log handle. * @return bool True if $handle is open. */ protected function is_open( $handle ) { return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] ); } /** * Close a handle. * * @param string $handle Log handle. * @return bool success */ protected function close( $handle ) { $result = false; if ( $this->is_open( $handle ) ) { $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine. unset( $this->handles[ $handle ] ); } return $result; } /** * Add a log entry to chosen file. * * @param string $entry Log entry text. * @param string $handle Log entry handle. * * @return bool True if write was successful. */ protected function add( $entry, $handle ) { $result = false; if ( $this->should_rotate( $handle ) ) { $this->log_rotate( $handle ); } if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) { $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine. } else { $this->cache_log( $entry, $handle ); } return false !== $result; } /** * Clear entries from chosen file. * * @param string $handle Log handle. * * @return bool */ public function clear( $handle ) { $result = false; // Close the file if it's already open. $this->close( $handle ); /** * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at * the beginning of the file, and truncate the file to zero length. */ if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) { $result = true; } do_action( 'woocommerce_log_clear', $handle ); return $result; } /** * Remove/delete the chosen file. * * @param string $handle Log handle. * * @return bool */ public function remove( $handle ) { $removed = false; $logs = $this->get_log_files(); $handle = sanitize_title( $handle ); if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) { $file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] ); if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked. $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink } do_action( 'woocommerce_log_remove', $handle, $removed ); } return $removed; } /** * Check if log file should be rotated. * * Compares the size of the log file to determine whether it is over the size limit. * * @param string $handle Log handle. * @return bool True if if should be rotated. */ protected function should_rotate( $handle ) { $file = self::get_log_file_path( $handle ); if ( $file ) { if ( $this->is_open( $handle ) ) { $file_stat = fstat( $this->handles[ $handle ] ); return $file_stat['size'] > $this->log_size_limit; } elseif ( file_exists( $file ) ) { return filesize( $file ) > $this->log_size_limit; } else { return false; } } else { return false; } } /** * Rotate log files. * * Logs are rotated by prepending '.x' to the '.log' suffix. * The current log plus 10 historical logs are maintained. * For example: * base.9.log -> [ REMOVED ] * base.8.log -> base.9.log * ... * base.0.log -> base.1.log * base.log -> base.0.log * * @param string $handle Log handle. */ protected function log_rotate( $handle ) { for ( $i = 8; $i >= 0; $i-- ) { $this->increment_log_infix( $handle, $i ); } $this->increment_log_infix( $handle ); } /** * Increment a log file suffix. * * @param string $handle Log handle. * @param null|int $number Optional. Default null. Log suffix number to be incremented. * @return bool True if increment was successful, otherwise false. */ protected function increment_log_infix( $handle, $number = null ) { if ( null === $number ) { $suffix = ''; $next_suffix = '.0'; } else { $suffix = '.' . $number; $next_suffix = '.' . ( $number + 1 ); } $rename_from = self::get_log_file_path( "{$handle}{$suffix}" ); $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" ); if ( $this->is_open( $rename_from ) ) { $this->close( $rename_from ); } if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename } else { return false; } } /** * Get a log file path. * * @param string $handle Log name. * @return bool|string The log file path or false if path cannot be determined. */ public static function get_log_file_path( $handle ) { if ( function_exists( 'wp_hash' ) ) { return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle ); } else { wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' ); return false; } } /** * Get a log file name. * * File names consist of the handle, followed by the date, followed by a hash, .log. * * @since 3.3 * @param string $handle Log name. * @return bool|string The log file name or false if cannot be determined. */ public static function get_log_file_name( $handle ) { if ( function_exists( 'wp_hash' ) ) { $date_suffix = date( 'Y-m-d', time() ); $hash_suffix = wp_hash( $handle ); return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' ); } else { wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' ); return false; } } /** * Cache log to write later. * * @param string $entry Log entry text. * @param string $handle Log entry handle. */ protected function cache_log( $entry, $handle ) { $this->cached_logs[] = array( 'entry' => $entry, 'handle' => $handle, ); } /** * Write cached logs. */ public function write_cached_logs() { foreach ( $this->cached_logs as $log ) { $this->add( $log['entry'], $log['handle'] ); } } /** * Delete all logs older than a defined timestamp. * * @since 3.4.0 * @param integer $timestamp Timestamp to delete logs before. */ public static function delete_logs_before_timestamp( $timestamp = 0 ) { if ( ! $timestamp ) { return; } $log_files = self::get_log_files(); foreach ( $log_files as $log_file ) { $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file ); if ( $last_modified < $timestamp ) { @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine. } } } /** * Get all log files in the log directory. * * @since 3.4.0 * @return array */ public static function get_log_files() { $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine. $result = array(); if ( ! empty( $files ) ) { foreach ( $files as $key => $value ) { if ( ! in_array( $value, array( '.', '..' ), true ) ) { if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { $result[ sanitize_title( $value ) ] = $value; } } } } return $result; } } includes/log-handlers/class-wc-log-handler-email.php 0000644 00000013405 15132754524 0016450 0 ustar 00 <?php /** * Class WC_Log_Handler_Email file. * * @package WooCommerce\Log Handlers */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Handles log entries by sending an email. * * WARNING! * This log handler has known limitations. * * Log messages are aggregated and sent once per request (if necessary). If the site experiences a * problem, the log email may never be sent. This handler should be used with another handler which * stores logs in order to prevent loss. * * It is not recommended to use this handler on a high traffic site. There will be a maximum of 1 * email sent per request per handler, but that could still be a dangerous amount of emails under * heavy traffic. Do not confuse this handler with an appropriate monitoring solution! * * If you understand these limitations, feel free to use this handler or borrow parts of the design * to implement your own! * * @class WC_Log_Handler_Email * @version 1.0.0 * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_Email extends WC_Log_Handler { /** * Minimum log level this handler will process. * * @var int Integer representation of minimum log level to handle. */ protected $threshold; /** * Stores email recipients. * * @var array */ protected $recipients = array(); /** * Stores log messages. * * @var array */ protected $logs = array(); /** * Stores integer representation of maximum logged level. * * @var int */ protected $max_severity = null; /** * Constructor for log handler. * * @param string|array $recipients Optional. Email(s) to receive log messages. Defaults to site admin email. * @param string $threshold Optional. Minimum level that should receive log messages. * Default 'alert'. One of: emergency|alert|critical|error|warning|notice|info|debug. */ public function __construct( $recipients = null, $threshold = 'alert' ) { if ( null === $recipients ) { $recipients = get_option( 'admin_email' ); } if ( is_array( $recipients ) ) { foreach ( $recipients as $recipient ) { $this->add_email( $recipient ); } } else { $this->add_email( $recipients ); } $this->set_threshold( $threshold ); add_action( 'shutdown', array( $this, 'send_log_email' ) ); } /** * Set handler severity threshold. * * @param string $level emergency|alert|critical|error|warning|notice|info|debug. */ public function set_threshold( $level ) { $this->threshold = WC_Log_Levels::get_level_severity( $level ); } /** * Determine whether handler should handle log. * * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @return bool True if the log should be handled. */ protected function should_handle( $level ) { return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); } /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ) { if ( $this->should_handle( $level ) ) { $this->add_log( $timestamp, $level, $message, $context ); return true; } return false; } /** * Send log email. * * @return bool True if email is successfully sent otherwise false. */ public function send_log_email() { $result = false; if ( ! empty( $this->logs ) ) { $subject = $this->get_subject(); $body = $this->get_body(); $result = wp_mail( $this->recipients, $subject, $body ); $this->clear_logs(); } return $result; } /** * Build subject for log email. * * @return string subject */ protected function get_subject() { $site_name = get_bloginfo( 'name' ); $max_level = strtoupper( WC_Log_Levels::get_severity_level( $this->max_severity ) ); $log_count = count( $this->logs ); return sprintf( /* translators: 1: Site name 2: Maximum level 3: Log count */ _n( '[%1$s] %2$s: %3$s WooCommerce log message', '[%1$s] %2$s: %3$s WooCommerce log messages', $log_count, 'woocommerce' ), $site_name, $max_level, $log_count ); } /** * Build body for log email. * * @return string body */ protected function get_body() { $site_name = get_bloginfo( 'name' ); $entries = implode( PHP_EOL, $this->logs ); $log_count = count( $this->logs ); return _n( 'You have received the following WooCommerce log message:', 'You have received the following WooCommerce log messages:', $log_count, 'woocommerce' ) . PHP_EOL . PHP_EOL . $entries . PHP_EOL . PHP_EOL /* translators: %s: Site name */ . sprintf( __( 'Visit %s admin area:', 'woocommerce' ), $site_name ) . PHP_EOL . admin_url(); } /** * Adds an email to the list of recipients. * * @param string $email Email address to add. */ public function add_email( $email ) { array_push( $this->recipients, $email ); } /** * Add log message. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. */ protected function add_log( $timestamp, $level, $message, $context ) { $this->logs[] = $this->format_entry( $timestamp, $level, $message, $context ); $log_severity = WC_Log_Levels::get_level_severity( $level ); if ( $this->max_severity < $log_severity ) { $this->max_severity = $log_severity; } } /** * Clear log messages. */ protected function clear_logs() { $this->logs = array(); } } includes/log-handlers/class-wc-log-handler-db.php 0000644 00000011503 15132754524 0015743 0 ustar 00 <?php /** * Class WC_Log_Handler_DB file. * * @package WooCommerce\Log Handlers */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Handles log entries by writing to database. * * @class WC_Log_Handler_DB * @version 1.0.0 * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_DB extends WC_Log_Handler { /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context { * Additional information for log handlers. * * @type string $source Optional. Source will be available in log table. * If no source is provided, attempt to provide sensible default. * } * * @see WC_Log_Handler_DB::get_log_source() for default source. * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ) { if ( isset( $context['source'] ) && $context['source'] ) { $source = $context['source']; } else { $source = $this->get_log_source(); } return $this->add( $timestamp, $level, $message, $source, $context ); } /** * Add a log entry to chosen file. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param string $source Log source. Useful for filtering and sorting. * @param array $context Context will be serialized and stored in database. * * @return bool True if write was successful. */ protected static function add( $timestamp, $level, $message, $source, $context ) { global $wpdb; $insert = array( 'timestamp' => date( 'Y-m-d H:i:s', $timestamp ), 'level' => WC_Log_Levels::get_level_severity( $level ), 'message' => $message, 'source' => $source, ); $format = array( '%s', '%d', '%s', '%s', '%s', // possible serialized context. ); if ( ! empty( $context ) ) { $insert['context'] = serialize( $context ); // @codingStandardsIgnoreLine. } return false !== $wpdb->insert( "{$wpdb->prefix}woocommerce_log", $insert, $format ); } /** * Clear all logs from the DB. * * @return bool True if flush was successful. */ public static function flush() { global $wpdb; return $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_log" ); } /** * Clear entries for a chosen handle/source. * * @param string $source Log source. * @return bool */ public function clear( $source ) { global $wpdb; return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE source = %s", $source ) ); } /** * Delete selected logs from DB. * * @param int|string|array $log_ids Log ID or array of Log IDs to be deleted. * * @return bool */ public static function delete( $log_ids ) { global $wpdb; if ( ! is_array( $log_ids ) ) { $log_ids = array( $log_ids ); } $format = array_fill( 0, count( $log_ids ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE log_id IN {$query_in}", $log_ids ) ); // @codingStandardsIgnoreLine. } /** * Delete all logs older than a defined timestamp. * * @since 3.4.0 * @param integer $timestamp Timestamp to delete logs before. */ public static function delete_logs_before_timestamp( $timestamp = 0 ) { if ( ! $timestamp ) { return; } global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE timestamp < %s", date( 'Y-m-d H:i:s', $timestamp ) ) ); } /** * Get appropriate source based on file name. * * Try to provide an appropriate source in case none is provided. * * @return string Text to use as log source. "" (empty string) if none is found. */ protected static function get_log_source() { static $ignore_files = array( 'class-wc-log-handler-db', 'class-wc-logger' ); /** * PHP < 5.3.6 correct behavior * * @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters */ if ( Constants::is_defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) { $debug_backtrace_arg = DEBUG_BACKTRACE_IGNORE_ARGS; // phpcs:ignore PHPCompatibility.Constants.NewConstants.debug_backtrace_ignore_argsFound } else { $debug_backtrace_arg = false; } $trace = debug_backtrace( $debug_backtrace_arg ); // @codingStandardsIgnoreLine. foreach ( $trace as $t ) { if ( isset( $t['file'] ) ) { $filename = pathinfo( $t['file'], PATHINFO_FILENAME ); if ( ! in_array( $filename, $ignore_files, true ) ) { return $filename; } } } return ''; } } includes/class-wc-order-item-tax.php 0000644 00000014646 15132754524 0013461 0 ustar 00 <?php /** * Order Line Item (tax) * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item tax. */ class WC_Order_Item_Tax extends WC_Order_Item { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $extra_data = array( 'rate_code' => '', 'rate_id' => 0, 'label' => '', 'compound' => false, 'tax_total' => 0, 'shipping_tax_total' => 0, 'rate_percent' => null, ); /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set order item name. * * @param string $value Name. */ public function set_name( $value ) { $this->set_rate_code( $value ); } /** * Set item name. * * @param string $value Rate code. */ public function set_rate_code( $value ) { $this->set_prop( 'rate_code', wc_clean( $value ) ); } /** * Set item name. * * @param string $value Label. */ public function set_label( $value ) { $this->set_prop( 'label', wc_clean( $value ) ); } /** * Set tax rate id. * * @param int $value Rate ID. */ public function set_rate_id( $value ) { $this->set_prop( 'rate_id', absint( $value ) ); } /** * Set tax total. * * @param string $value Tax total. */ public function set_tax_total( $value ) { $this->set_prop( 'tax_total', $value ? wc_format_decimal( $value ) : 0 ); } /** * Set shipping tax total. * * @param string $value Shipping tax total. */ public function set_shipping_tax_total( $value ) { $this->set_prop( 'shipping_tax_total', $value ? wc_format_decimal( $value ) : 0 ); } /** * Set compound. * * @param bool $value If tax is compound. */ public function set_compound( $value ) { $this->set_prop( 'compound', (bool) $value ); } /** * Set rate value. * * @param float $value tax rate value. */ public function set_rate_percent( $value ) { $this->set_prop( 'rate_percent', (float) $value ); } /** * Set properties based on passed in tax rate by ID. * * @param int $tax_rate_id Tax rate ID. */ public function set_rate( $tax_rate_id ) { $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id, OBJECT ); $this->set_rate_id( $tax_rate_id ); $this->set_rate_code( WC_Tax::get_rate_code( $tax_rate ) ); $this->set_label( WC_Tax::get_rate_label( $tax_rate ) ); $this->set_compound( WC_Tax::is_compound( $tax_rate ) ); $this->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate ) ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order item type. * * @return string */ public function get_type() { return 'tax'; } /** * Get rate code/name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { return $this->get_rate_code( $context ); } /** * Get rate code/name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_rate_code( $context = 'view' ) { return $this->get_prop( 'rate_code', $context ); } /** * Get label. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_label( $context = 'view' ) { $label = $this->get_prop( 'label', $context ); if ( 'view' === $context ) { return $label ? $label : __( 'Tax', 'woocommerce' ); } else { return $label; } } /** * Get tax rate ID. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return int */ public function get_rate_id( $context = 'view' ) { return $this->get_prop( 'rate_id', $context ); } /** * Get tax_total * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_total( $context = 'view' ) { return $this->get_prop( 'tax_total', $context ); } /** * Get shipping_tax_total * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_tax_total( $context = 'view' ) { return $this->get_prop( 'shipping_tax_total', $context ); } /** * Get compound. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return bool */ public function get_compound( $context = 'view' ) { return $this->get_prop( 'compound', $context ); } /** * Is this a compound tax rate? * * @return boolean */ public function is_compound() { return $this->get_compound(); } /** * Get rate value * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return float */ public function get_rate_percent( $context = 'view' ) { return $this->get_prop( 'rate_percent', $context ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * O for ArrayAccess/Backwards compatibility. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { if ( 'tax_amount' === $offset ) { $offset = 'tax_total'; } elseif ( 'shipping_tax_amount' === $offset ) { $offset = 'shipping_tax_total'; } return parent::offsetGet( $offset ); } /** * OffsetSet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Tax::offsetSet', '4.4.0', '' ); if ( 'tax_amount' === $offset ) { $offset = 'tax_total'; } elseif ( 'shipping_tax_amount' === $offset ) { $offset = 'shipping_tax_total'; } parent::offsetSet( $offset, $value ); } /** * OffsetExists for ArrayAccess. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { if ( in_array( $offset, array( 'tax_amount', 'shipping_tax_amount' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } includes/class-wc-cart-session.php 0000644 00000035270 15132754524 0013226 0 ustar 00 <?php /** * Cart session handling class. * * @package WooCommerce\Classes * @version 3.2.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Cart_Session class. * * @since 3.2.0 */ final class WC_Cart_Session { /** * Reference to cart object. * * @since 3.2.0 * @var WC_Cart */ protected $cart; /** * Sets up the items provided, and calculate totals. * * @since 3.2.0 * @throws Exception If missing WC_Cart object. * * @param WC_Cart $cart Cart object to calculate totals for. */ public function __construct( &$cart ) { if ( ! is_a( $cart, 'WC_Cart' ) ) { throw new Exception( 'A valid WC_Cart object is required' ); } $this->cart = $cart; } /** * Register methods for this object on the appropriate WordPress hooks. */ public function init() { add_action( 'wp_loaded', array( $this, 'get_cart_from_session' ) ); add_action( 'woocommerce_cart_emptied', array( $this, 'destroy_cart_session' ) ); add_action( 'woocommerce_after_calculate_totals', array( $this, 'set_session' ) ); add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'set_session' ) ); add_action( 'woocommerce_removed_coupon', array( $this, 'set_session' ) ); // Persistent cart stored to usermeta. add_action( 'woocommerce_add_to_cart', array( $this, 'persistent_cart_update' ) ); add_action( 'woocommerce_cart_item_removed', array( $this, 'persistent_cart_update' ) ); add_action( 'woocommerce_cart_item_restored', array( $this, 'persistent_cart_update' ) ); add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'persistent_cart_update' ) ); // Cookie events - cart cookies need to be set before headers are sent. add_action( 'woocommerce_add_to_cart', array( $this, 'maybe_set_cart_cookies' ) ); add_action( 'wp', array( $this, 'maybe_set_cart_cookies' ), 99 ); add_action( 'shutdown', array( $this, 'maybe_set_cart_cookies' ), 0 ); } /** * Get the cart data from the PHP session and store it in class variables. * * @since 3.2.0 */ public function get_cart_from_session() { do_action( 'woocommerce_load_cart_from_session' ); $this->cart->set_totals( WC()->session->get( 'cart_totals', null ) ); $this->cart->set_applied_coupons( WC()->session->get( 'applied_coupons', array() ) ); $this->cart->set_coupon_discount_totals( WC()->session->get( 'coupon_discount_totals', array() ) ); $this->cart->set_coupon_discount_tax_totals( WC()->session->get( 'coupon_discount_tax_totals', array() ) ); $this->cart->set_removed_cart_contents( WC()->session->get( 'removed_cart_contents', array() ) ); $update_cart_session = false; // Flag to indicate the stored cart should be updated. $order_again = false; // Flag to indicate whether this is a re-order. $cart = WC()->session->get( 'cart', null ); $merge_saved_cart = (bool) get_user_meta( get_current_user_id(), '_woocommerce_load_saved_cart_after_login', true ); // Merge saved cart with current cart. if ( is_null( $cart ) || $merge_saved_cart ) { $saved_cart = $this->get_saved_cart(); $cart = is_null( $cart ) ? array() : $cart; $cart = array_merge( $saved_cart, $cart ); $update_cart_session = true; delete_user_meta( get_current_user_id(), '_woocommerce_load_saved_cart_after_login' ); } // Populate cart from order. if ( isset( $_GET['order_again'], $_GET['_wpnonce'] ) && is_user_logged_in() && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-order_again' ) ) { // WPCS: input var ok, sanitization ok. $cart = $this->populate_cart_from_order( absint( $_GET['order_again'] ), $cart ); // WPCS: input var ok. $order_again = true; } // Prime caches to reduce future queries. if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( wp_list_pluck( $cart, 'product_id' ) ); } $cart_contents = array(); foreach ( $cart as $key => $values ) { if ( ! is_customize_preview() && 'customize-preview' === $key ) { continue; } $product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] ); if ( empty( $product ) || ! $product->exists() || 0 >= $values['quantity'] ) { continue; } /** * Allow 3rd parties to validate this item before it's added to cart and add their own notices. * * @since 3.6.0 * * @param bool $remove_cart_item_from_session If true, the item will not be added to the cart. Default: false. * @param string $key Cart item key. * @param array $values Cart item values e.g. quantity and product_id. */ if ( apply_filters( 'woocommerce_pre_remove_cart_item_from_session', false, $key, $values ) ) { $update_cart_session = true; do_action( 'woocommerce_remove_cart_item_from_session', $key, $values ); } elseif ( ! $product->is_purchasable() ) { $update_cart_session = true; /* translators: %s: product name */ $message = sprintf( __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce' ), $product->get_name() ); /** * Filter message about item removed from the cart. * * @since 3.8.0 * @param string $message Message. * @param WC_Product $product Product data. */ $message = apply_filters( 'woocommerce_cart_item_removed_message', $message, $product ); wc_add_notice( $message, 'error' ); do_action( 'woocommerce_remove_cart_item_from_session', $key, $values ); } elseif ( ! empty( $values['data_hash'] ) && ! hash_equals( $values['data_hash'], wc_get_cart_item_data_hash( $product ) ) ) { // phpcs:ignore PHPCompatibility.PHP.NewFunctions.hash_equalsFound $update_cart_session = true; /* translators: %1$s: product name. %2$s product permalink */ wc_add_notice( sprintf( __( '%1$s has been removed from your cart because it has since been modified. You can add it back to your cart <a href="%2$s">here</a>.', 'woocommerce' ), $product->get_name(), $product->get_permalink() ), 'notice' ); do_action( 'woocommerce_remove_cart_item_from_session', $key, $values ); } else { // Put session data into array. Run through filter so other plugins can load their own session data. $session_data = array_merge( $values, array( 'data' => $product, ) ); $cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key ); // Add to cart right away so the product is visible in woocommerce_get_cart_item_from_session hook. $this->cart->set_cart_contents( $cart_contents ); } } // If it's not empty, it's been already populated by the loop above. if ( ! empty( $cart_contents ) ) { $this->cart->set_cart_contents( apply_filters( 'woocommerce_cart_contents_changed', $cart_contents ) ); } do_action( 'woocommerce_cart_loaded_from_session', $this->cart ); if ( $update_cart_session || is_null( WC()->session->get( 'cart_totals', null ) ) ) { WC()->session->set( 'cart', $this->get_cart_for_session() ); $this->cart->calculate_totals(); if ( $merge_saved_cart ) { $this->persistent_cart_update(); } } // If this is a re-order, redirect to the cart page to get rid of the `order_again` query string. if ( $order_again ) { wp_safe_redirect( wc_get_cart_url() ); exit; } } /** * Destroy cart session data. * * @since 3.2.0 */ public function destroy_cart_session() { WC()->session->set( 'cart', null ); WC()->session->set( 'cart_totals', null ); WC()->session->set( 'applied_coupons', null ); WC()->session->set( 'coupon_discount_totals', null ); WC()->session->set( 'coupon_discount_tax_totals', null ); WC()->session->set( 'removed_cart_contents', null ); WC()->session->set( 'order_awaiting_payment', null ); } /** * Will set cart cookies if needed and when possible. * * @since 3.2.0 */ public function maybe_set_cart_cookies() { if ( ! headers_sent() && did_action( 'wp_loaded' ) ) { if ( ! $this->cart->is_empty() ) { $this->set_cart_cookies( true ); } elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) { // WPCS: input var ok. $this->set_cart_cookies( false ); } } } /** * Sets the php session data for the cart and coupons. */ public function set_session() { WC()->session->set( 'cart', $this->get_cart_for_session() ); WC()->session->set( 'cart_totals', $this->cart->get_totals() ); WC()->session->set( 'applied_coupons', $this->cart->get_applied_coupons() ); WC()->session->set( 'coupon_discount_totals', $this->cart->get_coupon_discount_totals() ); WC()->session->set( 'coupon_discount_tax_totals', $this->cart->get_coupon_discount_tax_totals() ); WC()->session->set( 'removed_cart_contents', $this->cart->get_removed_cart_contents() ); do_action( 'woocommerce_cart_updated' ); } /** * Returns the contents of the cart in an array without the 'data' element. * * @return array contents of the cart */ public function get_cart_for_session() { $cart_session = array(); foreach ( $this->cart->get_cart() as $key => $values ) { $cart_session[ $key ] = $values; unset( $cart_session[ $key ]['data'] ); // Unset product object. } return $cart_session; } /** * Save the persistent cart when the cart is updated. */ public function persistent_cart_update() { if ( get_current_user_id() && apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { update_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id(), array( 'cart' => $this->get_cart_for_session(), ) ); } } /** * Delete the persistent cart permanently. */ public function persistent_cart_destroy() { if ( get_current_user_id() && apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { delete_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id() ); } } /** * Set cart hash cookie and items in cart if not already set. * * @param bool $set Should cookies be set (true) or unset. */ private function set_cart_cookies( $set = true ) { if ( $set ) { $setcookies = array( 'woocommerce_items_in_cart' => '1', 'woocommerce_cart_hash' => WC()->cart->get_cart_hash(), ); foreach ( $setcookies as $name => $value ) { if ( ! isset( $_COOKIE[ $name ] ) || $_COOKIE[ $name ] !== $value ) { wc_setcookie( $name, $value ); } } } else { $unsetcookies = array( 'woocommerce_items_in_cart', 'woocommerce_cart_hash', ); foreach ( $unsetcookies as $name ) { if ( isset( $_COOKIE[ $name ] ) ) { wc_setcookie( $name, 0, time() - HOUR_IN_SECONDS ); unset( $_COOKIE[ $name ] ); } } } do_action( 'woocommerce_set_cart_cookies', $set ); } /** * Get the persistent cart from the database. * * @since 3.5.0 * @return array */ private function get_saved_cart() { $saved_cart = array(); if ( apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { $saved_cart_meta = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); if ( isset( $saved_cart_meta['cart'] ) ) { $saved_cart = array_filter( (array) $saved_cart_meta['cart'] ); } } return $saved_cart; } /** * Get a cart from an order, if user has permission. * * @since 3.5.0 * * @param int $order_id Order ID to try to load. * @param array $cart Current cart array. * * @return array */ private function populate_cart_from_order( $order_id, $cart ) { $order = wc_get_order( $order_id ); if ( ! $order->get_id() || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! current_user_can( 'order_again', $order->get_id() ) ) { return; } if ( apply_filters( 'woocommerce_empty_cart_when_order_again', true ) ) { $cart = array(); } $inital_cart_size = count( $cart ); $order_items = $order->get_items(); foreach ( $order_items as $item ) { $product_id = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $item->get_product_id() ); $quantity = $item->get_quantity(); $variation_id = (int) $item->get_variation_id(); $variations = array(); $cart_item_data = apply_filters( 'woocommerce_order_again_cart_item_data', array(), $item, $order ); $product = $item->get_product(); if ( ! $product ) { continue; } // Prevent reordering variable products if no selected variation. if ( ! $variation_id && $product->is_type( 'variable' ) ) { continue; } // Prevent reordering items specifically out of stock. if ( ! $product->is_in_stock() ) { continue; } foreach ( $item->get_meta_data() as $meta ) { if ( taxonomy_is_product_attribute( $meta->key ) ) { $term = get_term_by( 'slug', $meta->value, $meta->key ); $variations[ $meta->key ] = $term ? $term->name : $meta->value; } elseif ( meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) { $variations[ $meta->key ] = $meta->value; } } if ( ! apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations, $cart_item_data ) ) { continue; } // Add to cart directly. $cart_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data ); $product_data = wc_get_product( $variation_id ? $variation_id : $product_id ); $cart[ $cart_id ] = apply_filters( 'woocommerce_add_order_again_cart_item', array_merge( $cart_item_data, array( 'key' => $cart_id, 'product_id' => $product_id, 'variation_id' => $variation_id, 'variation' => $variations, 'quantity' => $quantity, 'data' => $product_data, 'data_hash' => wc_get_cart_item_data_hash( $product_data ), ) ), $cart_id ); } do_action_ref_array( 'woocommerce_ordered_again', array( $order->get_id(), $order_items, &$cart ) ); $num_items_in_cart = count( $cart ); $num_items_in_original_order = count( $order_items ); $num_items_added = $num_items_in_cart - $inital_cart_size; if ( $num_items_in_original_order > $num_items_added ) { wc_add_notice( sprintf( /* translators: %d item count */ _n( '%d item from your previous order is currently unavailable and could not be added to your cart.', '%d items from your previous order are currently unavailable and could not be added to your cart.', $num_items_in_original_order - $num_items_added, 'woocommerce' ), $num_items_in_original_order - $num_items_added ), 'error' ); } if ( 0 < $num_items_added ) { wc_add_notice( __( 'The cart has been filled with the items from your previous order.', 'woocommerce' ) ); } return $cart; } } includes/class-wc-order.php 0000644 00000173322 15132754524 0011730 0 ustar 00 <?php /** * Regular order * * @package WooCommerce\Classes * @version 2.2.0 */ defined( 'ABSPATH' ) || exit; /** * Order Class. * * These are regular WooCommerce orders, which extend the abstract order class. */ class WC_Order extends WC_Abstract_Order { /** * Stores data about status changes so relevant hooks can be fired. * * @var bool|array */ protected $status_transition = false; /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $data = array( // Abstract order props. 'parent_id' => 0, 'status' => '', 'currency' => '', 'version' => '', 'prices_include_tax' => false, 'date_created' => null, 'date_modified' => null, 'discount_total' => 0, 'discount_tax' => 0, 'shipping_total' => 0, 'shipping_tax' => 0, 'cart_tax' => 0, 'total' => 0, 'total_tax' => 0, // Order props. 'customer_id' => 0, 'order_key' => '', 'billing' => array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', 'email' => '', 'phone' => '', ), 'shipping' => array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', 'phone' => '', ), 'payment_method' => '', 'payment_method_title' => '', 'transaction_id' => '', 'customer_ip_address' => '', 'customer_user_agent' => '', 'created_via' => '', 'customer_note' => '', 'date_completed' => null, 'date_paid' => null, 'cart_hash' => '', ); /** * When a payment is complete this function is called. * * Most of the time this should mark an order as 'processing' so that admin can process/post the items. * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. * Stock levels are reduced at this point. * Sales are also recorded for products. * Finally, record the date of payment. * * @param string $transaction_id Optional transaction id to store in post meta. * @return bool success */ public function payment_complete( $transaction_id = '' ) { if ( ! $this->get_id() ) { // Order must exist. return false; } try { do_action( 'woocommerce_pre_payment_complete', $this->get_id() ); if ( WC()->session ) { WC()->session->set( 'order_awaiting_payment', false ); } if ( $this->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $this ) ) ) { if ( ! empty( $transaction_id ) ) { $this->set_transaction_id( $transaction_id ); } if ( ! $this->get_date_paid( 'edit' ) ) { $this->set_date_paid( time() ); } $this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ); $this->save(); do_action( 'woocommerce_payment_complete', $this->get_id() ); } else { do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() ); } } catch ( Exception $e ) { /** * If there was an error completing the payment, log to a file and add an order note so the admin can take action. */ $logger = wc_get_logger(); $logger->error( sprintf( 'Error completing payment for order #%d', $this->get_id() ), array( 'order' => $this, 'error' => $e, ) ); $this->add_order_note( __( 'Payment complete event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); return false; } return true; } /** * Gets order total - formatted for display. * * @param string $tax_display Type of tax display. * @param bool $display_refunded If should include refunded value. * * @return string */ public function get_formatted_order_total( $tax_display = '', $display_refunded = true ) { $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); $order_total = $this->get_total(); $total_refunded = $this->get_total_refunded(); $tax_string = ''; // Tax for inclusive prices. if ( wc_tax_enabled() && 'incl' === $tax_display ) { $tax_string_array = array(); $tax_totals = $this->get_tax_totals(); if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { foreach ( $tax_totals as $code => $tax ) { $tax_amount = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount; $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); } } elseif ( ! empty( $tax_totals ) ) { $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); $tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() ); } if ( ! empty( $tax_string_array ) ) { /* translators: %s: taxes */ $tax_string = ' <small class="includes_tax">' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) . '</small>'; } } if ( $total_refunded && $display_refunded ) { $formatted_total = '<del aria-hidden="true">' . wp_strip_all_tags( $formatted_total ) . '</del> <ins>' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . '</ins>'; } else { $formatted_total .= $tax_string; } /** * Filter WooCommerce formatted order total. * * @param string $formatted_total Total to display. * @param WC_Order $order Order data. * @param string $tax_display Type of tax display. * @param bool $display_refunded If should include refunded value. */ return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); } /* |-------------------------------------------------------------------------- | CRUD methods |-------------------------------------------------------------------------- | | Methods which create, read, update and delete orders from the database. | Written in abstract fashion so that the way orders are stored can be | changed more easily in the future. | | A save method is included for convenience (chooses update or create based | on if the order exists yet). | */ /** * Save data to the database. * * @since 3.0.0 * @return int order ID */ public function save() { $this->maybe_set_user_billing_email(); parent::save(); $this->status_transition(); return $this->get_id(); } /** * Log an error about this order is exception is encountered. * * @param Exception $e Exception object. * @param string $message Message regarding exception thrown. * @since 3.7.0 */ protected function handle_exception( $e, $message = 'Error' ) { wc_get_logger()->error( $message, array( 'order' => $this, 'error' => $e, ) ); $this->add_order_note( $message . ' ' . $e->getMessage() ); } /** * Set order status. * * @since 3.0.0 * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @param string $note Optional note to add. * @param bool $manual_update Is this a manual order status change?. * @return array */ public function set_status( $new_status, $note = '', $manual_update = false ) { $result = parent::set_status( $new_status ); if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { $this->status_transition = array( 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], 'to' => $result['to'], 'note' => $note, 'manual' => (bool) $manual_update, ); if ( $manual_update ) { do_action( 'woocommerce_order_edit_status', $this->get_id(), $result['to'] ); } $this->maybe_set_date_paid(); $this->maybe_set_date_completed(); } return $result; } /** * Maybe set date paid. * * Sets the date paid variable when transitioning to the payment complete * order status. This is either processing or completed. This is not filtered * to avoid infinite loops e.g. if loading an order via the filter. * * Date paid is set once in this manner - only when it is not already set. * This ensures the data exists even if a gateway does not use the * `payment_complete` method. * * @since 3.0.0 */ public function maybe_set_date_paid() { // This logic only runs if the date_paid prop has not been set yet. if ( ! $this->get_date_paid( 'edit' ) ) { $payment_completed_status = apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ); if ( $this->has_status( $payment_completed_status ) ) { // If payment complete status is reached, set paid now. $this->set_date_paid( time() ); } elseif ( 'processing' === $payment_completed_status && $this->has_status( 'completed' ) ) { // If payment complete status was processing, but we've passed that and still have no date, set it now. $this->set_date_paid( time() ); } } } /** * Maybe set date completed. * * Sets the date completed variable when transitioning to completed status. * * @since 3.0.0 */ protected function maybe_set_date_completed() { if ( $this->has_status( 'completed' ) ) { $this->set_date_completed( time() ); } } /** * Updates status of order immediately. * * @uses WC_Order::set_status() * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @param string $note Optional note to add. * @param bool $manual Is this a manual order status change?. * @return bool */ public function update_status( $new_status, $note = '', $manual = false ) { if ( ! $this->get_id() ) { // Order must exist. return false; } try { $this->set_status( $new_status, $note, $manual ); $this->save(); } catch ( Exception $e ) { $logger = wc_get_logger(); $logger->error( sprintf( 'Error updating status for order #%d', $this->get_id() ), array( 'order' => $this, 'error' => $e, ) ); $this->add_order_note( __( 'Update status event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); return false; } return true; } /** * Handle the status transition. */ protected function status_transition() { $status_transition = $this->status_transition; // Reset status transition variable. $this->status_transition = false; if ( $status_transition ) { try { do_action( 'woocommerce_order_status_' . $status_transition['to'], $this->get_id(), $this ); if ( ! empty( $status_transition['from'] ) ) { /* translators: 1: old order status 2: new order status */ $transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['from'] ), wc_get_order_status_name( $status_transition['to'] ) ); // Note the transition occurred. $this->add_status_transition_note( $transition_note, $status_transition ); do_action( 'woocommerce_order_status_' . $status_transition['from'] . '_to_' . $status_transition['to'], $this->get_id(), $this ); do_action( 'woocommerce_order_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this ); // Work out if this was for a payment, and trigger a payment_status hook instead. if ( in_array( $status_transition['from'], apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ), true ) && in_array( $status_transition['to'], wc_get_is_paid_statuses(), true ) ) { /** * Fires when the order progresses from a pending payment status to a paid one. * * @since 3.9.0 * @param int Order ID * @param WC_Order Order object */ do_action( 'woocommerce_order_payment_status_changed', $this->get_id(), $this ); } } else { /* translators: %s: new order status */ $transition_note = sprintf( __( 'Order status set to %s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['to'] ) ); // Note the transition occurred. $this->add_status_transition_note( $transition_note, $status_transition ); } } catch ( Exception $e ) { $logger = wc_get_logger(); $logger->error( sprintf( 'Status transition of order #%d errored!', $this->get_id() ), array( 'order' => $this, 'error' => $e, ) ); $this->add_order_note( __( 'Error during status transition.', 'woocommerce' ) . ' ' . $e->getMessage() ); } } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- | | Methods for getting data from the order object. | */ /** * Get basic order data in array format. * * @return array */ public function get_base_data() { return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'number' => $this->get_order_number() ) ); } /** * Get all class data in array format. * * @since 3.0.0 * @return array */ public function get_data() { return array_merge( $this->get_base_data(), array( 'meta_data' => $this->get_meta_data(), 'line_items' => $this->get_items( 'line_item' ), 'tax_lines' => $this->get_items( 'tax' ), 'shipping_lines' => $this->get_items( 'shipping' ), 'fee_lines' => $this->get_items( 'fee' ), 'coupon_lines' => $this->get_items( 'coupon' ), ) ); } /** * Expands the shipping and billing information in the changes array. */ public function get_changes() { $changed_props = parent::get_changes(); $subs = array( 'shipping', 'billing' ); foreach ( $subs as $sub ) { if ( ! empty( $changed_props[ $sub ] ) ) { foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { $changed_props[ $sub . '_' . $sub_prop ] = $value; } } } if ( isset( $changed_props['customer_note'] ) ) { $changed_props['post_excerpt'] = $changed_props['customer_note']; } return $changed_props; } /** * Gets the order number for display (by default, order ID). * * @return string */ public function get_order_number() { return (string) apply_filters( 'woocommerce_order_number', $this->get_id(), $this ); } /** * Get order key. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_order_key( $context = 'view' ) { return $this->get_prop( 'order_key', $context ); } /** * Get customer_id. * * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_customer_id( $context = 'view' ) { return $this->get_prop( 'customer_id', $context ); } /** * Alias for get_customer_id(). * * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_user_id( $context = 'view' ) { return $this->get_customer_id( $context ); } /** * Get the user associated with the order. False for guests. * * @return WP_User|false */ public function get_user() { return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false; } /** * Gets a prop for a getter method. * * @since 3.0.0 * @param string $prop Name of prop to get. * @param string $address billing or shipping. * @param string $context What the value is for. Valid values are view and edit. * @return mixed */ protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { $value = null; if ( array_key_exists( $prop, $this->data[ $address ] ) ) { $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); } } return $value; } /** * Get billing first name. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_first_name( $context = 'view' ) { return $this->get_address_prop( 'first_name', 'billing', $context ); } /** * Get billing last name. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_last_name( $context = 'view' ) { return $this->get_address_prop( 'last_name', 'billing', $context ); } /** * Get billing company. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_company( $context = 'view' ) { return $this->get_address_prop( 'company', 'billing', $context ); } /** * Get billing address line 1. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_address_1( $context = 'view' ) { return $this->get_address_prop( 'address_1', 'billing', $context ); } /** * Get billing address line 2. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_address_2( $context = 'view' ) { return $this->get_address_prop( 'address_2', 'billing', $context ); } /** * Get billing city. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_city( $context = 'view' ) { return $this->get_address_prop( 'city', 'billing', $context ); } /** * Get billing state. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_state( $context = 'view' ) { return $this->get_address_prop( 'state', 'billing', $context ); } /** * Get billing postcode. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_postcode( $context = 'view' ) { return $this->get_address_prop( 'postcode', 'billing', $context ); } /** * Get billing country. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_country( $context = 'view' ) { return $this->get_address_prop( 'country', 'billing', $context ); } /** * Get billing email. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_email( $context = 'view' ) { return $this->get_address_prop( 'email', 'billing', $context ); } /** * Get billing phone. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_billing_phone( $context = 'view' ) { return $this->get_address_prop( 'phone', 'billing', $context ); } /** * Get shipping first name. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_first_name( $context = 'view' ) { return $this->get_address_prop( 'first_name', 'shipping', $context ); } /** * Get shipping_last_name. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_last_name( $context = 'view' ) { return $this->get_address_prop( 'last_name', 'shipping', $context ); } /** * Get shipping company. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_company( $context = 'view' ) { return $this->get_address_prop( 'company', 'shipping', $context ); } /** * Get shipping address line 1. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_address_1( $context = 'view' ) { return $this->get_address_prop( 'address_1', 'shipping', $context ); } /** * Get shipping address line 2. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_address_2( $context = 'view' ) { return $this->get_address_prop( 'address_2', 'shipping', $context ); } /** * Get shipping city. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_city( $context = 'view' ) { return $this->get_address_prop( 'city', 'shipping', $context ); } /** * Get shipping state. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_state( $context = 'view' ) { return $this->get_address_prop( 'state', 'shipping', $context ); } /** * Get shipping postcode. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_postcode( $context = 'view' ) { return $this->get_address_prop( 'postcode', 'shipping', $context ); } /** * Get shipping country. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_country( $context = 'view' ) { return $this->get_address_prop( 'country', 'shipping', $context ); } /** * Get shipping phone. * * @since 5.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_shipping_phone( $context = 'view' ) { return $this->get_address_prop( 'phone', 'shipping', $context ); } /** * Get the payment method. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_payment_method( $context = 'view' ) { return $this->get_prop( 'payment_method', $context ); } /** * Get payment method title. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_payment_method_title( $context = 'view' ) { return $this->get_prop( 'payment_method_title', $context ); } /** * Get transaction d. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_transaction_id( $context = 'view' ) { return $this->get_prop( 'transaction_id', $context ); } /** * Get customer ip address. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_customer_ip_address( $context = 'view' ) { return $this->get_prop( 'customer_ip_address', $context ); } /** * Get customer user agent. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_customer_user_agent( $context = 'view' ) { return $this->get_prop( 'customer_user_agent', $context ); } /** * Get created via. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_created_via( $context = 'view' ) { return $this->get_prop( 'created_via', $context ); } /** * Get customer note. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_customer_note( $context = 'view' ) { return $this->get_prop( 'customer_note', $context ); } /** * Get date completed. * * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_completed( $context = 'view' ) { return $this->get_prop( 'date_completed', $context ); } /** * Get date paid. * * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_paid( $context = 'view' ) { $date_paid = $this->get_prop( 'date_paid', $context ); if ( 'view' === $context && ! $date_paid && version_compare( $this->get_version( 'edit' ), '3.0', '<' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) { // In view context, return a date if missing. $date_paid = $this->get_date_created( 'edit' ); } return $date_paid; } /** * Get cart hash. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_cart_hash( $context = 'view' ) { return $this->get_prop( 'cart_hash', $context ); } /** * Returns the requested address in raw, non-formatted way. * Note: Merges raw data with get_prop data so changes are returned too. * * @since 2.4.0 * @param string $type Billing or shipping. Anything else besides 'billing' will return shipping address. * @return array The stored address after filter. */ public function get_address( $type = 'billing' ) { return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this ); } /** * Get a formatted shipping address for the order. * * @return string */ public function get_shipping_address_map_url() { $address = $this->get_address( 'shipping' ); // Remove name and company before generate the Google Maps URL. unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] ); $address = apply_filters( 'woocommerce_shipping_address_map_url_parts', $address, $this ); return apply_filters( 'woocommerce_shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this ); } /** * Get a formatted billing full name. * * @return string */ public function get_formatted_billing_full_name() { /* translators: 1: first name 2: last name */ return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); } /** * Get a formatted shipping full name. * * @return string */ public function get_formatted_shipping_full_name() { /* translators: 1: first name 2: last name */ return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); } /** * Get a formatted billing address for the order. * * @param string $empty_content Content to show if no address is present. @since 3.3.0. * @return string */ public function get_formatted_billing_address( $empty_content = '' ) { $raw_address = apply_filters( 'woocommerce_order_formatted_billing_address', $this->get_address( 'billing' ), $this ); $address = WC()->countries->get_formatted_address( $raw_address ); /** * Filter orders formatted billing address. * * @since 3.8.0 * @param string $address Formatted billing address string. * @param array $raw_address Raw billing address. * @param WC_Order $order Order data. @since 3.9.0 */ return apply_filters( 'woocommerce_order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this ); } /** * Get a formatted shipping address for the order. * * @param string $empty_content Content to show if no address is present. @since 3.3.0. * @return string */ public function get_formatted_shipping_address( $empty_content = '' ) { $address = ''; $raw_address = $this->get_address( 'shipping' ); if ( $this->has_shipping_address() ) { $raw_address = apply_filters( 'woocommerce_order_formatted_shipping_address', $raw_address, $this ); $address = WC()->countries->get_formatted_address( $raw_address ); } /** * Filter orders formatted shipping address. * * @since 3.8.0 * @param string $address Formatted billing address string. * @param array $raw_address Raw billing address. * @param WC_Order $order Order data. @since 3.9.0 */ return apply_filters( 'woocommerce_order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); } /** * Returns true if the order has a billing address. * * @since 3.0.4 * @return boolean */ public function has_billing_address() { return $this->get_billing_address_1() || $this->get_billing_address_2(); } /** * Returns true if the order has a shipping address. * * @since 3.0.4 * @return boolean */ public function has_shipping_address() { return $this->get_shipping_address_1() || $this->get_shipping_address_2(); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting order data. These should not update anything in the | database itself and should only change what is stored in the class | object. However, for backwards compatibility pre 3.0.0 some of these | setters may handle both. | */ /** * Sets a prop for a setter method. * * @since 3.0.0 * @param string $prop Name of prop to set. * @param string $address Name of address to set. billing or shipping. * @param mixed $value Value of the prop. */ protected function set_address_prop( $prop, $address, $value ) { if ( array_key_exists( $prop, $this->data[ $address ] ) ) { if ( true === $this->object_read ) { if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { $this->changes[ $address ][ $prop ] = $value; } } else { $this->data[ $address ][ $prop ] = $value; } } } /** * Set order key. * * @param string $value Max length 22 chars. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_order_key( $value ) { $this->set_prop( 'order_key', substr( $value, 0, 22 ) ); } /** * Set customer id. * * @param int $value Customer ID. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_customer_id( $value ) { $this->set_prop( 'customer_id', absint( $value ) ); } /** * Set billing first name. * * @param string $value Billing first name. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_first_name( $value ) { $this->set_address_prop( 'first_name', 'billing', $value ); } /** * Set billing last name. * * @param string $value Billing last name. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_last_name( $value ) { $this->set_address_prop( 'last_name', 'billing', $value ); } /** * Set billing company. * * @param string $value Billing company. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_company( $value ) { $this->set_address_prop( 'company', 'billing', $value ); } /** * Set billing address line 1. * * @param string $value Billing address line 1. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_address_1( $value ) { $this->set_address_prop( 'address_1', 'billing', $value ); } /** * Set billing address line 2. * * @param string $value Billing address line 2. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_address_2( $value ) { $this->set_address_prop( 'address_2', 'billing', $value ); } /** * Set billing city. * * @param string $value Billing city. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_city( $value ) { $this->set_address_prop( 'city', 'billing', $value ); } /** * Set billing state. * * @param string $value Billing state. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_state( $value ) { $this->set_address_prop( 'state', 'billing', $value ); } /** * Set billing postcode. * * @param string $value Billing postcode. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_postcode( $value ) { $this->set_address_prop( 'postcode', 'billing', $value ); } /** * Set billing country. * * @param string $value Billing country. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_country( $value ) { $this->set_address_prop( 'country', 'billing', $value ); } /** * Maybe set empty billing email to that of the user who owns the order. */ protected function maybe_set_user_billing_email() { $user = $this->get_user(); if ( ! $this->get_billing_email() && $user ) { try { $this->set_billing_email( $user->user_email ); } catch ( WC_Data_Exception $e ) { unset( $e ); } } } /** * Set billing email. * * @param string $value Billing email. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_email( $value ) { if ( $value && ! is_email( $value ) ) { $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); } $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); } /** * Set billing phone. * * @param string $value Billing phone. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_billing_phone( $value ) { $this->set_address_prop( 'phone', 'billing', $value ); } /** * Set shipping first name. * * @param string $value Shipping first name. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_first_name( $value ) { $this->set_address_prop( 'first_name', 'shipping', $value ); } /** * Set shipping last name. * * @param string $value Shipping last name. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_last_name( $value ) { $this->set_address_prop( 'last_name', 'shipping', $value ); } /** * Set shipping company. * * @param string $value Shipping company. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_company( $value ) { $this->set_address_prop( 'company', 'shipping', $value ); } /** * Set shipping address line 1. * * @param string $value Shipping address line 1. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_address_1( $value ) { $this->set_address_prop( 'address_1', 'shipping', $value ); } /** * Set shipping address line 2. * * @param string $value Shipping address line 2. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_address_2( $value ) { $this->set_address_prop( 'address_2', 'shipping', $value ); } /** * Set shipping city. * * @param string $value Shipping city. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_city( $value ) { $this->set_address_prop( 'city', 'shipping', $value ); } /** * Set shipping state. * * @param string $value Shipping state. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_state( $value ) { $this->set_address_prop( 'state', 'shipping', $value ); } /** * Set shipping postcode. * * @param string $value Shipping postcode. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_postcode( $value ) { $this->set_address_prop( 'postcode', 'shipping', $value ); } /** * Set shipping country. * * @param string $value Shipping country. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_country( $value ) { $this->set_address_prop( 'country', 'shipping', $value ); } /** * Set shipping phone. * * @since 5.6.0 * @param string $value Shipping phone. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_shipping_phone( $value ) { $this->set_address_prop( 'phone', 'shipping', $value ); } /** * Set the payment method. * * @param string $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_payment_method( $payment_method = '' ) { if ( is_object( $payment_method ) ) { $this->set_payment_method( $payment_method->id ); $this->set_payment_method_title( $payment_method->get_title() ); } elseif ( '' === $payment_method ) { $this->set_prop( 'payment_method', '' ); $this->set_prop( 'payment_method_title', '' ); } else { $this->set_prop( 'payment_method', $payment_method ); } } /** * Set payment method title. * * @param string $value Payment method title. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_payment_method_title( $value ) { $this->set_prop( 'payment_method_title', $value ); } /** * Set transaction id. * * @param string $value Transaction id. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_transaction_id( $value ) { $this->set_prop( 'transaction_id', $value ); } /** * Set customer ip address. * * @param string $value Customer ip address. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_customer_ip_address( $value ) { $this->set_prop( 'customer_ip_address', $value ); } /** * Set customer user agent. * * @param string $value Customer user agent. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_customer_user_agent( $value ) { $this->set_prop( 'customer_user_agent', $value ); } /** * Set created via. * * @param string $value Created via. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_created_via( $value ) { $this->set_prop( 'created_via', $value ); } /** * Set customer note. * * @param string $value Customer note. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_customer_note( $value ) { $this->set_prop( 'customer_note', $value ); } /** * Set date completed. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_date_completed( $date = null ) { $this->set_date_prop( 'date_completed', $date ); } /** * Set date paid. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_date_paid( $date = null ) { $this->set_date_prop( 'date_paid', $date ); } /** * Set cart hash. * * @param string $value Cart hash. * @throws WC_Data_Exception Throws exception when invalid data is found. */ public function set_cart_hash( $value ) { $this->set_prop( 'cart_hash', $value ); } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- | | Checks if a condition is true or false. | */ /** * Check if an order key is valid. * * @param string $key Order key. * @return bool */ public function key_is_valid( $key ) { return hash_equals( $this->get_order_key(), $key ); } /** * See if order matches cart_hash. * * @param string $cart_hash Cart hash. * @return bool */ public function has_cart_hash( $cart_hash = '' ) { return hash_equals( $this->get_cart_hash(), $cart_hash ); // @codingStandardsIgnoreLine } /** * Checks if an order can be edited, specifically for use on the Edit Order screen. * * @return bool */ public function is_editable() { return apply_filters( 'wc_order_is_editable', in_array( $this->get_status(), array( 'pending', 'on-hold', 'auto-draft' ), true ), $this ); } /** * Returns if an order has been paid for based on the order status. * * @since 2.5.0 * @return bool */ public function is_paid() { return apply_filters( 'woocommerce_order_is_paid', $this->has_status( wc_get_is_paid_statuses() ), $this ); } /** * Checks if product download is permitted. * * @return bool */ public function is_download_permitted() { return apply_filters( 'woocommerce_order_is_download_permitted', $this->has_status( 'completed' ) || ( 'yes' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) && $this->has_status( 'processing' ) ), $this ); } /** * Checks if an order needs display the shipping address, based on shipping method. * * @return bool */ public function needs_shipping_address() { if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { return false; } $hide = apply_filters( 'woocommerce_order_hide_shipping_address', array( 'local_pickup' ), $this ); $needs_address = false; foreach ( $this->get_shipping_methods() as $shipping_method ) { $shipping_method_id = $shipping_method->get_method_id(); if ( ! in_array( $shipping_method_id, $hide, true ) ) { $needs_address = true; break; } } return apply_filters( 'woocommerce_order_needs_shipping_address', $needs_address, $hide, $this ); } /** * Returns true if the order contains a downloadable product. * * @return bool */ public function has_downloadable_item() { foreach ( $this->get_items() as $item ) { if ( $item->is_type( 'line_item' ) ) { $product = $item->get_product(); if ( $product && $product->has_file() ) { return true; } } } return false; } /** * Get downloads from all line items for this order. * * @since 3.2.0 * @return array */ public function get_downloadable_items() { $downloads = array(); foreach ( $this->get_items() as $item ) { if ( ! is_object( $item ) ) { continue; } // Check item refunds. $refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) ); if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) { continue; } if ( $item->is_type( 'line_item' ) ) { $item_downloads = $item->get_item_downloads(); $product = $item->get_product(); if ( $product && $item_downloads ) { foreach ( $item_downloads as $file ) { $downloads[] = array( 'download_url' => $file['download_url'], 'download_id' => $file['id'], 'product_id' => $product->get_id(), 'product_name' => $product->get_name(), 'product_url' => $product->is_visible() ? $product->get_permalink() : '', // Since 3.3.0. 'download_name' => $file['name'], 'order_id' => $this->get_id(), 'order_key' => $this->get_order_key(), 'downloads_remaining' => $file['downloads_remaining'], 'access_expires' => $file['access_expires'], 'file' => array( 'name' => $file['name'], 'file' => $file['file'], ), ); } } } } return apply_filters( 'woocommerce_order_get_downloadable_items', $downloads, $this ); } /** * Checks if an order needs payment, based on status and order total. * * @return bool */ public function needs_payment() { $valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ); return apply_filters( 'woocommerce_order_needs_payment', ( $this->has_status( $valid_order_statuses ) && $this->get_total() > 0 ), $this, $valid_order_statuses ); } /** * See if the order needs processing before it can be completed. * * Orders which only contain virtual, downloadable items do not need admin * intervention. * * Uses a transient so these calls are not repeated multiple times, and because * once the order is processed this code/transient does not need to persist. * * @since 3.0.0 * @return bool */ public function needs_processing() { $transient_name = 'wc_order_' . $this->get_id() . '_needs_processing'; $needs_processing = get_transient( $transient_name ); if ( false === $needs_processing ) { $needs_processing = 0; if ( count( $this->get_items() ) > 0 ) { foreach ( $this->get_items() as $item ) { if ( $item->is_type( 'line_item' ) ) { $product = $item->get_product(); if ( ! $product ) { continue; } $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { $needs_processing = 1; break; } } } } set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS ); } return 1 === absint( $needs_processing ); } /* |-------------------------------------------------------------------------- | URLs and Endpoints |-------------------------------------------------------------------------- */ /** * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices. * * @param bool $on_checkout If on checkout. * @return string */ public function get_checkout_payment_url( $on_checkout = false ) { $pay_url = wc_get_endpoint_url( 'order-pay', $this->get_id(), wc_get_checkout_url() ); if ( $on_checkout ) { $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); } else { $pay_url = add_query_arg( array( 'pay_for_order' => 'true', 'key' => $this->get_order_key(), ), $pay_url ); } return apply_filters( 'woocommerce_get_checkout_payment_url', $pay_url, $this ); } /** * Generates a URL for the thanks page (order received). * * @return string */ public function get_checkout_order_received_url() { $order_received_url = wc_get_endpoint_url( 'order-received', $this->get_id(), wc_get_checkout_url() ); $order_received_url = add_query_arg( 'key', $this->get_order_key(), $order_received_url ); return apply_filters( 'woocommerce_get_checkout_order_received_url', $order_received_url, $this ); } /** * Generates a URL so that a customer can cancel their (unpaid - pending) order. * * @param string $redirect Redirect URL. * @return string */ public function get_cancel_order_url( $redirect = '' ) { return apply_filters( 'woocommerce_get_cancel_order_url', wp_nonce_url( add_query_arg( array( 'cancel_order' => 'true', 'order' => $this->get_order_key(), 'order_id' => $this->get_id(), 'redirect' => $redirect, ), $this->get_cancel_endpoint() ), 'woocommerce-cancel_order' ) ); } /** * Generates a raw (unescaped) cancel-order URL for use by payment gateways. * * @param string $redirect Redirect URL. * @return string The unescaped cancel-order URL. */ public function get_cancel_order_url_raw( $redirect = '' ) { return apply_filters( 'woocommerce_get_cancel_order_url_raw', add_query_arg( array( 'cancel_order' => 'true', 'order' => $this->get_order_key(), 'order_id' => $this->get_id(), 'redirect' => $redirect, '_wpnonce' => wp_create_nonce( 'woocommerce-cancel_order' ), ), $this->get_cancel_endpoint() ) ); } /** * Helper method to return the cancel endpoint. * * @return string the cancel endpoint; either the cart page or the home page. */ public function get_cancel_endpoint() { $cancel_endpoint = wc_get_cart_url(); if ( ! $cancel_endpoint ) { $cancel_endpoint = home_url(); } if ( false === strpos( $cancel_endpoint, '?' ) ) { $cancel_endpoint = trailingslashit( $cancel_endpoint ); } return $cancel_endpoint; } /** * Generates a URL to view an order from the my account page. * * @return string */ public function get_view_order_url() { return apply_filters( 'woocommerce_get_view_order_url', wc_get_endpoint_url( 'view-order', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ), $this ); } /** * Get's the URL to edit the order in the backend. * * @since 3.3.0 * @return string */ public function get_edit_order_url() { return apply_filters( 'woocommerce_get_edit_order_url', get_admin_url( null, 'post.php?post=' . $this->get_id() . '&action=edit' ), $this ); } /* |-------------------------------------------------------------------------- | Order notes. |-------------------------------------------------------------------------- */ /** * Adds a note (comment) to the order. Order must exist. * * @param string $note Note to add. * @param int $is_customer_note Is this a note for the customer?. * @param bool $added_by_user Was the note added by a user?. * @return int Comment ID. */ public function add_order_note( $note, $is_customer_note = 0, $added_by_user = false ) { if ( ! $this->get_id() ) { return 0; } if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) { $user = get_user_by( 'id', get_current_user_id() ); $comment_author = $user->display_name; $comment_author_email = $user->user_email; } else { $comment_author = __( 'WooCommerce', 'woocommerce' ); $comment_author_email = strtolower( __( 'WooCommerce', 'woocommerce' ) ) . '@'; $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : 'noreply.com'; // WPCS: input var ok. $comment_author_email = sanitize_email( $comment_author_email ); } $commentdata = apply_filters( 'woocommerce_new_order_note_data', array( 'comment_post_ID' => $this->get_id(), 'comment_author' => $comment_author, 'comment_author_email' => $comment_author_email, 'comment_author_url' => '', 'comment_content' => $note, 'comment_agent' => 'WooCommerce', 'comment_type' => 'order_note', 'comment_parent' => 0, 'comment_approved' => 1, ), array( 'order_id' => $this->get_id(), 'is_customer_note' => $is_customer_note, ) ); $comment_id = wp_insert_comment( $commentdata ); if ( $is_customer_note ) { add_comment_meta( $comment_id, 'is_customer_note', 1 ); do_action( 'woocommerce_new_customer_note', array( 'order_id' => $this->get_id(), 'customer_note' => $commentdata['comment_content'], ) ); } /** * Action hook fired after an order note is added. * * @param int $order_note_id Order note ID. * @param WC_Order $order Order data. * * @since 4.4.0 */ do_action( 'woocommerce_order_note_added', $comment_id, $this ); return $comment_id; } /** * Add an order note for status transition * * @since 3.9.0 * @uses WC_Order::add_order_note() * @param string $note Note to be added giving status transition from and to details. * @param bool $transition Details of the status transition. * @return int Comment ID. */ private function add_status_transition_note( $note, $transition ) { return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] ); } /** * List order notes (public) for the customer. * * @return array */ public function get_customer_order_notes() { $notes = array(); $args = array( 'post_id' => $this->get_id(), 'approve' => 'approve', 'type' => '', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); $comments = get_comments( $args ); foreach ( $comments as $comment ) { if ( ! get_comment_meta( $comment->comment_ID, 'is_customer_note', true ) ) { continue; } $comment->comment_content = make_clickable( $comment->comment_content ); $notes[] = $comment; } add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); return $notes; } /* |-------------------------------------------------------------------------- | Refunds |-------------------------------------------------------------------------- */ /** * Get order refunds. * * @since 2.2 * @return array of WC_Order_Refund objects */ public function get_refunds() { $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } $this->refunds = wc_get_orders( array( 'type' => 'shop_order_refund', 'parent' => $this->get_id(), 'limit' => -1, ) ); wp_cache_set( $cache_key, $this->refunds, $this->cache_group ); return $this->refunds; } /** * Get amount already refunded. * * @since 2.2 * @return string */ public function get_total_refunded() { $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } $total_refunded = $this->data_store->get_total_refunded( $this ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Get the total tax refunded. * * @since 2.3 * @return float */ public function get_total_tax_refunded() { $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } $total_refunded = $this->data_store->get_total_tax_refunded( $this ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Get the total shipping refunded. * * @since 2.4 * @return float */ public function get_total_shipping_refunded() { $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } $total_refunded = $this->data_store->get_total_shipping_refunded( $this ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Gets the count of order items of a certain type that have been refunded. * * @since 2.4.0 * @param string $item_type Item type. * @return string */ public function get_item_count_refunded( $item_type = '' ) { if ( empty( $item_type ) ) { $item_type = array( 'line_item' ); } if ( ! is_array( $item_type ) ) { $item_type = array( $item_type ); } $count = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $count += abs( $refunded_item->get_quantity() ); } } return apply_filters( 'woocommerce_get_item_count_refunded', $count, $item_type, $this ); } /** * Get the total number of items refunded. * * @since 2.4.0 * * @param string $item_type Type of the item we're checking, if not a line_item. * @return int */ public function get_total_qty_refunded( $item_type = 'line_item' ) { $qty = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $qty += $refunded_item->get_quantity(); } } return $qty; } /** * Get the refunded amount for a line item. * * @param int $item_id ID of the item we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * @return int */ public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { $qty = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { $qty += $refunded_item->get_quantity(); } } } return $qty; } /** * Get the refunded amount for a line item. * * @param int $item_id ID of the item we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * @return int */ public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { $total += $refunded_item->get_total(); } } } return $total * -1; } /** * Get the refunded tax amount for a line item. * * @param int $item_id ID of the item we're checking. * @param int $tax_id ID of the tax we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * @return double */ public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); if ( $refunded_item_id === $item_id ) { $taxes = $refunded_item->get_taxes(); $total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0; break; } } } return wc_round_tax_total( $total ) * -1; } /** * Get total tax refunded by rate ID. * * @param int $rate_id Rate ID. * @return float */ public function get_total_tax_refunded_by_rate_id( $rate_id ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); } } } return $total; } /** * How much money is left to refund? * * @return string */ public function get_remaining_refund_amount() { return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() ); } /** * How many items are left to refund? * * @return int */ public function get_remaining_refund_items() { return absint( $this->get_item_count() - $this->get_item_count_refunded() ); } /** * Add total row for the payment method. * * @param array $total_rows Total rows. * @param string $tax_display Tax to display. */ protected function add_order_item_totals_payment_method_row( &$total_rows, $tax_display ) { if ( $this->get_total() > 0 && $this->get_payment_method_title() && 'other' !== $this->get_payment_method_title() ) { $total_rows['payment_method'] = array( 'label' => __( 'Payment method:', 'woocommerce' ), 'value' => $this->get_payment_method_title(), ); } } /** * Add total row for refunds. * * @param array $total_rows Total rows. * @param string $tax_display Tax to display. */ protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { $refunds = $this->get_refunds(); if ( $refunds ) { foreach ( $refunds as $id => $refund ) { $total_rows[ 'refund_' . $id ] = array( 'label' => $refund->get_reason() ? $refund->get_reason() : __( 'Refund', 'woocommerce' ) . ':', 'value' => wc_price( '-' . $refund->get_amount(), array( 'currency' => $this->get_currency() ) ), ); } } } /** * Get totals for display on pages and in emails. * * @param string $tax_display Tax to display. * @return array */ public function get_order_item_totals( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); $total_rows = array(); $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); $this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); $this->add_order_item_totals_total_row( $total_rows, $tax_display ); return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); } /** * Check if order has been created via admin, checkout, or in another way. * * @since 4.0.0 * @param string $modus Way of creating the order to test for. * @return bool */ public function is_created_via( $modus ) { return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); } } includes/class-wc-download-handler.php 0000644 00000060132 15132754524 0014031 0 ustar 00 <?php /** * Download handler * * Handle digital downloads. * * @package WooCommerce\Classes * @version 2.2.0 */ defined( 'ABSPATH' ) || exit; /** * Download handler class. */ class WC_Download_Handler { /** * Hook in methods. */ public static function init() { if ( isset( $_GET['download_file'], $_GET['order'] ) && ( isset( $_GET['email'] ) || isset( $_GET['uid'] ) ) ) { // WPCS: input var ok, CSRF ok. add_action( 'init', array( __CLASS__, 'download_product' ) ); } add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 ); add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 ); add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 ); } /** * Check if we need to download a file and check validity. */ public static function download_product() { $product_id = absint( $_GET['download_file'] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.VIP.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $product = wc_get_product( $product_id ); $data_store = WC_Data_Store::load( 'customer-download' ); if ( ! $product || empty( $_GET['key'] ) || empty( $_GET['order'] ) ) { // WPCS: input var ok, CSRF ok. self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); } // Fallback, accept email address if it's passed. if ( empty( $_GET['email'] ) && empty( $_GET['uid'] ) ) { // WPCS: input var ok, CSRF ok. self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); } $order_id = wc_get_order_id_by_order_key( wc_clean( wp_unslash( $_GET['order'] ) ) ); // WPCS: input var ok, CSRF ok. $order = wc_get_order( $order_id ); if ( isset( $_GET['email'] ) ) { // WPCS: input var ok, CSRF ok. $email_address = wp_unslash( $_GET['email'] ); // WPCS: input var ok, CSRF ok, sanitization ok. } else { // Get email address from order to verify hash. $email_address = is_a( $order, 'WC_Order' ) ? $order->get_billing_email() : null; // Prepare email address hash. $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address ); if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok. self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); } } $download_ids = $data_store->get_downloads( array( 'user_email' => sanitize_email( str_replace( ' ', '+', $email_address ) ), 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok. 'product_id' => $product_id, 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok. 'orderby' => 'downloads_remaining', 'order' => 'DESC', 'limit' => 1, 'return' => 'ids', ) ); if ( empty( $download_ids ) ) { self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); } $download = new WC_Customer_Download( current( $download_ids ) ); /** * Filter download filepath. * * @since 4.0.0 * @param string $file_path File path. * @param string $email_address Email address. * @param WC_Order|bool $order Order object or false. * @param WC_Product $product Product object. * @param WC_Customer_Download $download Download data. */ $file_path = apply_filters( 'woocommerce_download_product_filepath', $product->get_file_download_path( $download->get_download_id() ), $email_address, $order, $product, $download ); $parsed_file_path = self::parse_file_path( $file_path ); $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. self::check_order_is_valid( $download ); if ( ! $download_range['is_range_request'] ) { // If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices. self::check_downloads_remaining( $download ); } self::check_download_expiry( $download ); self::check_download_login_required( $download ); do_action( 'woocommerce_download_product', $download->get_user_email(), $download->get_order_key(), $download->get_product_id(), $download->get_user_id(), $download->get_download_id(), $download->get_order_id() ); $download->save(); // Track the download in logs and change remaining/counts. $current_user_id = get_current_user_id(); $ip_address = WC_Geolocation::get_ip_address(); if ( ! $download_range['is_range_request'] ) { $download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null ); } self::download( $file_path, $download->get_product_id() ); } /** * Check if an order is valid for downloading from. * * @param WC_Customer_Download $download Download instance. */ private static function check_order_is_valid( $download ) { if ( $download->get_order_id() ) { $order = wc_get_order( $download->get_order_id() ); if ( $order && ! $order->is_download_permitted() ) { self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); } } } /** * Check if there are downloads remaining. * * @param WC_Customer_Download $download Download instance. */ private static function check_downloads_remaining( $download ) { if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); } } /** * Check if the download has expired. * * @param WC_Customer_Download $download Download instance. */ private static function check_download_expiry( $download ) { if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', time() ) ) { self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); } } /** * Check if a download requires the user to login first. * * @param WC_Customer_Download $download Download instance. */ private static function check_download_login_required( $download ) { if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { if ( ! is_user_logged_in() ) { if ( wc_get_page_id( 'myaccount' ) ) { wp_safe_redirect( add_query_arg( 'wc_error', rawurlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); exit; } else { self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' <a href="' . esc_url( wp_login_url( wc_get_page_permalink( 'myaccount' ) ) ) . '" class="wc-forward">' . __( 'Login', 'woocommerce' ) . '</a>', __( 'Log in to Download Files', 'woocommerce' ), 403 ); } } elseif ( ! current_user_can( 'download_file', $download ) ) { self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); } } } /** * Count download. * * @deprecated 4.4.0 * @param array $download_data Download data. */ public static function count_download( $download_data ) { wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' ); } /** * Download a file - hook into init function. * * @param string $file_path URL to file. * @param integer $product_id Product ID of the product being downloaded. */ public static function download( $file_path, $product_id ) { if ( ! $file_path ) { self::download_error( __( 'No file defined', 'woocommerce' ) ); } $filename = basename( $file_path ); if ( strstr( $filename, '?' ) ) { $filename = current( explode( '?', $filename ) ); } $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); /** * Filter download method. * * @since 4.5.0 * @param string $method Download method. * @param int $product_id Product ID. * @param string $file_path URL to file. */ $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path ); // Add action to prevent issues in IE. add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); // Trigger download via one of the methods. do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); } /** * Redirect to a file to start the download. * * @param string $file_path File path. * @param string $filename File name. */ public static function download_file_redirect( $file_path, $filename = '' ) { header( 'Location: ' . $file_path ); exit; } /** * Parse file path and see if its remote or local. * * @param string $file_path File path. * @return array */ public static function parse_file_path( $file_path ) { $wp_uploads = wp_upload_dir(); $wp_uploads_dir = $wp_uploads['basedir']; $wp_uploads_url = $wp_uploads['baseurl']; /** * Replace uploads dir, site url etc with absolute counterparts if we can. * Note the str_replace on site_url is on purpose, so if https is forced * via filters we can still do the string replacement on a HTTP file. */ $replacements = array( $wp_uploads_url => $wp_uploads_dir, network_site_url( '/', 'https' ) => ABSPATH, str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH, site_url( '/', 'https' ) => ABSPATH, str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH, ); $count = 0; $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path ); $parsed_file_path = wp_parse_url( $file_path ); $remote_file = null === $count || 0 === $count; // Remote file only if there were no replacements. // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { return array( 'remote_file' => true, 'file_path' => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path, ); } // See if path needs an abspath prepended to work. if ( file_exists( ABSPATH . $file_path ) ) { $remote_file = false; $file_path = ABSPATH . $file_path; } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { $remote_file = false; $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); // Check if we have an absolute path. } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) ) { $remote_file = false; $file_path = $parsed_file_path['path']; } return array( 'remote_file' => $remote_file, 'file_path' => $file_path, ); } /** * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. * * @param string $file_path File path. * @param string $filename File name. */ public static function download_file_xsendfile( $file_path, $filename ) { $parsed_file_path = self::parse_file_path( $file_path ); /** * Fallback on force download method for remote files. This is because: * 1. xsendfile needs proxy configuration to work for remote files, which cannot be assumed to be available on most hosts. * 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini`. */ if ( $parsed_file_path['remote_file'] && ! apply_filters( 'woocommerce_use_xsendfile_for_remote', false ) ) { do_action( 'woocommerce_download_file_force', $file_path, $filename ); return; } if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) { self::download_headers( $parsed_file_path['file_path'], $filename ); $filepath = apply_filters( 'woocommerce_download_file_xsendfile_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); header( 'X-Sendfile: ' . $filepath ); exit; } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { self::download_headers( $parsed_file_path['file_path'], $filename ); $filepath = apply_filters( 'woocommerce_download_file_xsendfile_lighttpd_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); header( 'X-Lighttpd-Sendfile: ' . $filepath ); exit; } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { self::download_headers( $parsed_file_path['file_path'], $filename ); $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); $xsendfile_path = apply_filters( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path', $xsendfile_path, $file_path, $filename, $parsed_file_path ); header( "X-Accel-Redirect: /$xsendfile_path" ); exit; } // Fallback. wc_get_logger()->warning( sprintf( /* translators: %1$s contains the filepath of the digital asset. */ __( '%1$s could not be served using the X-Accel-Redirect/X-Sendfile method. A Force Download will be used instead.', 'woocommerce' ), $file_path ) ); self::download_file_force( $file_path, $filename ); } /** * Parse the HTTP_RANGE request from iOS devices. * Does not support multi-range requests. * * @param int $file_size Size of file in bytes. * @return array { * Information about range download request: beginning and length of * file chunk, whether the range is valid/supported and whether the request is a range request. * * @type int $start Byte offset of the beginning of the range. Default 0. * @type int $length Length of the requested file chunk in bytes. Optional. * @type bool $is_range_valid Whether the requested range is a valid and supported range. * @type bool $is_range_request Whether the request is a range request. * } */ protected static function get_download_range( $file_size ) { $start = 0; $download_range = array( 'start' => $start, 'is_range_valid' => false, 'is_range_request' => false, ); if ( ! $file_size ) { return $download_range; } $end = $file_size - 1; $download_range['length'] = $file_size; if ( isset( $_SERVER['HTTP_RANGE'] ) ) { // @codingStandardsIgnoreLine. $http_range = sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ); // WPCS: input var ok. $download_range['is_range_request'] = true; $c_start = $start; $c_end = $end; // Extract the range string. list( , $range ) = explode( '=', $http_range, 2 ); // Make sure the client hasn't sent us a multibyte range. if ( strpos( $range, ',' ) !== false ) { return $download_range; } /* * If the range starts with an '-' we start from the beginning. * If not, we forward the file pointer * and make sure to get the end byte if specified. */ if ( '-' === $range[0] ) { // The n-number of the last bytes is requested. $c_start = $file_size - substr( $range, 1 ); } else { $range = explode( '-', $range ); $c_start = ( isset( $range[0] ) && is_numeric( $range[0] ) ) ? (int) $range[0] : 0; $c_end = ( isset( $range[1] ) && is_numeric( $range[1] ) ) ? (int) $range[1] : $file_size; } /* * Check the range and make sure it's treated according to the specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. * End bytes can not be larger than $end. */ $c_end = ( $c_end > $end ) ? $end : $c_end; // Validate the requested range and return an error if it's not correct. if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) { return $download_range; } $start = $c_start; $end = $c_end; $length = $end - $start + 1; $download_range['start'] = $start; $download_range['length'] = $length; $download_range['is_range_valid'] = true; } return $download_range; } /** * Force download - this is the default method. * * @param string $file_path File path. * @param string $filename File name. */ public static function download_file_force( $file_path, $filename ) { $parsed_file_path = self::parse_file_path( $file_path ); $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. self::download_headers( $parsed_file_path['file_path'], $filename, $download_range ); $start = isset( $download_range['start'] ) ? $download_range['start'] : 0; $length = isset( $download_range['length'] ) ? $download_range['length'] : 0; if ( ! self::readfile_chunked( $parsed_file_path['file_path'], $start, $length ) ) { if ( $parsed_file_path['remote_file'] && 'yes' === get_option( 'woocommerce_downloads_redirect_fallback_allowed' ) ) { wc_get_logger()->warning( sprintf( /* translators: %1$s contains the filepath of the digital asset. */ __( '%1$s could not be served using the Force Download method. A redirect will be used instead.', 'woocommerce' ), $file_path ) ); self::download_file_redirect( $file_path ); } else { self::download_error( __( 'File not found', 'woocommerce' ) ); } } exit; } /** * Get content type of a download. * * @param string $file_path File path. * @return string */ private static function get_download_content_type( $file_path ) { $file_extension = strtolower( substr( strrchr( $file_path, '.' ), 1 ) ); $ctype = 'application/force-download'; foreach ( get_allowed_mime_types() as $mime => $type ) { $mimes = explode( '|', $mime ); if ( in_array( $file_extension, $mimes, true ) ) { $ctype = $type; break; } } return $ctype; } /** * Set headers for the download. * * @param string $file_path File path. * @param string $filename File name. * @param array $download_range Array containing info about range download request (see {@see get_download_range} for structure). */ private static function download_headers( $file_path, $filename, $download_range = array() ) { self::check_server_config(); self::clean_buffers(); wc_nocache_headers(); header( 'X-Robots-Tag: noindex, nofollow', true ); header( 'Content-Type: ' . self::get_download_content_type( $file_path ) ); header( 'Content-Description: File Transfer' ); header( 'Content-Disposition: attachment; filename="' . $filename . '";' ); header( 'Content-Transfer-Encoding: binary' ); $file_size = @filesize( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged if ( ! $file_size ) { return; } if ( isset( $download_range['is_range_request'] ) && true === $download_range['is_range_request'] ) { if ( false === $download_range['is_range_valid'] ) { header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); header( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size ); exit; } $start = $download_range['start']; $end = $download_range['start'] + $download_range['length'] - 1; $length = $download_range['length']; header( 'HTTP/1.1 206 Partial Content' ); header( "Accept-Ranges: 0-$file_size" ); header( "Content-Range: bytes $start-$end/$file_size" ); header( "Content-Length: $length" ); } else { header( 'Content-Length: ' . $file_size ); } } /** * Check and set certain server config variables to ensure downloads work as intended. */ private static function check_server_config() { wc_set_time_limit( 0 ); if ( function_exists( 'apache_setenv' ) ) { @apache_setenv( 'no-gzip', 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv } @ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set @session_write_close(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close } /** * Clean all output buffers. * * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. */ private static function clean_buffers() { if ( ob_get_level() ) { $levels = ob_get_level(); for ( $i = 0; $i < $levels; $i++ ) { @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged } } else { @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged } } /** * Read file chunked. * * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. * * @param string $file File. * @param int $start Byte offset/position of the beginning from which to read from the file. * @param int $length Length of the chunk to be read from the file in bytes, 0 means full file. * @return bool Success or fail */ public static function readfile_chunked( $file, $start = 0, $length = 0 ) { if ( ! defined( 'WC_CHUNK_SIZE' ) ) { define( 'WC_CHUNK_SIZE', 1024 * 1024 ); } $handle = @fopen( $file, 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen if ( false === $handle ) { return false; } if ( ! $length ) { $length = @filesize( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged } $read_length = (int) WC_CHUNK_SIZE; if ( $length ) { $end = $start + $length - 1; @fseek( $handle, $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged while ( ! @feof( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged // Don't run past the end of file. if ( $p + $read_length > $end ) { $read_length = $end - $p + 1; } echo @fread( $handle, $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged if ( ob_get_length() ) { ob_flush(); flush(); } } } else { while ( ! @feof( $handle ) ) { // @codingStandardsIgnoreLine. echo @fread( $handle, $read_length ); // @codingStandardsIgnoreLine. if ( ob_get_length() ) { ob_flush(); flush(); } } } return @fclose( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose } /** * Filter headers for IE to fix issues over SSL. * * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. * * @param array $headers HTTP headers. * @return array */ public static function ie_nocache_headers_fix( $headers ) { if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { $headers['Cache-Control'] = 'private'; unset( $headers['Pragma'] ); } return $headers; } /** * Die with an error message if the download fails. * * @param string $message Error message. * @param string $title Error title. * @param integer $status Error status. */ private static function download_error( $message, $title = '', $status = 404 ) { /* * Since we will now render a message instead of serving a download, we should unwind some of the previously set * headers. */ header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); header_remove( 'Content-Description;' ); header_remove( 'Content-Disposition' ); header_remove( 'Content-Transfer-Encoding' ); if ( ! strstr( $message, '<a ' ) ) { $message .= ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-forward">' . esc_html__( 'Go to shop', 'woocommerce' ) . '</a>'; } wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok. } } WC_Download_Handler::init(); includes/class-wc-customer.php 0000644 00000071646 15132754524 0012464 0 ustar 00 <?php /** * The WooCommerce customer class handles storage of the current customer's data, such as location. * * @package WooCommerce\Classes * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; require_once dirname( __FILE__ ) . '/legacy/class-wc-legacy-customer.php'; /** * Customer class. */ class WC_Customer extends WC_Legacy_Customer { /** * Stores customer data. * * @var array */ protected $data = array( 'date_created' => null, 'date_modified' => null, 'email' => '', 'first_name' => '', 'last_name' => '', 'display_name' => '', 'role' => 'customer', 'username' => '', 'billing' => array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'postcode' => '', 'country' => '', 'state' => '', 'email' => '', 'phone' => '', ), 'shipping' => array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'postcode' => '', 'country' => '', 'state' => '', 'phone' => '', ), 'is_paying_customer' => false, ); /** * Stores a password if this needs to be changed. Write-only and hidden from _data. * * @var string */ protected $password = ''; /** * Stores if user is VAT exempt for this session. * * @var string */ protected $is_vat_exempt = false; /** * Stores if user has calculated shipping in this session. * * @var string */ protected $calculated_shipping = false; /** * This is the name of this object type. * * @since 5.6.0 * @var string */ protected $object_type = 'customer'; /** * Load customer data based on how WC_Customer is called. * * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some * data will be pulled from the session for the current user/customer. * * @param WC_Customer|int $data Customer ID or data. * @param bool $is_session True if this is the customer session. * @throws Exception If customer cannot be read/found and $data is set. */ public function __construct( $data = 0, $is_session = false ) { parent::__construct( $data ); if ( $data instanceof WC_Customer ) { $this->set_id( absint( $data->get_id() ) ); } elseif ( is_numeric( $data ) ) { $this->set_id( $data ); } $this->data_store = WC_Data_Store::load( 'customer' ); // If we have an ID, load the user from the DB. if ( $this->get_id() ) { try { $this->data_store->read( $this ); } catch ( Exception $e ) { $this->set_id( 0 ); $this->set_object_read( true ); } } else { $this->set_object_read( true ); } // If this is a session, set or change the data store to sessions. Changes do not persist in the database. if ( $is_session && isset( WC()->session ) ) { $this->data_store = WC_Data_Store::load( 'customer-session' ); $this->data_store->read( $this ); } } /** * Delete a customer and reassign posts.. * * @param int $reassign Reassign posts and links to new User ID. * @since 3.0.0 * @return bool */ public function delete_and_reassign( $reassign = null ) { if ( $this->data_store ) { $this->data_store->delete( $this, array( 'force_delete' => true, 'reassign' => $reassign, ) ); $this->set_id( 0 ); return true; } return false; } /** * Is customer outside base country (for tax purposes)? * * @return bool */ public function is_customer_outside_base() { list( $country, $state ) = $this->get_taxable_address(); if ( $country ) { $default = wc_get_base_location(); if ( $default['country'] !== $country ) { return true; } if ( $default['state'] && $default['state'] !== $state ) { return true; } } return false; } /** * Return this customer's avatar. * * @since 3.0.0 * @return string */ public function get_avatar_url() { return get_avatar_url( $this->get_email() ); } /** * Get taxable address. * * @return array */ public function get_taxable_address() { $tax_based_on = get_option( 'woocommerce_tax_based_on' ); // Check shipping method at this point to see if we need special handling. if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) { $tax_based_on = 'base'; } if ( 'base' === $tax_based_on ) { $country = WC()->countries->get_base_country(); $state = WC()->countries->get_base_state(); $postcode = WC()->countries->get_base_postcode(); $city = WC()->countries->get_base_city(); } elseif ( 'billing' === $tax_based_on ) { $country = $this->get_billing_country(); $state = $this->get_billing_state(); $postcode = $this->get_billing_postcode(); $city = $this->get_billing_city(); } else { $country = $this->get_shipping_country(); $state = $this->get_shipping_state(); $postcode = $this->get_shipping_postcode(); $city = $this->get_shipping_city(); } return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) ); } /** * Gets a customer's downloadable products. * * @return array Array of downloadable products */ public function get_downloadable_products() { $downloads = array(); if ( $this->get_id() ) { $downloads = wc_get_customer_available_downloads( $this->get_id() ); } return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads ); } /** * Is customer VAT exempt? * * @return bool */ public function is_vat_exempt() { return $this->get_is_vat_exempt(); } /** * Has calculated shipping? * * @return bool */ public function has_calculated_shipping() { return $this->get_calculated_shipping(); } /** * Indicates if the customer has a non-empty shipping address. * * Note that this does not indicate if the customer's shipping address * is complete, only that one or more fields are populated. * * @since 5.3.0 * * @return bool */ public function has_shipping_address() { foreach ( $this->get_shipping() as $address_field ) { // Trim guards against a case where a subset of saved shipping address fields contain whitespace. if ( strlen( trim( $address_field ) ) > 0 ) { return true; } } return false; } /** * Get if customer is VAT exempt? * * @since 3.0.0 * @return bool */ public function get_is_vat_exempt() { return $this->is_vat_exempt; } /** * Get password (only used when updating the user object). * * @return string */ public function get_password() { return $this->password; } /** * Has customer calculated shipping? * * @return bool */ public function get_calculated_shipping() { return $this->calculated_shipping; } /** * Set if customer has tax exemption. * * @param bool $is_vat_exempt If is vat exempt. */ public function set_is_vat_exempt( $is_vat_exempt ) { $this->is_vat_exempt = wc_string_to_bool( $is_vat_exempt ); } /** * Calculated shipping? * * @param bool $calculated If shipping is calculated. */ public function set_calculated_shipping( $calculated = true ) { $this->calculated_shipping = wc_string_to_bool( $calculated ); } /** * Set customer's password. * * @since 3.0.0 * @param string $password Password. */ public function set_password( $password ) { $this->password = $password; } /** * Gets the customers last order. * * @return WC_Order|false */ public function get_last_order() { return $this->data_store->get_last_order( $this ); } /** * Return the number of orders this customer has. * * @return integer */ public function get_order_count() { return $this->data_store->get_order_count( $this ); } /** * Return how much money this customer has spent. * * @return float */ public function get_total_spent() { return $this->data_store->get_total_spent( $this ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Return the customer's username. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_username( $context = 'view' ) { return $this->get_prop( 'username', $context ); } /** * Return the customer's email. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_email( $context = 'view' ) { return $this->get_prop( 'email', $context ); } /** * Return customer's first name. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_first_name( $context = 'view' ) { return $this->get_prop( 'first_name', $context ); } /** * Return customer's last name. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_last_name( $context = 'view' ) { return $this->get_prop( 'last_name', $context ); } /** * Return customer's display name. * * @since 3.1.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_display_name( $context = 'view' ) { return $this->get_prop( 'display_name', $context ); } /** * Return customer's user role. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_role( $context = 'view' ) { return $this->get_prop( 'role', $context ); } /** * Return the date this customer was created. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|null object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { return $this->get_prop( 'date_created', $context ); } /** * Return the date this customer was last updated. * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return WC_DateTime|null object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { return $this->get_prop( 'date_modified', $context ); } /** * Gets a prop for a getter method. * * @since 3.0.0 * @param string $prop Name of prop to get. * @param string $address billing or shipping. * @param string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit. * @return mixed */ protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { $value = null; if ( array_key_exists( $prop, $this->data[ $address ] ) ) { $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); } } return $value; } /** * Get billing. * * @since 3.2.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_billing( $context = 'view' ) { $value = null; $prop = 'billing'; if ( array_key_exists( $prop, $this->data ) ) { $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); $value = array_merge( $this->data[ $prop ], $changes ); if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); } } return $value; } /** * Get billing_first_name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_first_name( $context = 'view' ) { return $this->get_address_prop( 'first_name', 'billing', $context ); } /** * Get billing_last_name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_last_name( $context = 'view' ) { return $this->get_address_prop( 'last_name', 'billing', $context ); } /** * Get billing_company. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_company( $context = 'view' ) { return $this->get_address_prop( 'company', 'billing', $context ); } /** * Get billing_address_1. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_address( $context = 'view' ) { return $this->get_billing_address_1( $context ); } /** * Get billing_address_1. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_address_1( $context = 'view' ) { return $this->get_address_prop( 'address_1', 'billing', $context ); } /** * Get billing_address_2. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string $value */ public function get_billing_address_2( $context = 'view' ) { return $this->get_address_prop( 'address_2', 'billing', $context ); } /** * Get billing_city. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string $value */ public function get_billing_city( $context = 'view' ) { return $this->get_address_prop( 'city', 'billing', $context ); } /** * Get billing_state. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_state( $context = 'view' ) { return $this->get_address_prop( 'state', 'billing', $context ); } /** * Get billing_postcode. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_postcode( $context = 'view' ) { return $this->get_address_prop( 'postcode', 'billing', $context ); } /** * Get billing_country. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_country( $context = 'view' ) { return $this->get_address_prop( 'country', 'billing', $context ); } /** * Get billing_email. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_email( $context = 'view' ) { return $this->get_address_prop( 'email', 'billing', $context ); } /** * Get billing_phone. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_billing_phone( $context = 'view' ) { return $this->get_address_prop( 'phone', 'billing', $context ); } /** * Get shipping. * * @since 3.2.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_shipping( $context = 'view' ) { $value = null; $prop = 'shipping'; if ( array_key_exists( $prop, $this->data ) ) { $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); $value = array_merge( $this->data[ $prop ], $changes ); if ( 'view' === $context ) { $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); } } return $value; } /** * Get shipping_first_name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_first_name( $context = 'view' ) { return $this->get_address_prop( 'first_name', 'shipping', $context ); } /** * Get shipping_last_name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_last_name( $context = 'view' ) { return $this->get_address_prop( 'last_name', 'shipping', $context ); } /** * Get shipping_company. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_company( $context = 'view' ) { return $this->get_address_prop( 'company', 'shipping', $context ); } /** * Get shipping_address_1. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_address( $context = 'view' ) { return $this->get_shipping_address_1( $context ); } /** * Get shipping_address_1. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_address_1( $context = 'view' ) { return $this->get_address_prop( 'address_1', 'shipping', $context ); } /** * Get shipping_address_2. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_address_2( $context = 'view' ) { return $this->get_address_prop( 'address_2', 'shipping', $context ); } /** * Get shipping_city. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_city( $context = 'view' ) { return $this->get_address_prop( 'city', 'shipping', $context ); } /** * Get shipping_state. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_state( $context = 'view' ) { return $this->get_address_prop( 'state', 'shipping', $context ); } /** * Get shipping_postcode. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_postcode( $context = 'view' ) { return $this->get_address_prop( 'postcode', 'shipping', $context ); } /** * Get shipping_country. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_country( $context = 'view' ) { return $this->get_address_prop( 'country', 'shipping', $context ); } /** * Get shipping phone. * * @since 5.6.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_shipping_phone( $context = 'view' ) { return $this->get_address_prop( 'phone', 'shipping', $context ); } /** * Is the user a paying customer? * * @since 3.0.0 * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return bool */ public function get_is_paying_customer( $context = 'view' ) { return $this->get_prop( 'is_paying_customer', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set customer's username. * * @since 3.0.0 * @param string $username Username. */ public function set_username( $username ) { $this->set_prop( 'username', $username ); } /** * Set customer's email. * * @since 3.0.0 * @param string $value Email. */ public function set_email( $value ) { if ( $value && ! is_email( $value ) ) { $this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) ); } $this->set_prop( 'email', sanitize_email( $value ) ); } /** * Set customer's first name. * * @since 3.0.0 * @param string $first_name First name. */ public function set_first_name( $first_name ) { $this->set_prop( 'first_name', $first_name ); } /** * Set customer's last name. * * @since 3.0.0 * @param string $last_name Last name. */ public function set_last_name( $last_name ) { $this->set_prop( 'last_name', $last_name ); } /** * Set customer's display name. * * @since 3.1.0 * @param string $display_name Display name. */ public function set_display_name( $display_name ) { /* translators: 1: first name 2: last name */ $this->set_prop( 'display_name', is_email( $display_name ) ? sprintf( _x( '%1$s %2$s', 'display name', 'woocommerce' ), $this->get_first_name(), $this->get_last_name() ) : $display_name ); } /** * Set customer's user role(s). * * @since 3.0.0 * @param mixed $role User role. */ public function set_role( $role ) { global $wp_roles; if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ), true ) ) { $this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) ); } $this->set_prop( 'role', $role ); } /** * Set the date this customer was last updated. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set the date this customer was last updated. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set customer address to match shop base address. * * @since 3.0.0 */ public function set_billing_address_to_base() { $base = wc_get_customer_default_location(); $this->set_billing_location( $base['country'], $base['state'], '', '' ); } /** * Set customer shipping address to base address. * * @since 3.0.0 */ public function set_shipping_address_to_base() { $base = wc_get_customer_default_location(); $this->set_shipping_location( $base['country'], $base['state'], '', '' ); } /** * Sets all address info at once. * * @param string $country Country. * @param string $state State. * @param string $postcode Postcode. * @param string $city City. */ public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) { $address_data = $this->get_prop( 'billing', 'edit' ); $address_data['address_1'] = ''; $address_data['address_2'] = ''; $address_data['city'] = $city; $address_data['state'] = $state; $address_data['postcode'] = $postcode; $address_data['country'] = $country; $this->set_prop( 'billing', $address_data ); } /** * Sets all shipping info at once. * * @param string $country Country. * @param string $state State. * @param string $postcode Postcode. * @param string $city City. */ public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { $address_data = $this->get_prop( 'shipping', 'edit' ); $address_data['address_1'] = ''; $address_data['address_2'] = ''; $address_data['city'] = $city; $address_data['state'] = $state; $address_data['postcode'] = $postcode; $address_data['country'] = $country; $this->set_prop( 'shipping', $address_data ); } /** * Sets a prop for a setter method. * * @since 3.0.0 * @param string $prop Name of prop to set. * @param string $address Name of address to set. billing or shipping. * @param mixed $value Value of the prop. */ protected function set_address_prop( $prop, $address, $value ) { if ( array_key_exists( $prop, $this->data[ $address ] ) ) { if ( true === $this->object_read ) { if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { $this->changes[ $address ][ $prop ] = $value; } } else { $this->data[ $address ][ $prop ] = $value; } } } /** * Set billing_first_name. * * @param string $value Billing first name. */ public function set_billing_first_name( $value ) { $this->set_address_prop( 'first_name', 'billing', $value ); } /** * Set billing_last_name. * * @param string $value Billing last name. */ public function set_billing_last_name( $value ) { $this->set_address_prop( 'last_name', 'billing', $value ); } /** * Set billing_company. * * @param string $value Billing company. */ public function set_billing_company( $value ) { $this->set_address_prop( 'company', 'billing', $value ); } /** * Set billing_address_1. * * @param string $value Billing address line 1. */ public function set_billing_address( $value ) { $this->set_billing_address_1( $value ); } /** * Set billing_address_1. * * @param string $value Billing address line 1. */ public function set_billing_address_1( $value ) { $this->set_address_prop( 'address_1', 'billing', $value ); } /** * Set billing_address_2. * * @param string $value Billing address line 2. */ public function set_billing_address_2( $value ) { $this->set_address_prop( 'address_2', 'billing', $value ); } /** * Set billing_city. * * @param string $value Billing city. */ public function set_billing_city( $value ) { $this->set_address_prop( 'city', 'billing', $value ); } /** * Set billing_state. * * @param string $value Billing state. */ public function set_billing_state( $value ) { $this->set_address_prop( 'state', 'billing', $value ); } /** * Set billing_postcode. * * @param string $value Billing postcode. */ public function set_billing_postcode( $value ) { $this->set_address_prop( 'postcode', 'billing', $value ); } /** * Set billing_country. * * @param string $value Billing country. */ public function set_billing_country( $value ) { $this->set_address_prop( 'country', 'billing', $value ); } /** * Set billing_email. * * @param string $value Billing email. */ public function set_billing_email( $value ) { if ( $value && ! is_email( $value ) ) { $this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); } $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); } /** * Set billing_phone. * * @param string $value Billing phone. */ public function set_billing_phone( $value ) { $this->set_address_prop( 'phone', 'billing', $value ); } /** * Set shipping_first_name. * * @param string $value Shipping first name. */ public function set_shipping_first_name( $value ) { $this->set_address_prop( 'first_name', 'shipping', $value ); } /** * Set shipping_last_name. * * @param string $value Shipping last name. */ public function set_shipping_last_name( $value ) { $this->set_address_prop( 'last_name', 'shipping', $value ); } /** * Set shipping_company. * * @param string $value Shipping company. */ public function set_shipping_company( $value ) { $this->set_address_prop( 'company', 'shipping', $value ); } /** * Set shipping_address_1. * * @param string $value Shipping address line 1. */ public function set_shipping_address( $value ) { $this->set_shipping_address_1( $value ); } /** * Set shipping_address_1. * * @param string $value Shipping address line 1. */ public function set_shipping_address_1( $value ) { $this->set_address_prop( 'address_1', 'shipping', $value ); } /** * Set shipping_address_2. * * @param string $value Shipping address line 2. */ public function set_shipping_address_2( $value ) { $this->set_address_prop( 'address_2', 'shipping', $value ); } /** * Set shipping_city. * * @param string $value Shipping city. */ public function set_shipping_city( $value ) { $this->set_address_prop( 'city', 'shipping', $value ); } /** * Set shipping_state. * * @param string $value Shipping state. */ public function set_shipping_state( $value ) { $this->set_address_prop( 'state', 'shipping', $value ); } /** * Set shipping_postcode. * * @param string $value Shipping postcode. */ public function set_shipping_postcode( $value ) { $this->set_address_prop( 'postcode', 'shipping', $value ); } /** * Set shipping_country. * * @param string $value Shipping country. */ public function set_shipping_country( $value ) { $this->set_address_prop( 'country', 'shipping', $value ); } /** * Set shipping phone. * * @since 5.6.0 * @param string $value Shipping phone. */ public function set_shipping_phone( $value ) { $this->set_address_prop( 'phone', 'shipping', $value ); } /** * Set if the user a paying customer. * * @since 3.0.0 * @param bool $is_paying_customer If is a paying customer. */ public function set_is_paying_customer( $is_paying_customer ) { $this->set_prop( 'is_paying_customer', (bool) $is_paying_customer ); } } includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php 0000644 00000002612 15132754524 0022521 0 ustar 00 <?php /** * WooCommerce.com Product Installation Requirements Check. * * @package WooCommerce\WCCom * @since 3.8.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_WCCOM_Site_Installer_Requirements_Check Class * Contains functionality to check the necessary requirements for the installer. */ class WC_WCCOM_Site_Installer_Requirements_Check { /** * Check if the site met the requirements * * @version 3.8.0 * @return bool|WP_Error Does the site met the requirements? */ public static function met_requirements() { $errs = array(); if ( ! self::met_wp_cron_requirement() ) { $errs[] = 'wp-cron'; } if ( ! self::met_filesystem_requirement() ) { $errs[] = 'filesystem'; } if ( ! empty( $errs ) ) { // translators: %s: Requirements unmet. return new WP_Error( 'requirements_not_met', sprintf( __( 'Server requirements not met, missing requirement(s): %s.', 'woocommerce' ), implode( ', ', $errs ) ), array( 'status' => 400 ) ); } return true; } /** * Validates if WP CRON is enabled. * * @since 3.8.0 * @return bool */ private static function met_wp_cron_requirement() { return ! Constants::is_true( 'DISABLE_WP_CRON' ); } /** * Validates if `WP_CONTENT_DIR` is writable. * * @since 3.8.0 * @return bool */ private static function met_filesystem_requirement() { return is_writable( WP_CONTENT_DIR ); } } includes/wccom-site/class-wc-wccom-site.php 0000644 00000020540 15132754524 0014732 0 ustar 00 <?php /** * WooCommerce.com Product Installation. * * @package WooCommerce\WCCom * @since 3.7.0 */ defined( 'ABSPATH' ) || exit; /** * WC_WCCOM_Site Class * * Main class for WooCommerce.com connected site. */ class WC_WCCOM_Site { const AUTH_ERROR_FILTER_NAME = 'wccom_auth_error'; /** * Load the WCCOM site class. * * @since 3.7.0 */ public static function load() { self::includes(); add_action( 'woocommerce_wccom_install_products', array( 'WC_WCCOM_Site_Installer', 'install' ) ); add_filter( 'determine_current_user', array( __CLASS__, 'authenticate_wccom' ), 14 ); add_action( 'woocommerce_rest_api_get_rest_namespaces', array( __CLASS__, 'register_rest_namespace' ) ); } /** * Include support files. * * @since 3.7.0 */ protected static function includes() { require_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper.php'; require_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site-installer.php'; require_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php'; } /** * Authenticate WooCommerce.com request. * * @since 3.7.0 * @param int|false $user_id User ID. * @return int|false */ public static function authenticate_wccom( $user_id ) { if ( ! empty( $user_id ) || ! self::is_request_to_wccom_site_rest_api() ) { return $user_id; } $auth_header = trim( self::get_authorization_header() ); if ( stripos( $auth_header, 'Bearer ' ) === 0 ) { $access_token = trim( substr( $auth_header, 7 ) ); } elseif ( ! empty( $_GET['token'] ) && is_string( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $access_token = trim( $_GET['token'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } else { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_CODE, WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_HTTP_CODE ) ); } ); return false; } if ( ! empty( $_SERVER['HTTP_X_WOO_SIGNATURE'] ) ) { $signature = trim( $_SERVER['HTTP_X_WOO_SIGNATURE'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( ! empty( $_GET['signature'] ) && is_string( $_GET['signature'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $signature = trim( $_GET['signature'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } else { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_CODE, WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_HTTP_CODE ) ); } ); return false; } require_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php'; $site_auth = WC_Helper_Options::get( 'auth' ); if ( empty( $site_auth['access_token'] ) ) { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_CODE, WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_HTTP_CODE ) ); } ); return false; } if ( ! hash_equals( $access_token, $site_auth['access_token'] ) ) { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_CODE, WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_HTTP_CODE ) ); } ); return false; } $body = WP_REST_Server::get_raw_data(); if ( ! self::verify_wccom_request( $body, $signature, $site_auth['access_token_secret'] ) ) { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_CODE, WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_HTTP_CODE ) ); } ); return false; } $user = get_user_by( 'id', $site_auth['user_id'] ); if ( ! $user ) { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_CODE, WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_HTTP_CODE ) ); } ); return false; } return $user; } /** * Get the authorization header. * * On certain systems and configurations, the Authorization header will be * stripped out by the server or PHP. Typically this is then used to * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use * `getallheaders` here to try and grab it out instead. * * @since 3.7.0 * @return string Authorization header if set. */ protected static function get_authorization_header() { if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) { return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } if ( function_exists( 'getallheaders' ) ) { $headers = getallheaders(); // Check for the authoization header case-insensitively. foreach ( $headers as $key => $value ) { if ( 'authorization' === strtolower( $key ) ) { return $value; } } } return ''; } /** * Check if this is a request to WCCOM Site REST API. * * @since 3.7.0 * @return bool */ protected static function is_request_to_wccom_site_rest_api() { if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $route = wp_unslash( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended $rest_prefix = ''; } else { $route = wp_unslash( add_query_arg( array() ) ); $rest_prefix = trailingslashit( rest_get_url_prefix() ); } return false !== strpos( $route, $rest_prefix . 'wccom-site/' ); } /** * Verify WooCommerce.com request from a given body and signature request. * * @since 3.7.0 * @param string $body Request body. * @param string $signature Request signature found in X-Woo-Signature header. * @param string $access_token_secret Access token secret for this site. * @return bool */ protected static function verify_wccom_request( $body, $signature, $access_token_secret ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data = array( 'host' => $_SERVER['HTTP_HOST'], 'request_uri' => urldecode( remove_query_arg( array( 'token', 'signature' ), $_SERVER['REQUEST_URI'] ) ), 'method' => strtoupper( $_SERVER['REQUEST_METHOD'] ), ); // phpcs:enable if ( ! empty( $body ) ) { $data['body'] = $body; } $expected_signature = hash_hmac( 'sha256', wp_json_encode( $data ), $access_token_secret ); return hash_equals( $expected_signature, $signature ); } /** * Register wccom-site REST namespace. * * @since 3.7.0 * @param array $namespaces List of registered namespaces. * @return array Registered namespaces. */ public static function register_rest_namespace( $namespaces ) { require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php'; require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php'; $namespaces['wccom-site/v1'] = array( 'installer' => 'WC_REST_WCCOM_Site_Installer_Controller', ); return $namespaces; } } WC_WCCOM_Site::load(); includes/wccom-site/class-wc-wccom-site-installer.php 0000644 00000037166 15132754524 0016741 0 ustar 00 <?php /** * WooCommerce.com Product Installation. * * @package WooCommerce\WCCom * @since 3.7.0 */ defined( 'ABSPATH' ) || exit; /** * WC_WCCOM_Site_Installer Class * * Contains functionalities to install products via WooCommerce.com helper connection. */ class WC_WCCOM_Site_Installer { /** * Error message returned install_package if the folder already exists. * * @var string */ private static $folder_exists = 'folder_exists'; /** * Default state. * * @var array */ private static $default_state = array( 'status' => 'idle', 'steps' => array(), 'current_step' => null, ); /** * Represents product step state. * * @var array */ private static $default_step_state = array( 'download_url' => '', 'product_type' => '', 'last_step' => '', 'last_error' => '', 'download_path' => '', 'unpacked_path' => '', 'installed_path' => '', 'activate' => false, ); /** * Product install steps. Each step is a method name in this class that * will be passed with product ID arg \WP_Upgrader instance. * * @var array */ private static $install_steps = array( 'get_product_info', 'download_product', 'unpack_product', 'move_product', 'activate_product', ); /** * Get the product install state. * * @since 3.7.0 * @param string $key Key in state data. If empty key is passed array of * state will be returned. * @return array Product install state. */ public static function get_state( $key = '' ) { $state = WC_Helper_Options::get( 'product_install', self::$default_state ); if ( ! empty( $key ) ) { return isset( $state[ $key ] ) ? $state[ $key ] : null; } return $state; } /** * Update the product install state. * * @since 3.7.0 * @param string $key Key in state data. * @param mixed $value Value. */ public static function update_state( $key, $value ) { $state = WC_Helper_Options::get( 'product_install', self::$default_state ); $state[ $key ] = $value; WC_Helper_Options::update( 'product_install', $state ); } /** * Reset product install state. * * @since 3.7.0 * @param array $products List of product IDs. */ public static function reset_state( $products = array() ) { WC()->queue()->cancel_all( 'woocommerce_wccom_install_products' ); WC_Helper_Options::update( 'product_install', self::$default_state ); } /** * Schedule installing given list of products. * * @since 3.7.0 * @param array $products Array of products where key is product ID and * element is install args. * @return array State. */ public static function schedule_install( $products ) { $state = self::get_state(); $status = ! empty( $state['status'] ) ? $state['status'] : ''; if ( 'in-progress' === $status ) { return $state; } self::update_state( 'status', 'in-progress' ); $steps = array_fill_keys( array_keys( $products ), self::$default_step_state ); self::update_state( 'steps', $steps ); self::update_state( 'current_step', null ); $args = array( 'products' => $products, ); // Clear the cache of customer's subscription before asking for them. // Thus, they will be re-fetched from WooCommerce.com after a purchase. WC_Helper::_flush_subscriptions_cache(); WC()->queue()->cancel_all( 'woocommerce_wccom_install_products', $args ); WC()->queue()->add( 'woocommerce_wccom_install_products', $args ); return self::get_state(); } /** * Install a given product IDs. * * Run via `woocommerce_wccom_install_products` hook. * * @since 3.7.0 * @param array $products Array of products where key is product ID and * element is install args. */ public static function install( $products ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php'; WP_Filesystem(); $upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() ); $upgrader->init(); wp_clean_plugins_cache(); foreach ( $products as $product_id => $install_args ) { self::install_product( $product_id, $install_args, $upgrader ); } self::finish_installation(); } /** * Finish installation by updating the state. * * @since 3.7.0 */ private static function finish_installation() { $state = self::get_state(); if ( empty( $state['steps'] ) ) { return; } foreach ( $state['steps'] as $step ) { if ( ! empty( $step['last_error'] ) ) { $state['status'] = 'has_error'; break; } } if ( 'has_error' !== $state['status'] ) { $state['status'] = 'finished'; } WC_Helper_Options::update( 'product_install', $state ); } /** * Install a single product given its ID. * * @since 3.7.0 * @param int $product_id Product ID. * @param array $install_args Install args. * @param \WP_Upgrader $upgrader Core class to handle installation. */ private static function install_product( $product_id, $install_args, $upgrader ) { foreach ( self::$install_steps as $step ) { self::do_install_step( $product_id, $install_args, $step, $upgrader ); } } /** * Perform product installation step. * * @since 3.7.0 * @param int $product_id Product ID. * @param array $install_args Install args. * @param string $step Installation step. * @param \WP_Upgrader $upgrader Core class to handle installation. */ private static function do_install_step( $product_id, $install_args, $step, $upgrader ) { $state_steps = self::get_state( 'steps' ); if ( empty( $state_steps[ $product_id ] ) ) { $state_steps[ $product_id ] = self::$default_step_state; } if ( ! empty( $state_steps[ $product_id ]['last_error'] ) ) { return; } $state_steps[ $product_id ]['last_step'] = $step; if ( ! empty( $install_args['activate'] ) ) { $state_steps[ $product_id ]['activate'] = true; } self::update_state( 'current_step', array( 'product_id' => $product_id, 'step' => $step, ) ); $result = call_user_func( array( __CLASS__, $step ), $product_id, $upgrader ); if ( is_wp_error( $result ) ) { $state_steps[ $product_id ]['last_error'] = $result->get_error_message(); } else { switch ( $step ) { case 'get_product_info': $state_steps[ $product_id ]['download_url'] = $result['download_url']; $state_steps[ $product_id ]['product_type'] = $result['product_type']; $state_steps[ $product_id ]['product_name'] = $result['product_name']; break; case 'download_product': $state_steps[ $product_id ]['download_path'] = $result; break; case 'unpack_product': $state_steps[ $product_id ]['unpacked_path'] = $result; break; case 'move_product': $state_steps[ $product_id ]['installed_path'] = $result['destination']; if ( isset( $result[ self::$folder_exists ] ) ) { $state_steps[ $product_id ]['warning'] = array( 'message' => self::$folder_exists, 'plugin_info' => self::get_plugin_info( $state_steps[ $product_id ]['installed_path'] ), ); } break; } } self::update_state( 'steps', $state_steps ); } /** * Get product info from its ID. * * @since 3.7.0 * @param int $product_id Product ID. * @return array|\WP_Error */ private static function get_product_info( $product_id ) { $product_info = array( 'download_url' => '', 'product_type' => '', ); // Get product info from woocommerce.com. $request = WC_Helper_API::get( add_query_arg( array( 'product_id' => absint( $product_id ) ), 'info' ), array( 'authenticated' => true, ) ); if ( 200 !== wp_remote_retrieve_response_code( $request ) ) { return new WP_Error( 'product_info_failed', __( 'Failed to retrieve product info from woocommerce.com', 'woocommerce' ) ); } $result = json_decode( wp_remote_retrieve_body( $request ), true ); $product_info['product_type'] = $result['_product_type']; $product_info['product_name'] = $result['name']; if ( ! empty( $result['_wporg_product'] ) && ! empty( $result['download_link'] ) ) { // For wporg product, download is set already from info response. $product_info['download_url'] = $result['download_link']; } elseif ( ! WC_Helper::has_product_subscription( $product_id ) ) { // Non-wporg product needs subscription. return new WP_Error( 'missing_subscription', __( 'Missing product subscription', 'woocommerce' ) ); } else { // Retrieve download URL for non-wporg product. WC_Helper_Updater::flush_updates_cache(); $updates = WC_Helper_Updater::get_update_data(); if ( empty( $updates[ $product_id ]['package'] ) ) { return new WP_Error( 'missing_product_package', __( 'Could not find product package.', 'woocommerce' ) ); } $product_info['download_url'] = $updates[ $product_id ]['package']; } return $product_info; } /** * Download product by its ID and returns the path of the zip package. * * @since 3.7.0 * @param int $product_id Product ID. * @param \WP_Upgrader $upgrader Core class to handle installation. * @return \WP_Error|string */ private static function download_product( $product_id, $upgrader ) { $steps = self::get_state( 'steps' ); if ( empty( $steps[ $product_id ]['download_url'] ) ) { return new WP_Error( 'missing_download_url', __( 'Could not find download url for the product.', 'woocommerce' ) ); } return $upgrader->download_package( $steps[ $product_id ]['download_url'] ); } /** * Unpack downloaded product. * * @since 3.7.0 * @param int $product_id Product ID. * @param \WP_Upgrader $upgrader Core class to handle installation. * @return \WP_Error|string */ private static function unpack_product( $product_id, $upgrader ) { $steps = self::get_state( 'steps' ); if ( empty( $steps[ $product_id ]['download_path'] ) ) { return new WP_Error( 'missing_download_path', __( 'Could not find download path.', 'woocommerce' ) ); } return $upgrader->unpack_package( $steps[ $product_id ]['download_path'], true ); } /** * Move product to plugins directory. * * @since 3.7.0 * @param int $product_id Product ID. * @param \WP_Upgrader $upgrader Core class to handle installation. * @return array|\WP_Error */ private static function move_product( $product_id, $upgrader ) { $steps = self::get_state( 'steps' ); if ( empty( $steps[ $product_id ]['unpacked_path'] ) ) { return new WP_Error( 'missing_unpacked_path', __( 'Could not find unpacked path.', 'woocommerce' ) ); } $destination = 'plugin' === $steps[ $product_id ]['product_type'] ? WP_PLUGIN_DIR : get_theme_root(); $package = array( 'source' => $steps[ $product_id ]['unpacked_path'], 'destination' => $destination, 'clear_working' => true, 'hook_extra' => array( 'type' => $steps[ $product_id ]['product_type'], 'action' => 'install', ), ); $result = $upgrader->install_package( $package ); /** * If install package returns error 'folder_exists' threat as success. */ if ( is_wp_error( $result ) && array_key_exists( self::$folder_exists, $result->errors ) ) { return array( self::$folder_exists => true, 'destination' => $result->error_data[ self::$folder_exists ], ); } return $result; } /** * Activate product given its product ID. * * @since 3.7.0 * @param int $product_id Product ID. * @return \WP_Error|null */ private static function activate_product( $product_id ) { $steps = self::get_state( 'steps' ); if ( ! $steps[ $product_id ]['activate'] ) { return null; } if ( 'plugin' === $steps[ $product_id ]['product_type'] ) { return self::activate_plugin( $product_id ); } return self::activate_theme( $product_id ); } /** * Activate plugin given its product ID. * * @since 3.7.0 * @param int $product_id Product ID. * @return \WP_Error|null */ private static function activate_plugin( $product_id ) { // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`. wp_clean_plugins_cache(); $filename = false; // If product is WP.org one, find out its filename. $dir_name = self::get_wporg_product_dir_name( $product_id ); if ( false !== $dir_name ) { $filename = self::get_wporg_plugin_main_file( $dir_name ); } if ( false === $filename ) { $plugins = wp_list_filter( WC_Helper::get_local_woo_plugins(), array( '_product_id' => $product_id, ) ); $filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : ''; } if ( empty( $filename ) ) { return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); } return activate_plugin( $filename ); } /** * Activate theme given its product ID. * * @since 3.7.0 * @param int $product_id Product ID. * @return \WP_Error|null */ private static function activate_theme( $product_id ) { // Clear plugins cache used in `WC_Helper::get_local_woo_themes`. wp_clean_themes_cache(); $theme_slug = false; // If product is WP.org theme, find out its slug. $dir_name = self::get_wporg_product_dir_name( $product_id ); if ( false !== $dir_name ) { $theme_slug = basename( $dir_name ); } if ( false === $theme_slug ) { $themes = wp_list_filter( WC_Helper::get_local_woo_themes(), array( '_product_id' => $product_id, ) ); $theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : ''; } if ( empty( $theme_slug ) ) { return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); } return switch_theme( $theme_slug ); } /** * Get installed directory of WP.org product. * * @since 3.7.0 * @param int $product_id Product ID. * @return bool|string */ private static function get_wporg_product_dir_name( $product_id ) { $steps = self::get_state( 'steps' ); $product = $steps[ $product_id ]; if ( empty( $product['download_url'] ) || empty( $product['installed_path'] ) ) { return false; } // Check whether product was downloaded from WordPress.org. $parsed_url = wp_parse_url( $product['download_url'] ); if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) { return false; } return basename( $product['installed_path'] ); } /** * Get WP.org plugin's main file. * * @since 3.7.0 * @param string $dir Directory name of the plugin. * @return bool|string */ private static function get_wporg_plugin_main_file( $dir ) { // Ensure that exact dir name is used. $dir = trailingslashit( $dir ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); foreach ( $plugins as $path => $plugin ) { if ( 0 === strpos( $path, $dir ) ) { return $path; } } return false; } /** * Get plugin info * * @since 3.9.0 * @param string $dir Directory name of the plugin. * @return bool|array */ private static function get_plugin_info( $dir ) { $plugin_folder = basename( $dir ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $related_plugins = array_filter( $plugins, function( $key ) use ( $plugin_folder ) { return strpos( $key, $plugin_folder . '/' ) === 0; }, ARRAY_FILTER_USE_KEY ); if ( 1 === count( $related_plugins ) ) { $plugin_key = array_keys( $related_plugins )[0]; $plugin_data = $plugins[ $plugin_key ]; return array( 'name' => $plugin_data['Name'], 'version' => $plugin_data['Version'], 'active' => is_plugin_active( $plugin_key ), ); } return false; } } includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php 0000644 00000003717 15132754524 0022745 0 ustar 00 <?php /** * WCCOM Site Installer Errors Class * * @package WooCommerce\WCCom\API * @since 3.9.0 */ defined( 'ABSPATH' ) || exit; /** * WCCOM Site Installer Errors Class * * Stores data for errors, returned by installer API. */ class WC_REST_WCCOM_Site_Installer_Errors { /** * Not unauthenticated generic error */ const NOT_AUTHENTICATED_CODE = 'not_authenticated'; const NOT_AUTHENTICATED_MESSAGE = 'Authentication required'; const NOT_AUTHENTICATED_HTTP_CODE = 401; /** * No access token provided */ const NO_ACCESS_TOKEN_CODE = 'no_access_token'; const NO_ACCESS_TOKEN_MESSAGE = 'No access token provided'; const NO_ACCESS_TOKEN_HTTP_CODE = 400; /** * No signature provided */ const NO_SIGNATURE_CODE = 'no_signature'; const NO_SIGNATURE_MESSAGE = 'No signature provided'; const NO_SIGNATURE_HTTP_CODE = 400; /** * Site not connected to WooCommerce.com */ const SITE_NOT_CONNECTED_CODE = 'site_not_connnected'; const SITE_NOT_CONNECTED_MESSAGE = 'Site not connected to WooCommerce.com'; const SITE_NOT_CONNECTED_HTTP_CODE = 401; /** * Provided access token is not valid */ const INVALID_TOKEN_CODE = 'invalid_token'; const INVALID_TOKEN_MESSAGE = 'Invalid access token provided'; const INVALID_TOKEN_HTTP_CODE = 401; /** * Request verification by provided signature failed */ const REQUEST_VERIFICATION_FAILED_CODE = 'request_verification_failed'; const REQUEST_VERIFICATION_FAILED_MESSAGE = 'Request verification by signature failed'; const REQUEST_VERIFICATION_FAILED_HTTP_CODE = 400; /** * User doesn't exist */ const USER_NOT_FOUND_CODE = 'user_not_found'; const USER_NOT_FOUND_MESSAGE = 'Token owning user not found'; const USER_NOT_FOUND_HTTP_CODE = 401; /** * No permissions error */ const NO_PERMISSION_CODE = 'forbidden'; const NO_PERMISSION_MESSAGE = 'You do not have permission to install plugin or theme'; const NO_PERMISSION_HTTP_CODE = 403; } includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php 0000644 00000011261 15132754524 0025610 0 ustar 00 <?php /** * WCCOM Site Installer REST API Controller * * Handles requests to /installer. * * @package WooCommerce\WCCom\API * @since 3.7.0 */ defined( 'ABSPATH' ) || exit; /** * REST API WCCOM Site Installer Controller Class. * * @extends WC_REST_Controller */ class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wccom-site/v1'; /** * Route base. * * @var string */ protected $rest_base = 'installer'; /** * Register the routes for product reviews. * * @since 3.7.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_install_state' ), 'permission_callback' => array( $this, 'check_permission' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'install' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'products' => array( 'required' => true, 'type' => 'object', ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'reset_install' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); } /** * Check permissions. * * @since 3.7.0 * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function check_permission( $request ) { $current_user = wp_get_current_user(); if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { return apply_filters( WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME, new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_CODE, WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_HTTP_CODE ) ) ); } if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) { return new WP_Error( WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_CODE, WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_MESSAGE, array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_HTTP_CODE ) ); } return true; } /** * Get installation state. * * @since 3.7.0 * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function get_install_state( $request ) { $requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements(); if ( is_wp_error( $requirements_met ) ) { return $requirements_met; } return rest_ensure_response( WC_WCCOM_Site_Installer::get_state() ); } /** * Install WooCommerce.com products. * * @since 3.7.0 * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function install( $request ) { $requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements(); if ( is_wp_error( $requirements_met ) ) { return $requirements_met; } if ( empty( $request['products'] ) ) { return new WP_Error( 'missing_products', __( 'Missing products in request body.', 'woocommerce' ), array( 'status' => 400 ) ); } $validation_result = $this->validate_products( $request['products'] ); if ( is_wp_error( $validation_result ) ) { return $validation_result; } return rest_ensure_response( WC_WCCOM_Site_Installer::schedule_install( $request['products'] ) ); } /** * Reset installation state. * * @since 3.7.0 * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ public function reset_install( $request ) { $resp = rest_ensure_response( WC_WCCOM_Site_Installer::reset_state() ); $resp->set_status( 204 ); return $resp; } /** * Validate products from request body. * * @since 3.7.0 * @param array $products Array of products where key is product ID and * element is install args. * @return bool|WP_Error */ protected function validate_products( $products ) { $err = new WP_Error( 'invalid_products', __( 'Invalid products in request body.', 'woocommerce' ), array( 'status' => 400 ) ); if ( ! is_array( $products ) ) { return $err; } foreach ( $products as $product_id => $install_args ) { if ( ! absint( $product_id ) ) { return $err; } if ( empty( $install_args ) || ! is_array( $install_args ) ) { return $err; } } return true; } } includes/class-wc-query.php 0000644 00000077151 15132754524 0011765 0 ustar 00 <?php /** * Contains the query functions for WooCommerce which alter the front-end post queries and loops * * @version 3.2.0 * @package WooCommerce\Classes */ use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer; defined( 'ABSPATH' ) || exit; /** * WC_Query Class. */ class WC_Query { /** * Query vars to add to wp. * * @var array */ public $query_vars = array(); /** * Reference to the main product query on the page. * * @var WP_Query */ private static $product_query; /** * Stores chosen attributes. * * @var array */ private static $chosen_attributes; /** * The instance of the class that helps filtering with the product attributes lookup table. * * @var Filterer */ private $filterer; /** * Constructor for the query class. Hooks in methods. */ public function __construct() { $this->filterer = wc_get_container()->get( Filterer::class ); add_action( 'init', array( $this, 'add_endpoints' ) ); if ( ! is_admin() ) { add_action( 'wp_loaded', array( $this, 'get_errors' ), 20 ); add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 ); } $this->init_query_vars(); } /** * Reset the chosen attributes so that get_layered_nav_chosen_attributes will get them from the query again. */ public static function reset_chosen_attributes() { self::$chosen_attributes = null; } /** * Get any errors from querystring. */ public function get_errors() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; if ( $error && ! wc_has_notice( $error, 'error' ) ) { wc_add_notice( $error, 'error' ); } } /** * Init query vars by loading options. */ public function init_query_vars() { // Query vars to add to WP. $this->query_vars = array( // Checkout actions. 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), // My account actions. 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), ); } /** * Get page title for an endpoint. * * @param string $endpoint Endpoint key. * @param string $action Optional action or variation within the endpoint. * * @since 2.3.0 * @since 4.6.0 Added $action parameter. * @return string The page title. */ public function get_endpoint_title( $endpoint, $action = '' ) { global $wp; switch ( $endpoint ) { case 'order-pay': $title = __( 'Pay for order', 'woocommerce' ); break; case 'order-received': $title = __( 'Order received', 'woocommerce' ); break; case 'orders': if ( ! empty( $wp->query_vars['orders'] ) ) { /* translators: %s: page */ $title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) ); } else { $title = __( 'Orders', 'woocommerce' ); } break; case 'view-order': $order = wc_get_order( $wp->query_vars['view-order'] ); /* translators: %s: order number */ $title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : ''; break; case 'downloads': $title = __( 'Downloads', 'woocommerce' ); break; case 'edit-account': $title = __( 'Account details', 'woocommerce' ); break; case 'edit-address': $title = __( 'Addresses', 'woocommerce' ); break; case 'payment-methods': $title = __( 'Payment methods', 'woocommerce' ); break; case 'add-payment-method': $title = __( 'Add payment method', 'woocommerce' ); break; case 'lost-password': if ( in_array( $action, array( 'rp', 'resetpass', 'newaccount' ), true ) ) { $title = __( 'Set password', 'woocommerce' ); } else { $title = __( 'Lost password', 'woocommerce' ); } break; default: $title = ''; break; } /** * Filters the page title used for my-account endpoints. * * @since 2.6.0 * @since 4.6.0 Added $action parameter. * * @see get_endpoint_title() * * @param string $title Default title. * @param string $endpoint Endpoint key. * @param string $action Optional action or variation within the endpoint. */ return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint, $action ); } /** * Endpoint mask describing the places the endpoint should be added. * * @since 2.6.2 * @return int */ public function get_endpoints_mask() { if ( 'page' === get_option( 'show_on_front' ) ) { $page_on_front = get_option( 'page_on_front' ); $myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' ); $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ), true ) ) { return EP_ROOT | EP_PAGES; } } return EP_PAGES; } /** * Add endpoints for query vars. */ public function add_endpoints() { $mask = $this->get_endpoints_mask(); foreach ( $this->get_query_vars() as $key => $var ) { if ( ! empty( $var ) ) { add_rewrite_endpoint( $var, $mask ); } } } /** * Add query vars. * * @param array $vars Query vars. * @return array */ public function add_query_vars( $vars ) { foreach ( $this->get_query_vars() as $key => $var ) { $vars[] = $key; } return $vars; } /** * Get query vars. * * @return array */ public function get_query_vars() { return apply_filters( 'woocommerce_get_query_vars', $this->query_vars ); } /** * Get query current active query var. * * @return string */ public function get_current_endpoint() { global $wp; foreach ( $this->get_query_vars() as $key => $value ) { if ( isset( $wp->query_vars[ $key ] ) ) { return $key; } } return ''; } /** * Parse the request and look for query vars - endpoints may not be supported. */ public function parse_request() { global $wp; // phpcs:disable WordPress.Security.NonceVerification.Recommended // Map query vars to their keys, or get them if endpoints are not supported. foreach ( $this->get_query_vars() as $key => $var ) { if ( isset( $_GET[ $var ] ) ) { $wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); } elseif ( isset( $wp->query_vars[ $var ] ) ) { $wp->query_vars[ $key ] = $wp->query_vars[ $var ]; } } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Are we currently on the front page? * * @param WP_Query $q Query instance. * @return bool */ private function is_showing_page_on_front( $q ) { return ( $q->is_home() && ! $q->is_posts_page ) && 'page' === get_option( 'show_on_front' ); } /** * Is the front page a page we define? * * @param int $page_id Page ID. * @return bool */ private function page_on_front_is( $page_id ) { return absint( get_option( 'page_on_front' ) ) === absint( $page_id ); } /** * Hook into pre_get_posts to do the main product query. * * @param WP_Query $q Query instance. */ public function pre_get_posts( $q ) { // We only want to affect the main query. if ( ! $q->is_main_query() ) { return; } // Fixes for queries on static homepages. if ( $this->is_showing_page_on_front( $q ) ) { // Fix for endpoints on the homepage. if ( ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) { $_query = wp_parse_args( $q->query ); if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->get_query_vars() ) ) ) { $q->is_page = true; $q->is_home = false; $q->is_singular = true; $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); add_filter( 'redirect_canonical', '__return_false' ); } } // When orderby is set, WordPress shows posts on the front-page. Get around that here. if ( $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) { $_query = wp_parse_args( $q->query ); if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) { $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); $q->is_page = true; $q->is_home = false; // WP supporting themes show post type archive. if ( current_theme_supports( 'woocommerce' ) ) { $q->set( 'post_type', 'product' ); } else { $q->is_singular = true; } } } elseif ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); $q->is_page = true; $q->is_home = false; $q->is_singular = true; } } // Fix product feeds. if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) { $q->is_comment_feed = false; } // Special check for shops with the PRODUCT POST TYPE ARCHIVE on front. if ( current_theme_supports( 'woocommerce' ) && $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) { // This is a front-page shop. $q->set( 'post_type', 'product' ); $q->set( 'page_id', '' ); if ( isset( $q->query['paged'] ) ) { $q->set( 'paged', $q->query['paged'] ); } // Define a variable so we know this is the front page shop later on. wc_maybe_define_constant( 'SHOP_IS_ON_FRONT', true ); // Get the actual WP page to avoid errors and let us use is_front_page(). // This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096. global $wp_post_types; $shop_page = get_post( wc_get_page_id( 'shop' ) ); $wp_post_types['product']->ID = $shop_page->ID; $wp_post_types['product']->post_title = $shop_page->post_title; $wp_post_types['product']->post_name = $shop_page->post_name; $wp_post_types['product']->post_type = $shop_page->post_type; $wp_post_types['product']->ancestors = get_ancestors( $shop_page->ID, $shop_page->post_type ); // Fix conditional Functions like is_front_page. $q->is_singular = false; $q->is_post_type_archive = true; $q->is_archive = true; $q->is_page = true; // Remove post type archive name from front page title tag. add_filter( 'post_type_archive_title', '__return_empty_string', 5 ); // Fix WP SEO. if ( class_exists( 'WPSEO_Meta' ) ) { add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) ); add_filter( 'wpseo_metakey', array( $this, 'wpseo_metakey' ) ); } } elseif ( ! $q->is_post_type_archive( 'product' ) && ! $q->is_tax( get_object_taxonomies( 'product' ) ) ) { // Only apply to product categories, the product post archive, the shop page, product tags, and product attribute taxonomies. return; } $this->product_query( $q ); } /** * Handler for the 'the_posts' WP filter. * * @param array $posts Posts from WP Query. * @param WP_Query $query Current query. * * @return array */ public function handle_get_posts( $posts, $query ) { if ( 'product_query' !== $query->get( 'wc_query' ) ) { return $posts; } $this->remove_product_query_filters( $posts ); return $posts; } /** * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure * all custom filters are removed. * * This is done here during the_posts filter. The input is not changed. * * @param array $posts Posts from WP Query. * @return array */ public function remove_product_query_filters( $posts ) { $this->remove_ordering_args(); remove_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); return $posts; } /** * This function used to be hooked to found_posts and adjust the posts count when the filtering by attribute * widget was used and variable products were present. Now it isn't hooked anymore and does nothing but return * the input unchanged, since the pull request in which it was introduced has been reverted. * * @since 4.4.0 * @param int $count Original posts count, as supplied by the found_posts filter. * @param WP_Query $query The current WP_Query object. * * @return int Adjusted posts count. */ public function adjust_posts_count( $count, $query ) { return $count; } /** * Instance version of get_layered_nav_chosen_attributes, needed for unit tests. * * @return array */ protected function get_layered_nav_chosen_attributes_inst() { return self::get_layered_nav_chosen_attributes(); } /** * Get the posts (or the ids of the posts) found in the current WP loop. * * @return array Array of posts or post ids. */ protected function get_current_posts() { return $GLOBALS['wp_query']->posts; } /** * WP SEO meta description. * * Hooked into wpseo_ hook already, so no need for function_exist. * * @return string */ public function wpseo_metadesc() { return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) ); } /** * WP SEO meta key. * * Hooked into wpseo_ hook already, so no need for function_exist. * * @return string */ public function wpseo_metakey() { return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) ); } /** * Query the products, applying sorting/ordering etc. * This applies to the main WordPress loop. * * @param WP_Query $q Query instance. */ public function product_query( $q ) { if ( ! is_feed() ) { $ordering = $this->get_catalog_ordering_args(); $q->set( 'orderby', $ordering['orderby'] ); $q->set( 'order', $ordering['order'] ); if ( isset( $ordering['meta_key'] ) ) { $q->set( 'meta_key', $ordering['meta_key'] ); } } // Query vars that affect posts shown. $q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) ); $q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) ); $q->set( 'wc_query', 'product_query' ); $q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) ); // Work out how many products to query. $q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) ); // Store reference to this query. self::$product_query = $q; // Additonal hooks to change WP Query. add_filter( 'posts_clauses', function( $args, $wp_query ) { return $this->product_query_post_clauses( $args, $wp_query ); }, 10, 2 ); add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); do_action( 'woocommerce_product_query', $q, $this ); } /** * Add extra clauses to the product query. * * @param array $args Product query clauses. * @param WP_Query $wp_query The current product query. * @return array The updated product query clauses array. */ private function product_query_post_clauses( $args, $wp_query ) { $args = $this->price_filter_post_clauses( $args, $wp_query ); $args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, $this->get_layered_nav_chosen_attributes() ); return $args; } /** * Remove the query. */ public function remove_product_query() { remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); } /** * Remove ordering queries. */ public function remove_ordering_args() { remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); } /** * Returns an array of arguments for ordering products based on the selected values. * * @param string $orderby Order by param. * @param string $order Order param. * @return array */ public function get_catalog_ordering_args( $orderby = '', $order = '' ) { // Get ordering from query string unless defined. if ( ! $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); if ( ! $orderby_value ) { if ( is_search() ) { $orderby_value = 'relevance'; } else { $orderby_value = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); } } // Get order + orderby args from string. $orderby_value = is_array( $orderby_value ) ? $orderby_value : explode( '-', $orderby_value ); $orderby = esc_attr( $orderby_value[0] ); $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : $order; } // Convert to correct format. $orderby = strtolower( is_array( $orderby ) ? (string) current( $orderby ) : (string) $orderby ); $order = strtoupper( is_array( $order ) ? (string) current( $order ) : (string) $order ); $args = array( 'orderby' => $orderby, 'order' => ( 'DESC' === $order ) ? 'DESC' : 'ASC', 'meta_key' => '', // @codingStandardsIgnoreLine ); switch ( $orderby ) { case 'id': $args['orderby'] = 'ID'; break; case 'menu_order': $args['orderby'] = 'menu_order title'; break; case 'title': $args['orderby'] = 'title'; $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; break; case 'relevance': $args['orderby'] = 'relevance'; $args['order'] = 'DESC'; break; case 'rand': $args['orderby'] = 'rand'; // @codingStandardsIgnoreLine break; case 'date': $args['orderby'] = 'date ID'; $args['order'] = ( 'ASC' === $order ) ? 'ASC' : 'DESC'; break; case 'price': $callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses'; add_filter( 'posts_clauses', array( $this, $callback ) ); break; case 'popularity': add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); break; case 'rating': add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); break; } return apply_filters( 'woocommerce_get_catalog_ordering_args', $args, $orderby, $order ); } /** * Custom query used to filter products by price. * * @since 3.6.0 * * @param array $args Query args. * @param WP_Query $wp_query WP_Query object. * * @return array */ public function price_filter_post_clauses( $args, $wp_query ) { global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) { return $args; } // phpcs:disable WordPress.Security.NonceVerification.Recommended $current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; $current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; // phpcs:enable WordPress.Security.NonceVerification.Recommended /** * Adjust if the store taxes are not displayed how they are stored. * Kicks in when prices excluding tax are displayed including tax. */ if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. $tax_rates = WC_Tax::get_rates( $tax_class ); if ( $tax_rates ) { $current_min_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) ); $current_max_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); } } $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= $wpdb->prepare( ' AND NOT (%f<wc_product_meta_lookup.min_price OR %f>wc_product_meta_lookup.max_price ) ', $current_max_price, $current_min_price ); return $args; } /** * Handle numeric price sorting. * * @param array $args Query args. * @return array */ public function order_by_price_asc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC '; return $args; } /** * Handle numeric price sorting. * * @param array $args Query args. * @return array */ public function order_by_price_desc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC '; return $args; } /** * WP Core does not let us change the sort direction for individual orderby params - https://core.trac.wordpress.org/ticket/17065. * * This lets us sort by meta value desc, and have a second orderby param. * * @param array $args Query args. * @return array */ public function order_by_popularity_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.total_sales DESC, wc_product_meta_lookup.product_id DESC '; return $args; } /** * Order by rating post clauses. * * @param array $args Query args. * @return array */ public function order_by_rating_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.average_rating DESC, wc_product_meta_lookup.rating_count DESC, wc_product_meta_lookup.product_id DESC '; return $args; } /** * Join wc_product_meta_lookup to posts if not already joined. * * @param string $sql SQL join. * @return string */ private function append_product_sorting_table_join( $sql ) { global $wpdb; if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; } return $sql; } /** * Appends meta queries to an array. * * @param array $meta_query Meta query. * @param bool $main_query If is main query. * @return array */ public function get_meta_query( $meta_query = array(), $main_query = false ) { if ( ! is_array( $meta_query ) ) { $meta_query = array(); } return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) ); } /** * Appends tax queries to an array. * * @param array $tax_query Tax query. * @param bool $main_query If is main query. * @return array */ public function get_tax_query( $tax_query = array(), $main_query = false ) { if ( ! is_array( $tax_query ) ) { $tax_query = array( 'relation' => 'AND', ); } if ( $main_query && ! $this->filterer->filtering_via_lookup_table_is_active() ) { // Layered nav filters on terms. foreach ( $this->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => $data['terms'], 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', 'include_children' => false, ); } } $product_visibility_terms = wc_get_product_visibility_term_ids(); $product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); // Hide out of stock products. if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $product_visibility_not_in[] = $product_visibility_terms['outofstock']; } // phpcs:disable WordPress.Security.NonceVerification.Recommended // Filter by rating. if ( isset( $_GET['rating_filter'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ); $rating_terms = array(); for ( $i = 1; $i <= 5; $i ++ ) { if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) { $rating_terms[] = $product_visibility_terms[ 'rated-' . $i ]; } } if ( ! empty( $rating_terms ) ) { $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $rating_terms, 'operator' => 'IN', 'rating_filter' => true, ); } } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! empty( $product_visibility_not_in ) ) { $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_not_in, 'operator' => 'NOT IN', ); } return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) ); } /** * Get the main query which product queries ran against. * * @return WP_Query */ public static function get_main_query() { return self::$product_query; } /** * Get the tax query which was used by the main query. * * @return array */ public static function get_main_tax_query() { $tax_query = isset( self::$product_query->tax_query, self::$product_query->tax_query->queries ) ? self::$product_query->tax_query->queries : array(); return $tax_query; } /** * Get the meta query which was used by the main query. * * @return array */ public static function get_main_meta_query() { $args = self::$product_query->query_vars; $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); return $meta_query; } /** * Based on WP_Query::parse_search */ public static function get_main_search_query_sql() { global $wpdb; $args = self::$product_query->query_vars; $search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array(); $sql = array(); foreach ( $search_terms as $term ) { // Terms prefixed with '-' should be excluded. $include = '-' !== substr( $term, 0, 1 ); if ( $include ) { $like_op = 'LIKE'; $andor_op = 'OR'; } else { $like_op = 'NOT LIKE'; $andor_op = 'AND'; $term = substr( $term, 1 ); } $like = '%' . $wpdb->esc_like( $term ) . '%'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); } if ( ! empty( $sql ) && ! is_user_logged_in() ) { $sql[] = "($wpdb->posts.post_password = '')"; } return implode( ' AND ', $sql ); } /** * Get an array of attributes and terms selected with the layered nav widget. * * @return array */ public static function get_layered_nav_chosen_attributes() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! is_array( self::$chosen_attributes ) ) { self::$chosen_attributes = array(); if ( ! empty( $_GET ) ) { foreach ( $_GET as $key => $value ) { if ( 0 === strpos( $key, 'filter_' ) ) { $attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) ); $taxonomy = wc_attribute_taxonomy_name( $attribute ); $filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array(); if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) { continue; } $query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding. self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); } } } } return self::$chosen_attributes; // phpcs:disable WordPress.Security.NonceVerification.Recommended } /** * Remove the add-to-cart param from pagination urls. * * @param string $url URL. * @return string */ public function remove_add_to_cart_pagination( $url ) { return remove_query_arg( 'add-to-cart', $url ); } /** * Return a meta query for filtering by rating. * * @deprecated 3.0.0 Replaced with taxonomy. * @return array */ public function rating_filter_meta_query() { return array(); } /** * Returns a meta query to handle product visibility. * * @deprecated 3.0.0 Replaced with taxonomy. * @param string $compare (default: 'IN'). * @return array */ public function visibility_meta_query( $compare = 'IN' ) { return array(); } /** * Returns a meta query to handle product stock status. * * @deprecated 3.0.0 Replaced with taxonomy. * @param string $status (default: 'instock'). * @return array */ public function stock_status_meta_query( $status = 'instock' ) { return array(); } /** * Layered nav init. * * @deprecated 2.6.0 */ public function layered_nav_init() { wc_deprecated_function( 'layered_nav_init', '2.6' ); } /** * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients. * * @deprecated 2.6.0 due to performance concerns */ public function get_products_in_view() { wc_deprecated_function( 'get_products_in_view', '2.6' ); } /** * Layered Nav post filter. * * @deprecated 2.6.0 due to performance concerns * * @param mixed $deprecated Deprecated. */ public function layered_nav_query( $deprecated ) { wc_deprecated_function( 'layered_nav_query', '2.6' ); } /** * Search post excerpt. * * @param string $where Where clause. * * @deprecated 3.2.0 - Not needed anymore since WordPress 4.5. */ public function search_post_excerpt( $where = '' ) { wc_deprecated_function( 'WC_Query::search_post_excerpt', '3.2.0', 'Excerpt added to search query by default since WordPress 4.5.' ); return $where; } /** * Remove the posts_where filter. * * @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated. */ public function remove_posts_where() { wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' ); } } includes/class-wc-datetime.php 0000644 00000004313 15132754524 0012402 0 ustar 00 <?php /** * WC Wrapper for PHP DateTime which adds support for gmt/utc offset when a * timezone is absent * * @since 3.0.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * Datetime class. */ class WC_DateTime extends DateTime { /** * UTC Offset, if needed. Only used when a timezone is not set. When * timezones are used this will equal 0. * * @var integer */ protected $utc_offset = 0; /** * Output an ISO 8601 date string in local (WordPress) timezone. * * @since 3.0.0 * @return string */ public function __toString() { return $this->format( DATE_ATOM ); } /** * Set UTC offset - this is a fixed offset instead of a timezone. * * @param int $offset Offset. */ public function set_utc_offset( $offset ) { $this->utc_offset = intval( $offset ); } /** * Get UTC offset if set, or default to the DateTime object's offset. */ public function getOffset() { return $this->utc_offset ? $this->utc_offset : parent::getOffset(); } /** * Set timezone. * * @param DateTimeZone $timezone DateTimeZone instance. * @return DateTime */ public function setTimezone( $timezone ) { $this->utc_offset = 0; return parent::setTimezone( $timezone ); } /** * Missing in PHP 5.2 so just here so it can be supported consistently. * * @since 3.0.0 * @return int */ public function getTimestamp() { return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); } /** * Get the timestamp with the WordPress timezone offset added or subtracted. * * @since 3.0.0 * @return int */ public function getOffsetTimestamp() { return $this->getTimestamp() + $this->getOffset(); } /** * Format a date based on the offset timestamp. * * @since 3.0.0 * @param string $format Date format. * @return string */ public function date( $format ) { return gmdate( $format, $this->getOffsetTimestamp() ); } /** * Return a localised date based on offset timestamp. Wrapper for date_i18n function. * * @since 3.0.0 * @param string $format Date format. * @return string */ public function date_i18n( $format = 'Y-m-d' ) { return date_i18n( $format, $this->getOffsetTimestamp() ); } } includes/admin/views/html-notice-no-shipping-methods.php 0000644 00000002450 15132754524 0017436 0 ustar 00 <?php /** * Admin View: Notice - No Shipping methods. * * @package WooCommerce\Admin\Notices */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'no_shipping_methods' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"> <?php esc_html_e( 'Dismiss', 'woocommerce' ); ?> </a> <p class="main"> <strong> <?php esc_html_e( 'Add shipping methods & zones', 'woocommerce' ); ?> </strong> </p> <p> <?php esc_html_e( 'Shipping is currently enabled, but you have not added any shipping methods to your shipping zones.', 'woocommerce' ); ?> </p> <p> <?php esc_html_e( 'Customers will not be able to purchase physical goods from your store until a shipping method is available.', 'woocommerce' ); ?> </p> <p class="submit"> <a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>"> <?php esc_html_e( 'Setup shipping zones', 'woocommerce' ); ?> </a> <a class="button-secondary" href="https://docs.woocommerce.com/document/setting-up-shipping-zones/"> <?php esc_html_e( 'Learn more about shipping zones', 'woocommerce' ); ?> </a> </p> </div> includes/admin/views/html-admin-page-addons.php 0000644 00000013310 15132754524 0015530 0 ustar 00 <?php /** * Admin View: Page - Addons * * @package WooCommerce\Admin * @var string $view * @var object $addons * @var object $promotions */ use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine; if ( ! defined( 'ABSPATH' ) ) { exit; } $current_section_name = __( 'Browse Categories', 'woocommerce' ); ?> <div class="woocommerce wc-addons-wrap"> <h1 class="screen-reader-text"><?php esc_html_e( 'Marketplace', 'woocommerce' ); ?></h1> <?php if ( $sections ) : ?> <div class="marketplace-header"> <h1 class="marketplace-header__title"><?php esc_html_e( 'WooCommerce Marketplace', 'woocommerce' ); ?></h1> <p class="marketplace-header__description"><?php esc_html_e( 'Grow your business with hundreds of free and paid WooCommerce extensions.', 'woocommerce' ); ?></p> <form class="marketplace-header__search-form" method="GET"> <input type="text" name="search" value="<?php echo esc_attr( ! empty( $search ) ? sanitize_text_field( wp_unslash( $search ) ) : '' ); ?>" placeholder="<?php esc_attr_e( 'Search for extensions', 'woocommerce' ); ?>" /> <button type="submit"> <span class="dashicons dashicons-search"></span> </button> <input type="hidden" name="page" value="wc-addons"> <input type="hidden" name="section" value="_all"> </form> </div> <div class="top-bar"> <div id="marketplace-current-section-dropdown" class="current-section-dropdown"> <ul> <?php foreach ( $sections as $section ) : ?> <?php if ( $current_section === $section->slug && '_featured' !== $section->slug ) { $current_section_name = $section->label; } ?> <li> <a class="<?php echo $current_section === $section->slug ? 'current' : ''; ?>" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons§ion=' . esc_attr( $section->slug ) ) ); ?>"> <?php echo esc_html( $section->label ); ?> </a> </li> <?php endforeach; ?> </ul> <div id="marketplace-current-section-name" class="current-section-name"><?php echo esc_html( $current_section_name ); ?></div> </div> </div> <div class="wp-header-end"></div> <div class="wrap"> <div class="marketplace-content-wrapper"> <?php if ( ! empty( $search ) && 0 === count( $addons ) ) : ?> <h1 class="search-form-title"> <?php esc_html_e( 'Sorry, could not find anything. Try searching again using a different term.', 'woocommerce' ); ?></p> </h1> <?php endif; ?> <?php if ( ! empty( $search ) && count( $addons ) > 0 ) : ?> <h1 class="search-form-title"> <?php // translators: search keyword. ?> <?php printf( esc_html__( 'Search results for "%s"', 'woocommerce' ), esc_html( sanitize_text_field( wp_unslash( $search ) ) ) ); ?> </h1> <?php endif; ?> <?php if ( '_featured' === $current_section ) : ?> <div class="addons-featured"> <?php WC_Admin_Addons::render_featured(); ?> </div> <?php endif; ?> <?php if ( '_featured' !== $current_section && $addons ) : ?> <?php if ( ! empty( $promotions ) && WC()->is_wc_admin_active() ) { foreach ( $promotions as $promotion ) { WC_Admin_Addons::output_search_promotion_block( $promotion ); } } ?> <ul class="products"> <?php foreach ( $addons as $addon ) : ?> <?php if ( 'shipping_methods' === $current_section ) { // Do not show USPS or Canada Post extensions for US and CA stores, respectively. $country = WC()->countries->get_base_country(); if ( 'US' === $country && false !== strpos( $addon->link, 'woocommerce.com/products/usps-shipping-method' ) ) { continue; } if ( 'CA' === $country && false !== strpos( $addon->link, 'woocommerce.com/products/canada-post-shipping-method' ) ) { continue; } } WC_Admin_Addons::render_product_card( $addon ); ?> <?php endforeach; ?> </ul> <?php endif; ?> </div> <?php else : ?> <?php /* translators: a url */ ?> <p><?php printf( wp_kses_post( __( 'Our catalog of WooCommerce Extensions can be found on WooCommerce.com here: <a href="%s">WooCommerce Extensions Catalog</a>', 'woocommerce' ) ), 'https://woocommerce.com/product-category/woocommerce-extensions/' ); ?></p> <?php endif; ?> <?php if ( 'Storefront' !== $theme['Name'] && '_featured' !== $current_section ) : ?> <?php $storefront_url = WC_Admin_Addons::add_in_app_purchase_url_params( 'https://woocommerce.com/storefront/?utm_source=extensionsscreen&utm_medium=product&utm_campaign=wcaddon' ); ?> <div class="storefront"> <a href="<?php echo esc_url( $storefront_url ); ?>" target="_blank"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/storefront.png" alt="<?php esc_attr_e( 'Storefront', 'woocommerce' ); ?>" /></a> <h2><?php esc_html_e( 'Looking for a WooCommerce theme?', 'woocommerce' ); ?></h2> <p><?php echo wp_kses_post( __( 'We recommend Storefront, the <em>official</em> WooCommerce theme.', 'woocommerce' ) ); ?></p> <p><?php echo wp_kses_post( __( 'Storefront is an intuitive, flexible and <strong>free</strong> WordPress theme offering deep integration with WooCommerce and many of the most popular customer-facing extensions.', 'woocommerce' ) ); ?></p> <p> <a href="<?php echo esc_url( $storefront_url ); ?>" target="_blank" class="button"><?php esc_html_e( 'Read all about it', 'woocommerce' ); ?></a> <a href="<?php echo esc_url( wp_nonce_url( self_admin_url( 'update.php?action=install-theme&theme=storefront' ), 'install-theme_storefront' ) ); ?>" class="button button-primary"><?php esc_html_e( 'Download & install', 'woocommerce' ); ?></a> </p> </div> <?php endif; ?> </div> </div> includes/admin/views/html-admin-page-product-export.php 0000644 00000007640 15132754524 0017270 0 ustar 00 <?php /** * Admin View: Product Export * * @package WooCommerce\Admin\Export */ if ( ! defined( 'ABSPATH' ) ) { exit; } wp_enqueue_script( 'wc-product-export' ); $exporter = new WC_Product_CSV_Exporter(); ?> <div class="wrap woocommerce"> <h1><?php esc_html_e( 'Export Products', 'woocommerce' ); ?></h1> <div class="woocommerce-exporter-wrapper"> <form class="woocommerce-exporter"> <header> <span class="spinner is-active"></span> <h2><?php esc_html_e( 'Export products to a CSV file', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'This tool allows you to generate and download a CSV file containing a list of all products.', 'woocommerce' ); ?></p> </header> <section> <table class="form-table woocommerce-exporter-options"> <tbody> <tr> <th scope="row"> <label for="woocommerce-exporter-columns"><?php esc_html_e( 'Which columns should be exported?', 'woocommerce' ); ?></label> </th> <td> <select id="woocommerce-exporter-columns" class="woocommerce-exporter-columns wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all columns', 'woocommerce' ); ?>"> <?php foreach ( $exporter->get_default_column_names() as $column_id => $column_name ) { echo '<option value="' . esc_attr( $column_id ) . '">' . esc_html( $column_name ) . '</option>'; } ?> <option value="downloads"><?php esc_html_e( 'Downloads', 'woocommerce' ); ?></option> <option value="attributes"><?php esc_html_e( 'Attributes', 'woocommerce' ); ?></option> </select> </td> </tr> <tr> <th scope="row"> <label for="woocommerce-exporter-types"><?php esc_html_e( 'Which product types should be exported?', 'woocommerce' ); ?></label> </th> <td> <select id="woocommerce-exporter-types" class="woocommerce-exporter-types wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all products', 'woocommerce' ); ?>"> <?php foreach ( WC_Admin_Exporters::get_product_types() as $value => $label ) { echo '<option value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>'; } ?> </select> </td> </tr> <tr> <th scope="row"> <label for="woocommerce-exporter-category"><?php esc_html_e( 'Which product category should be exported?', 'woocommerce' ); ?></label> </th> <td> <select id="woocommerce-exporter-category" class="woocommerce-exporter-category wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all categories', 'woocommerce' ); ?>"> <?php $categories = get_categories( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, ) ); foreach ( $categories as $category ) { echo '<option value="' . esc_attr( $category->slug ) . '">' . esc_html( $category->name ) . '</option>'; } ?> </select> </td> </tr> <tr> <th scope="row"> <label for="woocommerce-exporter-meta"><?php esc_html_e( 'Export custom meta?', 'woocommerce' ); ?></label> </th> <td> <input type="checkbox" id="woocommerce-exporter-meta" value="1" /> <label for="woocommerce-exporter-meta"><?php esc_html_e( 'Yes, export all custom meta', 'woocommerce' ); ?></label> </td> </tr> <?php do_action( 'woocommerce_product_export_row' ); ?> </tbody> </table> <progress class="woocommerce-exporter-progress" max="100" value="0"></progress> </section> <div class="wc-actions"> <button type="submit" class="woocommerce-exporter-button button button-primary" value="<?php esc_attr_e( 'Generate CSV', 'woocommerce' ); ?>"><?php esc_html_e( 'Generate CSV', 'woocommerce' ); ?></button> </div> </form> </div> </div> includes/admin/views/html-notice-base-table-missing.php 0000644 00000003111 15132754524 0017203 0 ustar 00 <?php /** * Admin View: Notice - Base table missing. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; ?> <div class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'base_tables_missing' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"> <?php esc_html_e( 'Dismiss', 'woocommerce' ); ?> </a> <p> <strong><?php esc_html_e( 'Database tables missing', 'woocommerce' ); ?></strong> </p> <p> <?php $verify_db_tool_available = array_key_exists( 'verify_db_tables', WC_Admin_Status::get_tools() ); $missing_tables = get_option( 'woocommerce_schema_missing_tables' ); if ( $verify_db_tool_available ) { echo wp_kses_post( sprintf( /* translators: %1%s: Missing tables (seperated by ",") %2$s: Link to check again */ __( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s. <a href="%2$s">Check again.</a>', 'woocommerce' ), esc_html( implode( ', ', $missing_tables ) ), wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=verify_db_tables' ), 'debug_action' ) ) ); } else { echo wp_kses_post( sprintf( /* translators: %1%s: Missing tables (seperated by ",") */ __( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s.', 'woocommerce' ), esc_html( implode( ', ', $missing_tables ) ) ) ); } ?> </p> </div> includes/admin/views/html-admin-page-status-logs.php 0000644 00000003713 15132754524 0016553 0 ustar 00 <?php /** * Admin View: Page - Status Logs * * @package WooCommerce\Admin\Logs */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <?php if ( $logs ) : ?> <div id="log-viewer-select"> <div class="alignleft"> <h2> <?php echo esc_html( $viewed_log ); ?> <?php if ( ! empty( $viewed_log ) ) : ?> <a class="page-title-action" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'handle' => sanitize_title( $viewed_log ) ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ), 'remove_log' ) ); ?>" class="button"><?php esc_html_e( 'Delete log', 'woocommerce' ); ?></a> <?php endif; ?> </h2> </div> <div class="alignright"> <form action="<?php echo esc_url( admin_url( 'admin.php?page=wc-status&tab=logs' ) ); ?>" method="post"> <select name="log_file"> <?php foreach ( $logs as $log_key => $log_file ) : ?> <?php $timestamp = filemtime( WC_LOG_DIR . $log_file ); $date = sprintf( /* translators: 1: last access date 2: last access time 3: last access timezone abbreviation */ __( '%1$s at %2$s %3$s', 'woocommerce' ), wp_date( wc_date_format(), $timestamp ), wp_date( wc_time_format(), $timestamp ), wp_date( 'T', $timestamp ) ); ?> <option value="<?php echo esc_attr( $log_key ); ?>" <?php selected( sanitize_title( $viewed_log ), $log_key ); ?>><?php echo esc_html( $log_file ); ?> (<?php echo esc_html( $date ); ?>)</option> <?php endforeach; ?> </select> <button type="submit" class="button" value="<?php esc_attr_e( 'View', 'woocommerce' ); ?>"><?php esc_html_e( 'View', 'woocommerce' ); ?></button> </form> </div> <div class="clear"></div> </div> <div id="log-viewer"> <pre><?php echo esc_html( file_get_contents( WC_LOG_DIR . $viewed_log ) ); ?></pre> </div> <?php else : ?> <div class="updated woocommerce-message inline"><p><?php esc_html_e( 'There are currently no logs to view.', 'woocommerce' ); ?></p></div> <?php endif; ?> includes/admin/views/html-notice-secure-connection.php 0000644 00000001525 15132754524 0017167 0 ustar 00 <?php /** * Admin View: Notice - Secure connection. * * @package WooCommerce\Admin\Notices */ defined( 'ABSPATH' ) || exit; ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'no_secure_connection' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <?php echo wp_kses_post( sprintf( /* translators: %s: documentation URL */ __( 'Your store does not appear to be using a secure connection. We highly recommend serving your entire website over an HTTPS connection to help keep customer data secure. <a href="%s">Learn more here.</a>', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/' ) ); ?> </p> </div> includes/admin/views/html-notice-regenerating-thumbnails.php 0000644 00000001152 15132754524 0020356 0 ustar 00 <?php /** * Admin View: Notice - Regenerating thumbnails. */ defined( 'ABSPATH' ) || exit; ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'regenerating_thumbnails' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Cancel thumbnail regeneration', 'woocommerce' ); ?></a> <p><?php esc_html_e( 'Thumbnail regeneration is running in the background. Depending on the amount of images in your store this may take a while.', 'woocommerce' ); ?></p> </div> includes/admin/views/html-notice-updating.php 0000644 00000002171 15132754524 0015355 0 ustar 00 <?php /** * Admin View: Notice - Updating * * @package WooCommerce\Admin */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } $pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' ); $cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' ); $cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' ); ?> <div id="message" class="updated woocommerce-message wc-connect"> <p> <strong><?php esc_html_e( 'WooCommerce database update', 'woocommerce' ); ?></strong><br> <?php esc_html_e( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ); ?> <?php if ( $cron_disabled ) { echo '<br>' . esc_html__( 'Note: WP CRON has been disabled on your install which may prevent this update from completing.', 'woocommerce' ); } ?> <a href="<?php echo esc_url( $pending_actions_url ); ?>"><?php echo esc_html( $cron_cta ); ?></a> </p> </div> includes/admin/views/html-admin-page-status-report.php 0000644 00000152722 15132754524 0017127 0 ustar 00 <?php /** * Admin View: Page - Status Report. * * @package WooCommerce */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; global $wpdb; $report = wc()->api->get_endpoint_data( '/wc/v3/system_status' ); $environment = $report['environment']; $database = $report['database']; $post_type_counts = isset( $report['post_type_counts'] ) ? $report['post_type_counts'] : array(); $active_plugins = $report['active_plugins']; $inactive_plugins = $report['inactive_plugins']; $dropins_mu_plugins = $report['dropins_mu_plugins']; $theme = $report['theme']; $security = $report['security']; $settings = $report['settings']; $wp_pages = $report['pages']; $plugin_updates = new WC_Plugin_Updates(); $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ); ?> <div class="updated woocommerce-message inline"> <p> <?php esc_html_e( 'Please copy and paste this information in your ticket when contacting support:', 'woocommerce' ); ?> </p> <p class="submit"> <a href="#" class="button-primary debug-report"><?php esc_html_e( 'Get system report', 'woocommerce' ); ?></a> <a class="button-secondary docs" href="https://docs.woocommerce.com/document/understanding-the-woocommerce-system-status-report/" target="_blank"> <?php esc_html_e( 'Understanding the status report', 'woocommerce' ); ?> </a> </p> <div id="debug-report"> <textarea readonly="readonly"></textarea> <p class="submit"> <button id="copy-for-support" class="button-primary" href="#" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>"> <?php esc_html_e( 'Copy for support', 'woocommerce' ); ?> </button> </p> <p class="copy-error hidden"> <?php esc_html_e( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ); ?> </p> </div> </div> <table class="wc_status_table widefat" cellspacing="0" id="status"> <thead> <tr> <th colspan="3" data-export-label="WordPress Environment"><h2><?php esc_html_e( 'WordPress environment', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="WordPress address (URL)"><?php esc_html_e( 'WordPress address (URL)', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The root URL of your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['site_url'] ); ?></td> </tr> <tr> <td data-export-label="Site address (URL)"><?php esc_html_e( 'Site address (URL)', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The homepage URL of your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['home_url'] ); ?></td> </tr> <tr> <td data-export-label="WC Version"><?php esc_html_e( 'WooCommerce version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WooCommerce installed on your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['version'] ); ?></td> </tr> <tr> <td data-export-label="REST API Version"><?php esc_html_e( 'WooCommerce REST API package', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce REST API package running on your site.', 'woocommerce' ) ); ?></td> <td> <?php $version = wc()->api->get_rest_api_package_version(); if ( ! is_null( $version ) ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( wc()->api->get_rest_api_package_path() ) . '</code></mark> '; } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the REST API package.', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="WC Blocks Version"><?php esc_html_e( 'WooCommerce Blocks package', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Blocks package running on your site.', 'woocommerce' ) ); ?></td> <td> <?php if ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) ) { $version = \Automattic\WooCommerce\Blocks\Package::get_version(); $path = \Automattic\WooCommerce\Blocks\Package::get_path(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } else { $version = null; } if ( ! is_null( $version ) ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $path ) . '</code></mark> '; } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the Blocks package.', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="Action Scheduler Version"><?php esc_html_e( 'Action Scheduler package', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Action Scheduler package running on your site.', 'woocommerce' ) ); ?></td> <td> <?php if ( class_exists( 'ActionScheduler_Versions' ) && class_exists( 'ActionScheduler' ) ) { $version = ActionScheduler_Versions::instance()->latest_version(); $path = ActionScheduler::plugin_path( '' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } else { $version = null; } if ( ! is_null( $version ) ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $path ) . '</code></mark> '; } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the Action Scheduler package.', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="WC Admin Version"><?php esc_html_e( 'WooCommerce Admin package', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Admin package running on your site.', 'woocommerce' ) ); ?></td> <td> <?php $wc_admin_path = null; if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) { // Plugin version of WC Admin. $version = WC_ADMIN_VERSION_NUMBER; $package_active = false; } elseif ( class_exists( '\Automattic\WooCommerce\Admin\Composer\Package' ) ) { if ( WC()->is_wc_admin_active() ) { // Fully active package version of WC Admin. $version = \Automattic\WooCommerce\Admin\Composer\Package::get_active_version(); $package_active = \Automattic\WooCommerce\Admin\Composer\Package::is_package_active(); } else { // with WP version < 5.3, package is present, but inactive. $version = sprintf( /* translators: %s: Version number of wc-admin package */ __( 'Inactive %s', 'woocommerce' ), \Automattic\WooCommerce\Admin\Composer\Package::VERSION ); $package_active = false; } $wc_admin_path = \Automattic\WooCommerce\Admin\Composer\Package::get_path(); } else { $version = null; } if ( ! is_null( $version ) ) { if ( ! isset( $wc_admin_path ) ) { if ( defined( 'WC_ADMIN_PLUGIN_FILE' ) ) { $wc_admin_path = dirname( WC_ADMIN_PLUGIN_FILE ); } else { $wc_admin_path = __( 'Active Plugin', 'woocommerce' ); } } if ( WC()->is_wc_admin_active() ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $wc_admin_path ) . '</code></mark> '; } else { echo '<span class="dashicons dashicons-no-alt"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $wc_admin_path ) . '</code> '; } } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the WC Admin package.', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="Log Directory Writable"><?php esc_html_e( 'Log directory writable', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Several WooCommerce extensions can write logs which makes debugging problems easier. The directory must be writable for this to happen.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['log_directory_writable'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> <code class="private">' . esc_html( $environment['log_directory'] ) . '</code></mark> '; } else { /* Translators: %1$s: Log directory, %2$s: Log directory constant */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'To allow logging, make %1$s writable or define a custom %2$s.', 'woocommerce' ), '<code>' . esc_html( $environment['log_directory'] ) . '</code>', '<code>WC_LOG_DIR</code>' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="WP Version"><?php esc_html_e( 'WordPress version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WordPress installed on your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $latest_version = get_transient( 'woocommerce_system_status_wp_version_check' ); if ( false === $latest_version ) { $version_check = wp_remote_get( 'https://api.wordpress.org/core/version-check/1.7/' ); $api_response = json_decode( wp_remote_retrieve_body( $version_check ), true ); if ( $api_response && isset( $api_response['offers'], $api_response['offers'][0], $api_response['offers'][0]['version'] ) ) { $latest_version = $api_response['offers'][0]['version']; } else { $latest_version = $environment['wp_version']; } set_transient( 'woocommerce_system_status_wp_version_check', $latest_version, DAY_IN_SECONDS ); } if ( version_compare( $environment['wp_version'], $latest_version, '<' ) ) { /* Translators: %1$s: Current version, %2$s: New version */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - There is a newer version of WordPress available (%2$s)', 'woocommerce' ), esc_html( $environment['wp_version'] ), esc_html( $latest_version ) ) . '</mark>'; } else { echo '<mark class="yes">' . esc_html( $environment['wp_version'] ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="WP Multisite"><?php esc_html_e( 'WordPress multisite', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether or not you have WordPress Multisite enabled.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo ( $environment['wp_multisite'] ) ? '<span class="dashicons dashicons-yes"></span>' : '–'; ?></td> </tr> <tr> <td data-export-label="WP Memory Limit"><?php esc_html_e( 'WordPress memory limit', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The maximum amount of memory (RAM) that your site can use at one time.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['wp_memory_limit'] < 67108864 ) { /* Translators: %1$s: Memory limit, %2$s: Docs link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend setting memory to at least 64MB. See: %2$s', 'woocommerce' ), esc_html( size_format( $environment['wp_memory_limit'] ) ), '<a href="https://wordpress.org/support/article/editing-wp-config-php/#increasing-memory-allocated-to-php" target="_blank">' . esc_html__( 'Increasing memory allocated to PHP', 'woocommerce' ) . '</a>' ) . '</mark>'; } else { echo '<mark class="yes">' . esc_html( size_format( $environment['wp_memory_limit'] ) ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="WP Debug Mode"><?php esc_html_e( 'WordPress debug mode', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WordPress is in Debug Mode.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['wp_debug_mode'] ) : ?> <mark class="yes"><span class="dashicons dashicons-yes"></span></mark> <?php else : ?> <mark class="no">–</mark> <?php endif; ?> </td> </tr> <tr> <td data-export-label="WP Cron"><?php esc_html_e( 'WordPress cron', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WP Cron Jobs are enabled.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['wp_cron'] ) : ?> <mark class="yes"><span class="dashicons dashicons-yes"></span></mark> <?php else : ?> <mark class="no">–</mark> <?php endif; ?> </td> </tr> <tr> <td data-export-label="Language"><?php esc_html_e( 'Language', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The current language used by WordPress. Default = English', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['language'] ); ?></td> </tr> <tr> <td data-export-label="External object cache"><?php esc_html_e( 'External object cache', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WordPress is using an external object cache.', 'woocommerce' ) ); ?></td> <td> <?php if ( $environment['external_object_cache'] ) : ?> <mark class="yes"><span class="dashicons dashicons-yes"></span></mark> <?php else : ?> <mark class="no">–</mark> <?php endif; ?> </td> </tr> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Server Environment"><h2><?php esc_html_e( 'Server environment', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="Server Info"><?php esc_html_e( 'Server info', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Information about the web server that is currently hosting your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['server_info'] ); ?></td> </tr> <tr> <td data-export-label="PHP Version"><?php esc_html_e( 'PHP version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The version of PHP installed on your hosting server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( version_compare( $environment['php_version'], '7.2', '>=' ) ) { echo '<mark class="yes">' . esc_html( $environment['php_version'] ) . '</mark>'; } else { $update_link = ' <a href="https://docs.woocommerce.com/document/how-to-update-your-php-version/" target="_blank">' . esc_html__( 'How to update your PHP version', 'woocommerce' ) . '</a>'; $class = 'error'; if ( version_compare( $environment['php_version'], '5.4', '<' ) ) { $notice = '<span class="dashicons dashicons-warning"></span> ' . __( 'WooCommerce will run under this version of PHP, however, some features such as geolocation are not compatible. Support for this version will be dropped in the next major release. We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link; } elseif ( version_compare( $environment['php_version'], '5.6', '<' ) ) { $notice = '<span class="dashicons dashicons-warning"></span> ' . __( 'WooCommerce will run under this version of PHP, however, it has reached end of life. We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link; } elseif ( version_compare( $environment['php_version'], '7.2', '<' ) ) { $notice = __( 'We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link; $class = 'recommendation'; } echo '<mark class="' . esc_attr( $class ) . '">' . esc_html( $environment['php_version'] ) . ' - ' . wp_kses_post( $notice ) . '</mark>'; } ?> </td> </tr> <?php if ( function_exists( 'ini_get' ) ) : ?> <tr> <td data-export-label="PHP Post Max Size"><?php esc_html_e( 'PHP post max size', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The largest filesize that can be contained in one post.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( size_format( $environment['php_post_max_size'] ) ); ?></td> </tr> <tr> <td data-export-label="PHP Time Limit"><?php esc_html_e( 'PHP time limit', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The amount of time (in seconds) that your site will spend on a single operation before timing out (to avoid server lockups)', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['php_max_execution_time'] ); ?></td> </tr> <tr> <td data-export-label="PHP Max Input Vars"><?php esc_html_e( 'PHP max input vars', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The maximum number of variables your server can use for a single function to avoid overloads.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['php_max_input_vars'] ); ?></td> </tr> <tr> <td data-export-label="cURL Version"><?php esc_html_e( 'cURL version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The version of cURL installed on your server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $environment['curl_version'] ); ?></td> </tr> <tr> <td data-export-label="SUHOSIN Installed"><?php esc_html_e( 'SUHOSIN installed', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Suhosin is an advanced protection system for PHP installations. It was designed to protect your servers on the one hand against a number of well known problems in PHP applications and on the other hand against potential unknown vulnerabilities within these applications or the PHP core itself. If enabled on your server, Suhosin may need to be configured to increase its data submission limits.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo $environment['suhosin_installed'] ? '<span class="dashicons dashicons-yes"></span>' : '–'; ?></td> </tr> <?php endif; ?> <?php if ( $environment['mysql_version'] ) : ?> <tr> <td data-export-label="MySQL Version"><?php esc_html_e( 'MySQL version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The version of MySQL installed on your hosting server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( version_compare( $environment['mysql_version'], '5.6', '<' ) && ! strstr( $environment['mysql_version_string'], 'MariaDB' ) ) { /* Translators: %1$s: MySQL version, %2$s: Recommended MySQL version. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend a minimum MySQL version of 5.6. See: %2$s', 'woocommerce' ), esc_html( $environment['mysql_version_string'] ), '<a href="https://wordpress.org/about/requirements/" target="_blank">' . esc_html__( 'WordPress requirements', 'woocommerce' ) . '</a>' ) . '</mark>'; } else { echo '<mark class="yes">' . esc_html( $environment['mysql_version_string'] ) . '</mark>'; } ?> </td> </tr> <?php endif; ?> <tr> <td data-export-label="Max Upload Size"><?php esc_html_e( 'Max upload size', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The largest filesize that can be uploaded to your WordPress installation.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( size_format( $environment['max_upload_size'] ) ); ?></td> </tr> <tr> <td data-export-label="Default Timezone is UTC"><?php esc_html_e( 'Default timezone is UTC', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The default timezone for your server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( 'UTC' !== $environment['default_timezone'] ) { /* Translators: %s: default timezone.. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Default timezone is %s - it should be UTC', 'woocommerce' ), esc_html( $environment['default_timezone'] ) ) . '</mark>'; } else { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } ?> </td> </tr> <tr> <td data-export-label="fsockopen/cURL"><?php esc_html_e( 'fsockopen/cURL', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Payment gateways can use cURL to communicate with remote servers to authorize payments, other plugins may also use it when communicating with remote services.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['fsockopen_or_curl_enabled'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Your server does not have fsockopen or cURL enabled - PayPal IPN and other scripts which communicate with other servers will not work. Contact your hosting provider.', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="SoapClient"><?php esc_html_e( 'SoapClient', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Some webservices like shipping use SOAP to get information from remote servers, for example, live shipping quotes from FedEx require SOAP to be installed.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['soapclient_enabled'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s classname and link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not have the %s class enabled - some gateway plugins which use SOAP may not work as expected.', 'woocommerce' ), '<a href="https://php.net/manual/en/class.soapclient.php">SoapClient</a>' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="DOMDocument"><?php esc_html_e( 'DOMDocument', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'HTML/Multipart emails use DOMDocument to generate inline CSS in templates.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['domdocument_enabled'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s: classname and link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not have the %s class enabled - HTML/Multipart emails, and also some extensions, will not work without DOMDocument.', 'woocommerce' ), '<a href="https://php.net/manual/en/class.domdocument.php">DOMDocument</a>' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="GZip"><?php esc_html_e( 'GZip', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'GZip (gzopen) is used to open the GEOIP database from MaxMind.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['gzip_enabled'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s: classname and link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not support the %s function - this is required to use the GeoIP database from MaxMind.', 'woocommerce' ), '<a href="https://php.net/manual/en/zlib.installation.php">gzopen</a>' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="Multibyte String"><?php esc_html_e( 'Multibyte string', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Multibyte String (mbstring) is used to convert character encoding, like for emails or converting characters to lowercase.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['mbstring_enabled'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s: classname and link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not support the %s functions - this is required for better character encoding. Some fallbacks will be used instead for it.', 'woocommerce' ), '<a href="https://php.net/manual/en/mbstring.installation.php">mbstring</a>' ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="Remote Post"><?php esc_html_e( 'Remote post', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'PayPal uses this method of communicating when sending back transaction information.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['remote_post_successful'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s: function name. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%s failed. Contact your hosting provider.', 'woocommerce' ), 'wp_remote_post()' ) . ' ' . esc_html( $environment['remote_post_response'] ) . '</mark>'; } ?> </td> </tr> <tr> <td data-export-label="Remote Get"><?php esc_html_e( 'Remote get', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'WooCommerce plugins may use this method of communication when checking for plugin updates.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $environment['remote_get_successful'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s: function name. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%s failed. Contact your hosting provider.', 'woocommerce' ), 'wp_remote_get()' ) . ' ' . esc_html( $environment['remote_get_response'] ) . '</mark>'; } ?> </td> </tr> <?php $rows = apply_filters( 'woocommerce_system_status_environment_rows', array() ); foreach ( $rows as $row ) { if ( ! empty( $row['success'] ) ) { $css_class = 'yes'; $icon = '<span class="dashicons dashicons-yes"></span>'; } else { $css_class = 'error'; $icon = '<span class="dashicons dashicons-no-alt"></span>'; } ?> <tr> <td data-export-label="<?php echo esc_attr( $row['name'] ); ?>"><?php echo esc_html( $row['name'] ); ?>:</td> <td class="help"><?php echo esc_html( isset( $row['help'] ) ? $row['help'] : '' ); ?></td> <td> <mark class="<?php echo esc_attr( $css_class ); ?>"> <?php echo wp_kses_post( $icon ); ?> <?php echo wp_kses_data( ! empty( $row['note'] ) ? $row['note'] : '' ); ?> </mark> </td> </tr> <?php } ?> </tbody> </table> <table id="status-database" class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Database"> <h2> <?php esc_html_e( 'Database', 'woocommerce' ); self::output_tables_info(); ?> </h2> </th> </tr> </thead> <tbody> <tr> <td data-export-label="WC Database Version"><?php esc_html_e( 'WooCommerce database version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The database version for WooCommerce. Note that it may not match WooCommerce core version and that is normal.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $database['wc_database_version'] ); ?></td> </tr> <tr> <td data-export-label="WC Database Prefix"><?php esc_html_e( 'Database prefix', 'woocommerce' ); ?></td> <td class="help"> </td> <td> <?php if ( strlen( $database['database_prefix'] ) > 20 ) { /* Translators: %1$s: Database prefix, %2$s: Docs link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend using a prefix with less than 20 characters. See: %2$s', 'woocommerce' ), esc_html( $database['database_prefix'] ), '<a href="https://docs.woocommerce.com/document/completed-order-email-doesnt-contain-download-links/#section-2" target="_blank">' . esc_html__( 'How to update your database table prefix', 'woocommerce' ) . '</a>' ) . '</mark>'; } else { echo '<mark class="yes">' . esc_html( $database['database_prefix'] ) . '</mark>'; } ?> </td> </tr> <?php if ( ! empty( $database['database_size'] ) && ! empty( $database['database_tables'] ) ) : ?> <tr> <td><?php esc_html_e( 'Total Database Size', 'woocommerce' ); ?></td> <td class="help"> </td> <td><?php printf( '%.2fMB', esc_html( $database['database_size']['data'] + $database['database_size']['index'] ) ); ?></td> </tr> <tr> <td><?php esc_html_e( 'Database Data Size', 'woocommerce' ); ?></td> <td class="help"> </td> <td><?php printf( '%.2fMB', esc_html( $database['database_size']['data'] ) ); ?></td> </tr> <tr> <td><?php esc_html_e( 'Database Index Size', 'woocommerce' ); ?></td> <td class="help"> </td> <td><?php printf( '%.2fMB', esc_html( $database['database_size']['index'] ) ); ?></td> </tr> <?php foreach ( $database['database_tables']['woocommerce'] as $table => $table_data ) { ?> <tr> <td><?php echo esc_html( $table ); ?></td> <td class="help"> </td> <td> <?php if ( ! $table_data ) { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Table does not exist', 'woocommerce' ) . '</mark>'; } else { /* Translators: %1$f: Table size, %2$f: Index size, %3$s Engine. */ printf( esc_html__( 'Data: %1$.2fMB + Index: %2$.2fMB + Engine %3$s', 'woocommerce' ), esc_html( wc_format_decimal( $table_data['data'], 2 ) ), esc_html( wc_format_decimal( $table_data['index'], 2 ) ), esc_html( $table_data['engine'] ) ); } ?> </td> </tr> <?php } ?> <?php foreach ( $database['database_tables']['other'] as $table => $table_data ) { ?> <tr> <td><?php echo esc_html( $table ); ?></td> <td class="help"> </td> <td> <?php /* Translators: %1$f: Table size, %2$f: Index size, %3$s Engine. */ printf( esc_html__( 'Data: %1$.2fMB + Index: %2$.2fMB + Engine %3$s', 'woocommerce' ), esc_html( wc_format_decimal( $table_data['data'], 2 ) ), esc_html( wc_format_decimal( $table_data['index'], 2 ) ), esc_html( $table_data['engine'] ) ); ?> </td> </tr> <?php } ?> <?php else : ?> <tr> <td><?php esc_html_e( 'Database information:', 'woocommerce' ); ?></td> <td class="help"> </td> <td> <?php esc_html_e( 'Unable to retrieve database information. Usually, this is not a problem, and it only means that your install is using a class that replaces the WordPress database class (e.g., HyperDB) and WooCommerce is unable to get database information.', 'woocommerce' ); ?> </td> </tr> <?php endif; ?> </tbody> </table> <?php if ( $post_type_counts ) : ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Post Type Counts"><h2><?php esc_html_e( 'Post Type Counts', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <?php foreach ( $post_type_counts as $ptype ) { ?> <tr> <td><?php echo esc_html( $ptype['type'] ); ?></td> <td class="help"> </td> <td><?php echo absint( $ptype['count'] ); ?></td> </tr> <?php } ?> </tbody> </table> <?php endif; ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Security"><h2><?php esc_html_e( 'Security', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="Secure connection (HTTPS)"><?php esc_html_e( 'Secure connection (HTTPS)', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Is the connection to your store secure?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $security['secure_connection'] ) : ?> <mark class="yes"><span class="dashicons dashicons-yes"></span></mark> <?php else : ?> <mark class="error"><span class="dashicons dashicons-warning"></span> <?php /* Translators: %s: docs link. */ echo wp_kses_post( sprintf( __( 'Your store is not using HTTPS. <a href="%s" target="_blank">Learn more about HTTPS and SSL Certificates</a>.', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/' ) ); ?> </mark> <?php endif; ?> </td> </tr> <tr> <td data-export-label="Hide errors from visitors"><?php esc_html_e( 'Hide errors from visitors', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Error messages can contain sensitive information about your store environment. These should be hidden from untrusted visitors.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $security['hide_errors'] ) : ?> <mark class="yes"><span class="dashicons dashicons-yes"></span></mark> <?php else : ?> <mark class="error"><span class="dashicons dashicons-warning"></span><?php esc_html_e( 'Error messages should not be shown to visitors.', 'woocommerce' ); ?></mark> <?php endif; ?> </td> </tr> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Active Plugins (<?php echo count( $active_plugins ); ?>)"><h2><?php esc_html_e( 'Active plugins', 'woocommerce' ); ?> (<?php echo count( $active_plugins ); ?>)</h2></th> </tr> </thead> <tbody> <?php self::output_plugins_info( $active_plugins, $untested_plugins ); ?> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Inactive Plugins (<?php echo count( $inactive_plugins ); ?>)"><h2><?php esc_html_e( 'Inactive plugins', 'woocommerce' ); ?> (<?php echo count( $inactive_plugins ); ?>)</h2></th> </tr> </thead> <tbody> <?php self::output_plugins_info( $inactive_plugins, $untested_plugins ); ?> </tbody> </table> <?php if ( 0 < count( $dropins_mu_plugins['dropins'] ) ) : ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Dropin Plugins (<?php echo count( $dropins_mu_plugins['dropins'] ); ?>)"><h2><?php esc_html_e( 'Dropin Plugins', 'woocommerce' ); ?> (<?php echo count( $dropins_mu_plugins['dropins'] ); ?>)</h2></th> </tr> </thead> <tbody> <?php foreach ( $dropins_mu_plugins['dropins'] as $dropin ) { ?> <tr> <td><?php echo wp_kses_post( $dropin['plugin'] ); ?></td> <td class="help"> </td> <td><?php echo wp_kses_post( $dropin['name'] ); ?> </tr> <?php } ?> </tbody> </table> <?php endif; if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) : ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Must Use Plugins (<?php echo count( $dropins_mu_plugins['mu_plugins'] ); ?>)"><h2><?php esc_html_e( 'Must Use Plugins', 'woocommerce' ); ?> (<?php echo count( $dropins_mu_plugins['mu_plugins'] ); ?>)</h2></th> </tr> </thead> <tbody> <?php foreach ( $dropins_mu_plugins['mu_plugins'] as $mu_plugin ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $plugin_name = esc_html( $mu_plugin['name'] ); if ( ! empty( $mu_plugin['url'] ) ) { $plugin_name = '<a href="' . esc_url( $mu_plugin['url'] ) . '" aria-label="' . esc_attr__( 'Visit plugin homepage', 'woocommerce' ) . '" target="_blank">' . $plugin_name . '</a>'; } ?> <tr> <td><?php echo wp_kses_post( $plugin_name ); ?></td> <td class="help"> </td> <td> <?php /* translators: %s: plugin author */ printf( esc_html__( 'by %s', 'woocommerce' ), esc_html( $mu_plugin['author_name'] ) ); echo ' – ' . esc_html( $mu_plugin['version'] ); ?> </tr> <?php } ?> </tbody> </table> <?php endif; ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Settings"><h2><?php esc_html_e( 'Settings', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="API Enabled"><?php esc_html_e( 'API enabled', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site have REST API enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo $settings['api_enabled'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td> </tr> <tr> <td data-export-label="Force SSL"><?php esc_html_e( 'Force SSL', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site force a SSL Certificate for transactions?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo $settings['force_ssl'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td> </tr> <tr> <td data-export-label="Currency"><?php esc_html_e( 'Currency', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'What currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $settings['currency'] ); ?> (<?php echo esc_html( $settings['currency_symbol'] ); ?>)</td> </tr> <tr> <td data-export-label="Currency Position"><?php esc_html_e( 'Currency position', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The position of the currency symbol.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $settings['currency_position'] ); ?></td> </tr> <tr> <td data-export-label="Thousand Separator"><?php esc_html_e( 'Thousand separator', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The thousand separator of displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $settings['thousand_separator'] ); ?></td> </tr> <tr> <td data-export-label="Decimal Separator"><?php esc_html_e( 'Decimal separator', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The decimal separator of displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $settings['decimal_separator'] ); ?></td> </tr> <tr> <td data-export-label="Number of Decimals"><?php esc_html_e( 'Number of decimals', 'woocommerce' ); ?></td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The number of decimal points shown in displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $settings['number_of_decimals'] ); ?></td> </tr> <tr> <td data-export-label="Taxonomies: Product Types"><?php esc_html_e( 'Taxonomies: Product types', 'woocommerce' ); ?></th> <td class="help"><?php echo wc_help_tip( esc_html__( 'A list of taxonomy terms that can be used in regard to order/product statuses.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $display_terms = array(); foreach ( $settings['taxonomies'] as $slug => $name ) { $display_terms[] = strtolower( $name ) . ' (' . $slug . ')'; } echo implode( ', ', array_map( 'esc_html', $display_terms ) ); ?> </td> </tr> <tr> <td data-export-label="Taxonomies: Product Visibility"><?php esc_html_e( 'Taxonomies: Product visibility', 'woocommerce' ); ?></th> <td class="help"><?php echo wc_help_tip( esc_html__( 'A list of taxonomy terms used for product visibility.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $display_terms = array(); foreach ( $settings['product_visibility_terms'] as $slug => $name ) { $display_terms[] = strtolower( $name ) . ' (' . $slug . ')'; } echo implode( ', ', array_map( 'esc_html', $display_terms ) ); ?> </td> </tr> <tr> <td data-export-label="Connected to WooCommerce.com"><?php esc_html_e( 'Connected to WooCommerce.com', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Is your site connected to WooCommerce.com?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo 'yes' === $settings['woocommerce_com_connected'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td> </tr> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="WC Pages"><h2><?php esc_html_e( 'WooCommerce pages', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <?php $alt = 1; foreach ( $wp_pages as $_page ) { $found_error = false; if ( $_page['page_id'] ) { /* Translators: %s: page name. */ $page_name = '<a href="' . get_edit_post_link( $_page['page_id'] ) . '" aria-label="' . sprintf( esc_html__( 'Edit %s page', 'woocommerce' ), esc_html( $_page['page_name'] ) ) . '">' . esc_html( $_page['page_name'] ) . '</a>'; } else { $page_name = esc_html( $_page['page_name'] ); } echo '<tr><td data-export-label="' . esc_attr( $page_name ) . '">' . wp_kses_post( $page_name ) . ':</td>'; /* Translators: %s: page name. */ echo '<td class="help">' . wc_help_tip( sprintf( esc_html__( 'The URL of your %s page (along with the Page ID).', 'woocommerce' ), $page_name ) ) . '</td><td>'; // Page ID check. if ( ! $_page['page_set'] ) { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Page not set', 'woocommerce' ) . '</mark>'; $found_error = true; } elseif ( ! $_page['page_exists'] ) { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Page ID is set, but the page does not exist', 'woocommerce' ) . '</mark>'; $found_error = true; } elseif ( ! $_page['page_visible'] ) { /* Translators: %s: docs link. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'Page visibility should be <a href="%s" target="_blank">public</a>', 'woocommerce' ), 'https://wordpress.org/support/article/content-visibility/' ) ) . '</mark>'; $found_error = true; } else { // Shortcode and block check. if ( $_page['shortcode_required'] || $_page['block_required'] ) { if ( ! $_page['shortcode_present'] && ! $_page['block_present'] ) { /* Translators: %1$s: shortcode text, %2$s: block slug. */ echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ $found_error = true; } } } if ( ! $found_error ) { echo '<mark class="yes">#' . absint( $_page['page_id'] ) . ' - ' . esc_html( str_replace( home_url(), '', get_permalink( $_page['page_id'] ) ) ) . '</mark>'; } echo '</td></tr>'; } ?> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Theme"><h2><?php esc_html_e( 'Theme', 'woocommerce' ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="Name"><?php esc_html_e( 'Name', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The name of the current active theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $theme['name'] ); ?></td> </tr> <tr> <td data-export-label="Version"><?php esc_html_e( 'Version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The installed version of the current active theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( version_compare( $theme['version'], $theme['version_latest'], '<' ) ) { /* translators: 1: current version. 2: latest version */ echo esc_html( sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $theme['version'], $theme['version_latest'] ) ); } else { echo esc_html( $theme['version'] ); } ?> </td> </tr> <tr> <td data-export-label="Author URL"><?php esc_html_e( 'Author URL', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The theme developers URL.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $theme['author_url'] ); ?></td> </tr> <tr> <td data-export-label="Child Theme"><?php esc_html_e( 'Child theme', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not the current theme is a child theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $theme['is_child_theme'] ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } else { /* Translators: %s docs link. */ echo '<span class="dashicons dashicons-no-alt"></span> – ' . wp_kses_post( sprintf( __( 'If you are modifying WooCommerce on a parent theme that you did not build personally we recommend using a child theme. See: <a href="%s" target="_blank">How to create a child theme</a>', 'woocommerce' ), 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ) ); } ?> </td> </tr> <?php if ( $theme['is_child_theme'] ) : ?> <tr> <td data-export-label="Parent Theme Name"><?php esc_html_e( 'Parent theme name', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The name of the parent theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $theme['parent_name'] ); ?></td> </tr> <tr> <td data-export-label="Parent Theme Version"><?php esc_html_e( 'Parent theme version', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The installed version of the parent theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php echo esc_html( $theme['parent_version'] ); if ( version_compare( $theme['parent_version'], $theme['parent_version_latest'], '<' ) ) { /* translators: %s: parent theme latest version */ echo ' – <strong style="color:red;">' . sprintf( esc_html__( '%s is available', 'woocommerce' ), esc_html( $theme['parent_version_latest'] ) ) . '</strong>'; } ?> </td> </tr> <tr> <td data-export-label="Parent Theme Author URL"><?php esc_html_e( 'Parent theme author URL', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The parent theme developers URL.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $theme['parent_author_url'] ); ?></td> </tr> <?php endif ?> <tr> <td data-export-label="WooCommerce Support"><?php esc_html_e( 'WooCommerce support', 'woocommerce' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not the current active theme declares WooCommerce support.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( ! $theme['has_woocommerce_support'] ) { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not declared', 'woocommerce' ) . '</mark>'; } else { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'; } ?> </td> </tr> </tbody> </table> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Templates"><h2><?php esc_html_e( 'Templates', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows any files that are overriding the default WooCommerce template pages.', 'woocommerce' ) ); ?></h2></th> </tr> </thead> <tbody> <?php if ( $theme['has_woocommerce_file'] ) : ?> <tr> <td data-export-label="Archive Template"><?php esc_html_e( 'Archive template', 'woocommerce' ); ?>:</td> <td class="help"> </td> <td><?php esc_html_e( 'Your theme has a woocommerce.php file, you will not be able to override the woocommerce/archive-product.php custom template since woocommerce.php has priority over archive-product.php. This is intended to prevent display issues.', 'woocommerce' ); ?></td> </tr> <?php endif ?> <?php if ( ! empty( $theme['overrides'] ) ) : ?> <tr> <td data-export-label="Overrides"><?php esc_html_e( 'Overrides', 'woocommerce' ); ?></td> <td class="help"> </td> <td> <?php $total_overrides = count( $theme['overrides'] ); for ( $i = 0; $i < $total_overrides; $i++ ) { $override = $theme['overrides'][ $i ]; if ( $override['core_version'] && ( empty( $override['version'] ) || version_compare( $override['version'], $override['core_version'], '<' ) ) ) { $current_version = $override['version'] ? $override['version'] : '-'; printf( /* Translators: %1$s: Template name, %2$s: Template version, %3$s: Core version. */ esc_html__( '%1$s version %2$s is out of date. The core version is %3$s', 'woocommerce' ), '<code>' . esc_html( $override['file'] ) . '</code>', '<strong style="color:red">' . esc_html( $current_version ) . '</strong>', esc_html( $override['core_version'] ) ); } else { echo esc_html( $override['file'] ); } if ( ( count( $theme['overrides'] ) - 1 ) !== $i ) { echo ', '; } echo '<br />'; } ?> </td> </tr> <?php else : ?> <tr> <td data-export-label="Overrides"><?php esc_html_e( 'Overrides', 'woocommerce' ); ?>:</td> <td class="help"> </td> <td>–</td> </tr> <?php endif; ?> <?php if ( true === $theme['has_outdated_templates'] ) : ?> <tr> <td data-export-label="Outdated Templates"><?php esc_html_e( 'Outdated templates', 'woocommerce' ); ?>:</td> <td class="help"> </td> <td> <mark class="error"> <span class="dashicons dashicons-warning"></span> </mark> <a href="https://docs.woocommerce.com/document/fix-outdated-templates-woocommerce/" target="_blank"> <?php esc_html_e( 'Learn how to update', 'woocommerce' ); ?> </a> </td> </tr> <?php endif; ?> </tbody> </table> <?php do_action( 'woocommerce_system_status_report' ); ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="Status report information"><h2><?php esc_html_e( 'Status report information', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows information about this status report.', 'woocommerce' ) ); ?></h2></th> </tr> </thead> <tbody> <tr> <td data-export-label="Generated at"><?php esc_html_e( 'Generated at', 'woocommerce' ); ?>:</td> <td class="help"> </td> <td><?php echo esc_html( current_time( 'Y-m-d H:i:s P' ) ); ?></td> </tr> </tbody> </table> includes/admin/views/html-bulk-edit-product.php 0000644 00000026274 15132754524 0015633 0 ustar 00 <?php /** * Admin View: Bulk Edit Products */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } ?> <fieldset class="inline-edit-col-right"> <div id="woocommerce-fields-bulk" class="inline-edit-col"> <h4><?php _e( 'Product data', 'woocommerce' ); ?></h4> <?php do_action( 'woocommerce_product_bulk_edit_start' ); ?> <div class="inline-edit-group"> <label class="alignleft"> <span class="title"><?php _e( 'Price', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="change_regular_price change_to" name="change_regular_price"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), '1' => __( 'Change to:', 'woocommerce' ), '2' => __( 'Increase existing price by (fixed amount or %):', 'woocommerce' ), '3' => __( 'Decrease existing price by (fixed amount or %):', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="change-input"> <input type="text" name="_regular_price" class="text regular_price" placeholder="<?php printf( esc_attr__( 'Enter price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); ?>" value="" /> </label> </div> <div class="inline-edit-group"> <label class="alignleft"> <span class="title"><?php _e( 'Sale', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="change_sale_price change_to" name="change_sale_price"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), '1' => __( 'Change to:', 'woocommerce' ), '2' => __( 'Increase existing sale price by (fixed amount or %):', 'woocommerce' ), '3' => __( 'Decrease existing sale price by (fixed amount or %):', 'woocommerce' ), '4' => __( 'Set to regular price decreased by (fixed amount or %):', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="change-input"> <input type="text" name="_sale_price" class="text sale_price" placeholder="<?php printf( esc_attr__( 'Enter sale price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); ?>" value="" /> </label> </div> <?php if ( wc_tax_enabled() ) : ?> <label> <span class="title"><?php _e( 'Tax status', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="tax_status" name="_tax_status"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'taxable' => __( 'Taxable', 'woocommerce' ), 'shipping' => __( 'Shipping only', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label> <span class="title"><?php _e( 'Tax class', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="tax_class" name="_tax_class"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'standard' => __( 'Standard', 'woocommerce' ), ); $tax_classes = WC_Tax::get_tax_classes(); if ( ! empty( $tax_classes ) ) { foreach ( $tax_classes as $class ) { $options[ sanitize_title( $class ) ] = esc_html( $class ); } } foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <?php endif; ?> <?php if ( wc_product_weight_enabled() ) : ?> <div class="inline-edit-group"> <label class="alignleft"> <span class="title"><?php _e( 'Weight', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="change_weight change_to" name="change_weight"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), '1' => __( 'Change to:', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="change-input"> <input type="text" name="_weight" class="text weight" placeholder="<?php printf( esc_attr__( '%1$s (%2$s)', 'woocommerce' ), wc_format_localized_decimal( 0 ), get_option( 'woocommerce_weight_unit' ) ); ?>" value=""> </label> </div> <?php endif; ?> <?php if ( wc_product_dimensions_enabled() ) : ?> <div class="inline-edit-group dimensions"> <label class="alignleft"> <span class="title"><?php _e( 'L/W/H', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="change_dimensions change_to" name="change_dimensions"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), '1' => __( 'Change to:', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="change-input"> <input type="text" name="_length" class="text length" placeholder="<?php printf( esc_attr__( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value=""> <input type="text" name="_width" class="text width" placeholder="<?php printf( esc_attr__( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value=""> <input type="text" name="_height" class="text height" placeholder="<?php printf( esc_attr__( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value=""> </label> </div> <?php endif; ?> <label> <span class="title"><?php _e( 'Shipping class', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="shipping_class" name="_shipping_class"> <option value=""><?php _e( '— No change —', 'woocommerce' ); ?></option> <option value="_no_shipping_class"><?php _e( 'No shipping class', 'woocommerce' ); ?></option> <?php foreach ( $shipping_class as $key => $value ) { echo '<option value="' . esc_attr( $value->slug ) . '">' . esc_html( $value->name ) . '</option>'; } ?> </select> </span> </label> <label> <span class="title"><?php _e( 'Visibility', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="visibility" name="_visibility"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'visible' => __( 'Catalog & search', 'woocommerce' ), 'catalog' => __( 'Catalog', 'woocommerce' ), 'search' => __( 'Search', 'woocommerce' ), 'hidden' => __( 'Hidden', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label> <span class="title"><?php _e( 'Featured', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="featured" name="_featured"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'yes' => __( 'Yes', 'woocommerce' ), 'no' => __( 'No', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label> <span class="title"><?php _e( 'In stock?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="stock_status" name="_stock_status"> <?php echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>'; foreach ( wc_get_product_stock_status_options() as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <?php if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) : ?> <label> <span class="title"><?php _e( 'Manage stock?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="manage_stock" name="_manage_stock"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'yes' => __( 'Yes', 'woocommerce' ), 'no' => __( 'No', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <div class="inline-edit-group"> <label class="alignleft stock_qty_field"> <span class="title"><?php _e( 'Stock qty', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="change_stock change_to" name="change_stock"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), '1' => __( 'Change to:', 'woocommerce' ), '2' => __( 'Increase existing stock by:', 'woocommerce' ), '3' => __( 'Decrease existing stock by:', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="change-input"> <input type="text" name="_stock" class="text stock" placeholder="<?php esc_attr_e( 'Stock qty', 'woocommerce' ); ?>" step="any" value=""> </label> </div> <label> <span class="title"><?php _e( 'Backorders?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="backorders" name="_backorders"> <?php echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>'; foreach ( wc_get_product_backorder_options() as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <?php endif; ?> <label> <span class="title"><?php esc_html_e( 'Sold individually?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="sold_individually" name="_sold_individually"> <?php $options = array( '' => __( '— No change —', 'woocommerce' ), 'yes' => __( 'Yes', 'woocommerce' ), 'no' => __( 'No', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <?php do_action( 'woocommerce_product_bulk_edit_end' ); ?> <input type="hidden" name="woocommerce_bulk_edit" value="1" /> <input type="hidden" name="woocommerce_quick_edit_nonce" value="<?php echo wp_create_nonce( 'woocommerce_quick_edit_nonce' ); ?>" /> </div> </fieldset> includes/admin/views/html-admin-page-status-logs-db.php 0000644 00000001456 15132754524 0017140 0 ustar 00 <?php /** * Admin View: Page - Status Database Logs * * @package WooCommerce\Admin\Logs */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <form method="post" id="mainform" action=""> <?php $log_table_list->search_box( __( 'Search logs', 'woocommerce' ), 'log' ); ?> <?php $log_table_list->display(); ?> <input type="hidden" name="page" value="wc-status" /> <input type="hidden" name="tab" value="logs" /> <?php submit_button( __( 'Flush all logs', 'woocommerce' ), 'delete', 'flush-logs' ); ?> <?php wp_nonce_field( 'woocommerce-status-logs' ); ?> </form> <?php wc_enqueue_js( "jQuery( '#flush-logs' ).on( 'click', function() { if ( window.confirm('" . esc_js( __( 'Are you sure you want to clear all logs from the database?', 'woocommerce' ) ) . "') ) { return true; } return false; });" ); includes/admin/views/html-notice-redirect-only-download.php 0000644 00000002315 15132754524 0020127 0 ustar 00 <?php /** * Admin View: Notice - Redirect only download method is selected. * * @package WooCommerce\Admin\Notices */ defined( 'ABSPATH' ) || exit; ?> <div class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'redirect_download_method' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <?php echo wp_kses_post( sprintf( /* translators: %s: Link to settings page. */ __( 'Your store is configured to serve digital products using "Redirect only" method. This method is deprecated, <a href="%s">please switch to a different method instead.</a><br><em>If you use a remote server for downloadable files (such as Google Drive, Dropbox, Amazon S3), you may optionally wish to "allow using redirects as a last resort". Enabling that and/or selecting any of the other options will make this notice go away.</em>', 'woocommerce' ), add_query_arg( array( 'page' => 'wc-settings', 'tab' => 'products', 'section' => 'downloadable', ), admin_url( 'admin.php' ) ) ) ); ?> </p> </div> includes/admin/views/html-notice-legacy-shipping.php 0000644 00000003232 15132754524 0016624 0 ustar 00 <?php /** * Admin View: Notice - Legacy Shipping. * * @package WooCommerce\Admin\Notices */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'legacy_shipping' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"> <?php esc_html_e( 'Dismiss', 'woocommerce' ); ?> </a> <p class="main"> <strong><?php esc_html_e( 'New:', 'woocommerce' ); ?> <?php esc_html_e( 'Shipping zones', 'woocommerce' ); ?></strong> – <?php esc_html_e( 'a group of regions that can be assigned different shipping methods and rates.', 'woocommerce' ); ?> </p> <p> <?php esc_html_e( 'Legacy shipping methods (flat rate, international flat rate, local pickup and delivery, and free shipping) are deprecated but will continue to work as normal for now. <b><em>They will be removed in future versions of WooCommerce</em></b>. We recommend disabling these and setting up new rates within shipping zones as soon as possible.', 'woocommerce' ); ?> </p> <p class="submit"> <?php if ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) : ?> <a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>"> <?php esc_html_e( 'Setup shipping zones', 'woocommerce' ); ?> </a> <?php endif; ?> <a class="button-secondary" href="https://docs.woocommerce.com/document/setting-up-shipping-zones/"> <?php esc_html_e( 'Learn more about shipping zones', 'woocommerce' ); ?> </a> </p> </div> includes/admin/views/html-notice-template-check.php 0000644 00000003253 15132754524 0016432 0 ustar 00 <?php /** * Admin View: Notice - Template Check * * @package WooCommerce\Views */ if ( ! defined( 'ABSPATH' ) ) { exit; } $theme = wp_get_theme(); ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'template_files' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <?php /* translators: %s: theme name */ ?> <?php printf( __( '<strong>Your theme (%s) contains outdated copies of some WooCommerce template files.</strong> These files may need updating to ensure they are compatible with the current version of WooCommerce. Suggestions to fix this:', 'woocommerce' ), esc_html( $theme['Name'] ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> <ol> <li><?php esc_html_e( 'Update your theme to the latest version. If no update is available contact your theme author asking about compatibility with the current WooCommerce version.', 'woocommerce' ); ?></li> <li><?php esc_html_e( 'If you copied over a template file to change something, then you will need to copy the new version of the template and apply your changes again.', 'woocommerce' ); ?></li> </ol> </p> <p class="submit"> <a class="button-primary" href="https://docs.woocommerce.com/document/template-structure/" target="_blank"><?php esc_html_e( 'Learn more about templates', 'woocommerce' ); ?></a> <a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-status' ) ); ?>" target="_blank"><?php esc_html_e( 'View affected templates', 'woocommerce' ); ?></a> </p> </div> includes/admin/views/html-notice-update.php 0000644 00000002560 15132754524 0015026 0 ustar 00 <?php /** * Admin View: Notice - Update * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } $update_url = wp_nonce_url( add_query_arg( 'do_update_woocommerce', 'true', admin_url( 'admin.php?page=wc-settings' ) ), 'wc_db_update', 'wc_db_update_nonce' ); ?> <div id="message" class="updated woocommerce-message wc-connect"> <p> <strong><?php esc_html_e( 'WooCommerce database update required', 'woocommerce' ); ?></strong> </p> <p> <?php esc_html_e( 'WooCommerce has been updated! To keep things running smoothly, we have to update your database to the newest version.', 'woocommerce' ); /* translators: 1: Link to docs 2: Close link. */ printf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' ); ?> </p> <p class="submit"> <a href="<?php echo esc_url( $update_url ); ?>" class="wc-update-now button-primary"> <?php esc_html_e( 'Update WooCommerce Database', 'woocommerce' ); ?> </a> <a href="https://docs.woocommerce.com/document/how-to-update-woocommerce/" class="button-secondary"> <?php esc_html_e( 'Learn more about updates', 'woocommerce' ); ?> </a> </p> </div> includes/admin/views/html-notice-custom.php 0000644 00000000704 15132754524 0015054 0 ustar 00 <?php /** * Admin View: Custom Notices */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', $notice ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Dismiss', 'woocommerce' ); ?></a> <?php echo wp_kses_post( wpautop( $notice_html ) ); ?> </div> includes/admin/views/html-quick-edit-product.php 0000644 00000017154 15132754524 0016007 0 ustar 00 <?php /** * Admin View: Quick Edit Product * * @package WooCommerce\Admin\Notices */ defined( 'ABSPATH' ) || exit; ?> <fieldset class="inline-edit-col-left"> <div id="woocommerce-fields" class="inline-edit-col"> <h4><?php esc_html_e( 'Product data', 'woocommerce' ); ?></h4> <?php do_action( 'woocommerce_product_quick_edit_start' ); ?> <?php if ( wc_product_sku_enabled() ) : ?> <label> <span class="title"><?php esc_html_e( 'SKU', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="text" name="_sku" class="text sku" value=""> </span> </label> <br class="clear" /> <?php endif; ?> <div class="price_fields"> <label> <span class="title"><?php esc_html_e( 'Price', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="text" name="_regular_price" class="text wc_input_price regular_price" placeholder="<?php esc_attr_e( 'Regular price', 'woocommerce' ); ?>" value=""> </span> </label> <br class="clear" /> <label> <span class="title"><?php esc_html_e( 'Sale', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="text" name="_sale_price" class="text wc_input_price sale_price" placeholder="<?php esc_attr_e( 'Sale price', 'woocommerce' ); ?>" value=""> </span> </label> <br class="clear" /> </div> <?php if ( wc_tax_enabled() ) : ?> <label class="alignleft"> <span class="title"><?php esc_html_e( 'Tax status', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="tax_status" name="_tax_status"> <?php $options = array( 'taxable' => __( 'Taxable', 'woocommerce' ), 'shipping' => __( 'Shipping only', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <br class="clear" /> <label class="alignleft"> <span class="title"><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="tax_class" name="_tax_class"> <?php $options = array( '' => __( 'Standard', 'woocommerce' ), ); $tax_classes = WC_Tax::get_tax_classes(); if ( ! empty( $tax_classes ) ) { foreach ( $tax_classes as $class ) { $options[ sanitize_title( $class ) ] = esc_html( $class ); } } foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <br class="clear" /> <?php endif; ?> <?php if ( wc_product_weight_enabled() || wc_product_dimensions_enabled() ) : ?> <div class="dimension_fields"> <?php if ( wc_product_weight_enabled() ) : ?> <label> <span class="title"><?php esc_html_e( 'Weight', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="text" name="_weight" class="text weight" placeholder="<?php echo esc_attr( wc_format_localized_decimal( 0 ) ); ?>" value=""> </span> </label> <br class="clear" /> <?php endif; ?> <?php if ( wc_product_dimensions_enabled() ) : ?> <div class="inline-edit-group dimensions"> <div> <span class="title"><?php esc_html_e( 'L/W/H', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="text" name="_length" class="text wc_input_decimal length" placeholder="<?php esc_attr_e( 'Length', 'woocommerce' ); ?>" value=""> <input type="text" name="_width" class="text wc_input_decimal width" placeholder="<?php esc_attr_e( 'Width', 'woocommerce' ); ?>" value=""> <input type="text" name="_height" class="text wc_input_decimal height" placeholder="<?php esc_attr_e( 'Height', 'woocommerce' ); ?>" value=""> </span> </div> </div> <?php endif; ?> </div> <?php endif; ?> <div class="inline-edit-group"> <span class="title"><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="shipping_class" name="_shipping_class"> <option value="_no_shipping_class"><?php esc_html_e( 'No shipping class', 'woocommerce' ); ?></option> <?php foreach ( $shipping_class as $key => $value ) { echo '<option value="' . esc_attr( $value->slug ) . '">' . esc_html( $value->name ) . '</option>'; } ?> </select> </span> </div> <div class="inline-edit-group"> <label class="alignleft"> <span class="title"><?php esc_html_e( 'Visibility', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="visibility" name="_visibility"> <?php $options = apply_filters( 'woocommerce_product_visibility_options', array( 'visible' => __( 'Catalog & search', 'woocommerce' ), 'catalog' => __( 'Catalog', 'woocommerce' ), 'search' => __( 'Search', 'woocommerce' ), 'hidden' => __( 'Hidden', 'woocommerce' ), ) ); foreach ( $options as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <label class="alignleft featured"> <input type="checkbox" name="_featured" value="1"> <span class="checkbox-title"><?php esc_html_e( 'Featured', 'woocommerce' ); ?></span> </label> </div> <?php if ( get_option( 'woocommerce_manage_stock' ) === 'yes' ) : ?> <div class="inline-edit-group manage_stock_field"> <label class="manage_stock"> <input type="checkbox" name="_manage_stock" value="1"> <span class="checkbox-title"><?php esc_html_e( 'Manage stock?', 'woocommerce' ); ?></span> </label> </div> <?php endif; ?> <label class="stock_status_field"> <span class="title"><?php esc_html_e( 'In stock?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="stock_status" name="_stock_status"> <?php echo '<option value="" id="stock_status_no_change">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>'; foreach ( wc_get_product_stock_status_options() as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> <div class="wc-quick-edit-warning" style="display:none"> <?php echo esc_html__( 'This will change the stock status of all variations.', 'woocommerce' ); ?></p> </div> </span> </label> <div class="stock_fields"> <?php if ( get_option( 'woocommerce_manage_stock' ) === 'yes' ) : ?> <label class="stock_qty_field"> <span class="title"><?php esc_html_e( 'Stock qty', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <input type="number" name="_stock" class="text stock" step="any" value=""> </span> </label> <?php endif; ?> </div> <label class="alignleft backorder_field"> <span class="title"><?php esc_html_e( 'Backorders?', 'woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="backorders" name="_backorders"> <?php foreach ( wc_get_product_backorder_options() as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>'; } ?> </select> </span> </label> <?php do_action( 'woocommerce_product_quick_edit_end' ); ?> <input type="hidden" name="woocommerce_quick_edit" value="1" /> <input type="hidden" name="woocommerce_quick_edit_nonce" value="<?php echo esc_attr( wp_create_nonce( 'woocommerce_quick_edit_nonce' ) ); ?>" /> </div> </fieldset> includes/admin/views/html-notice-install.php 0000644 00000001334 15132754524 0015210 0 ustar 00 <?php /** * Admin View: Notice - Install * * @deprecated 4.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="message" class="updated woocommerce-message wc-connect"> <p><?php _e( '<strong>Welcome to WooCommerce</strong> – You‘re almost ready to start selling :)', 'woocommerce' ); ?></p> <p class="submit"><a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-setup' ) ); ?>" class="button-primary"><?php _e( 'Run the Setup Wizard', 'woocommerce' ); ?></a> <a class="button-secondary skip" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'install' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Skip setup', 'woocommerce' ); ?></a></p> </div> includes/admin/views/html-notice-uploads-directory-is-unprotected.php 0000644 00000002034 15132754524 0022154 0 ustar 00 <?php /** * Admin View: Notice - Uploads directory is unprotected. * * @package WooCommerce\Admin\Notices * @since 4.2.0 */ defined( 'ABSPATH' ) || exit; $uploads = wp_get_upload_dir(); ?> <div id="message" class="error woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'uploads_directory_is_public' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <?php echo wp_kses_post( sprintf( /* translators: 1: uploads directory URL 2: documentation URL */ __( 'Your store\'s uploads directory is <a href="%1$s">browsable via the web</a>. We strongly recommend <a href="%2$s">configuring your web server to prevent directory indexing</a>.', 'woocommerce' ), esc_url( $uploads['baseurl'] . '/woocommerce_uploads' ), 'https://docs.woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory' ) ); ?> </p> </div> includes/admin/views/html-email-template-preview.php 0000644 00000002034 15132754524 0016640 0 ustar 00 <?php /** * Admin View: Email Template Preview */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } ?> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquet diam a facilisis eleifend. Cras ac justo felis. Mauris faucibus, orci eu blandit fermentum, lorem nibh sollicitudin mi, sit amet interdum metus urna ut lacus.</p> <p><a class="link" href="#">Sed sit amet sapien odio</a></p> <p>Phasellus quis varius augue. Fusce eu euismod leo, a accumsan tellus. Quisque vitae dolor eu justo cursus egestas. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sit amet sapien odio. Sed pellentesque arcu mi, quis malesuada lectus lacinia et. Cras a tempor leo.</p> <h2>Lorem ipsum dolor</h2> <p>Fusce eu euismod leo, a accumsan tellus. Quisque vitae dolor eu justo cursus egestas. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sit amet sapien odio. Sed pellentesque arcu mi, quis malesuada lectus lacinia et. Cras a tempor leo.</p> includes/admin/views/html-notice-wp-php-minimum-requirements.php 0000644 00000001652 15132754524 0021152 0 ustar 00 <?php /** * Admin View: Notice - PHP & WP minimum requirements. * * @package WooCommerce\Admin\Notices */ defined( 'ABSPATH' ) || exit; ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', WC_PHP_MIN_REQUIREMENTS_NOTICE ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <?php echo wp_kses_post( sprintf( $msg . '<p><a href="%s" class="button button-primary">' . __( 'Learn how to upgrade', 'woocommerce' ) . '</a></p>', add_query_arg( array( 'utm_source' => 'wpphpupdatebanner', 'utm_medium' => 'product', 'utm_campaign' => 'woocommerceplugin', 'utm_content' => 'docs', ), 'https://docs.woocommerce.com/document/update-php-wordpress/' ) ) ); ?> </p> </div> includes/admin/views/html-admin-page-reports.php 0000644 00000004622 15132754524 0015764 0 ustar 00 <?php /** * Admin View: Page - Reports */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wrap woocommerce"> <?php if ( WC()->is_wc_admin_active() ) { ?> <div id="message" class="error inline" style="margin-top:30px"> <p> <strong> <?php /* translators: 1: Link URL */ echo wp_kses_post( sprintf( __( 'With the release of WooCommerce 4.0, these reports are being replaced. There is a new and better Analytics section available for users running WordPress 5.3+. Head on over to the <a href="%1$s">WooCommerce Analytics</a> or learn more about the new experience in the <a href="https://docs.woocommerce.com/document/woocommerce-analytics/" target="_blank">WooCommerce Analytics documentation</a>.', 'woocommerce' ), esc_url( wc_admin_url( '&path=/analytics/overview' ) ) ) ); ?> </strong> </p> </div> <?php } ?> <nav class="nav-tab-wrapper woo-nav-tab-wrapper"> <?php foreach ( $reports as $key => $report_group ) { echo '<a href="' . admin_url( 'admin.php?page=wc-reports&tab=' . urlencode( $key ) ) . '" class="nav-tab '; if ( $current_tab == $key ) { echo 'nav-tab-active'; } echo '">' . esc_html( $report_group['title'] ) . '</a>'; } do_action( 'wc_reports_tabs' ); ?> </nav> <?php if ( count( $reports[ $current_tab ]['reports'] ) > 1 ) { ?> <ul class="subsubsub"> <li> <?php $links = array(); foreach ( $reports[ $current_tab ]['reports'] as $key => $report ) { $link = '<a href="admin.php?page=wc-reports&tab=' . urlencode( $current_tab ) . '&report=' . urlencode( $key ) . '" class="'; if ( $key == $current_report ) { $link .= 'current'; } $link .= '">' . $report['title'] . '</a>'; $links[] = $link; } echo implode( ' | </li><li>', $links ); ?> </li> </ul> <br class="clear" /> <?php } if ( isset( $reports[ $current_tab ]['reports'][ $current_report ] ) ) { $report = $reports[ $current_tab ]['reports'][ $current_report ]; if ( ! isset( $report['hide_title'] ) || true != $report['hide_title'] ) { echo '<h1>' . esc_html( $report['title'] ) . '</h1>'; } else { echo '<h1 class="screen-reader-text">' . esc_html( $report['title'] ) . '</h1>'; } if ( $report['description'] ) { echo '<p>' . $report['description'] . '</p>'; } if ( $report['callback'] && ( is_callable( $report['callback'] ) ) ) { call_user_func( $report['callback'], $current_report ); } } ?> </div> includes/admin/views/html-notice-maxmind-license-key.php 0000644 00000002232 15132754524 0017403 0 ustar 00 <?php /** * Admin View: Notice - Missing MaxMind license key * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; ?> <div id="message" class="updated woocommerce-message"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'maxmind_license_key' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p> <strong><?php esc_html_e( 'Geolocation has not been configured.', 'woocommerce' ); ?></strong> </p> <p> <?php echo wp_kses_post( sprintf( /* translators: %1%s: integration page %2$s: general settings page */ __( 'You must enter a valid license key on the <a href="%1$s">MaxMind integration settings page</a> in order to use the geolocation service. If you do not need geolocation for shipping or taxes, you should change the default customer location on the <a href="%2$s">general settings page</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=integration§ion=maxmind_geolocation' ), admin_url( 'admin.php?page=wc-settings&tab=general' ) ) ); ?> </p> </div> includes/admin/views/html-admin-settings.php 0000644 00000003651 15132754524 0015215 0 ustar 00 <?php /** * Admin View: Settings * * @package WooCommerce */ if ( ! defined( 'ABSPATH' ) ) { exit; } $tab_exists = isset( $tabs[ $current_tab ] ) || has_action( 'woocommerce_sections_' . $current_tab ) || has_action( 'woocommerce_settings_' . $current_tab ) || has_action( 'woocommerce_settings_tabs_' . $current_tab ); $current_tab_label = isset( $tabs[ $current_tab ] ) ? $tabs[ $current_tab ] : ''; if ( ! $tab_exists ) { wp_safe_redirect( admin_url( 'admin.php?page=wc-settings' ) ); exit; } ?> <div class="wrap woocommerce"> <?php do_action( 'woocommerce_before_settings_' . $current_tab ); ?> <form method="<?php echo esc_attr( apply_filters( 'woocommerce_settings_form_method_tab_' . $current_tab, 'post' ) ); ?>" id="mainform" action="" enctype="multipart/form-data"> <nav class="nav-tab-wrapper woo-nav-tab-wrapper"> <?php foreach ( $tabs as $slug => $label ) { echo '<a href="' . esc_html( admin_url( 'admin.php?page=wc-settings&tab=' . esc_attr( $slug ) ) ) . '" class="nav-tab ' . ( $current_tab === $slug ? 'nav-tab-active' : '' ) . '">' . esc_html( $label ) . '</a>'; } do_action( 'woocommerce_settings_tabs' ); ?> </nav> <h1 class="screen-reader-text"><?php echo esc_html( $current_tab_label ); ?></h1> <?php do_action( 'woocommerce_sections_' . $current_tab ); self::show_messages(); do_action( 'woocommerce_settings_' . $current_tab ); do_action( 'woocommerce_settings_tabs_' . $current_tab ); // @deprecated 3.4.0 hook. ?> <p class="submit"> <?php if ( empty( $GLOBALS['hide_save_button'] ) ) : ?> <button name="save" class="button-primary woocommerce-save-button" type="submit" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button> <?php endif; ?> <?php wp_nonce_field( 'woocommerce-settings' ); ?> </p> </form> <?php do_action( 'woocommerce_after_settings_' . $current_tab ); ?> </div> includes/admin/views/html-admin-page-status.php 0000644 00000002377 15132754524 0015616 0 ustar 00 <?php /** * Admin View: Page - Status */ if ( ! defined( 'ABSPATH' ) ) { exit; } $current_tab = ! empty( $_REQUEST['tab'] ) ? sanitize_title( $_REQUEST['tab'] ) : 'status'; $tabs = array( 'status' => __( 'System status', 'woocommerce' ), 'tools' => __( 'Tools', 'woocommerce' ), 'logs' => __( 'Logs', 'woocommerce' ), ); $tabs = apply_filters( 'woocommerce_admin_status_tabs', $tabs ); ?> <div class="wrap woocommerce"> <nav class="nav-tab-wrapper woo-nav-tab-wrapper"> <?php foreach ( $tabs as $name => $label ) { echo '<a href="' . admin_url( 'admin.php?page=wc-status&tab=' . $name ) . '" class="nav-tab '; if ( $current_tab == $name ) { echo 'nav-tab-active'; } echo '">' . $label . '</a>'; } ?> </nav> <h1 class="screen-reader-text"><?php echo esc_html( $tabs[ $current_tab ] ); ?></h1> <?php switch ( $current_tab ) { case 'tools': WC_Admin_Status::status_tools(); break; case 'logs': WC_Admin_Status::status_logs(); break; default: if ( array_key_exists( $current_tab, $tabs ) && has_action( 'woocommerce_admin_status_content_' . $current_tab ) ) { do_action( 'woocommerce_admin_status_content_' . $current_tab ); } else { WC_Admin_Status::status_report(); } break; } ?> </div> includes/admin/views/html-report-by-date.php 0000644 00000010143 15132754524 0015117 0 ustar 00 <?php /** * Admin View: Report by Date (with date filters) * * @package WooCommerce\Admin\Reporting */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } ?> <div id="poststuff" class="woocommerce-reports-wide"> <div class="postbox"> <?php if ( 'custom' === $current_range && isset( $_GET['start_date'], $_GET['end_date'] ) ) : ?> <h3 class="screen-reader-text"> <?php /* translators: 1: start date 2: end date */ printf( esc_html__( 'From %1$s to %2$s', 'woocommerce' ), esc_html( wc_clean( wp_unslash( $_GET['start_date'] ) ) ), esc_html( wc_clean( wp_unslash( $_GET['end_date'] ) ) ) ); ?> </h3> <?php else : ?> <h3 class="screen-reader-text"><?php echo esc_html( $ranges[ $current_range ] ); ?></h3> <?php endif; ?> <div class="stats_range"> <?php $this->get_export_button(); ?> <ul> <?php foreach ( $ranges as $range => $name ) { echo '<li class="' . ( $current_range == $range ? 'active' : '' ) . '"><a href="' . esc_url( remove_query_arg( array( 'start_date', 'end_date' ), add_query_arg( 'range', $range ) ) ) . '">' . esc_html( $name ) . '</a></li>'; } ?> <li class="custom <?php echo ( 'custom' === $current_range ) ? 'active' : ''; ?>"> <?php esc_html_e( 'Custom:', 'woocommerce' ); ?> <form method="GET"> <div> <?php // Maintain query string. foreach ( $_GET as $key => $value ) { if ( is_array( $value ) ) { foreach ( $value as $v ) { echo '<input type="hidden" name="' . esc_attr( sanitize_text_field( $key ) ) . '[]" value="' . esc_attr( sanitize_text_field( $v ) ) . '" />'; } } else { echo '<input type="hidden" name="' . esc_attr( sanitize_text_field( $key ) ) . '" value="' . esc_attr( sanitize_text_field( $value ) ) . '" />'; } } ?> <input type="hidden" name="range" value="custom" /> <input type="text" size="11" placeholder="yyyy-mm-dd" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" name="start_date" class="range_datepicker from" autocomplete="off" /><?php //@codingStandardsIgnoreLine ?> <span>–</span> <input type="text" size="11" placeholder="yyyy-mm-dd" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" name="end_date" class="range_datepicker to" autocomplete="off" /><?php //@codingStandardsIgnoreLine ?> <button type="submit" class="button" value="<?php esc_attr_e( 'Go', 'woocommerce' ); ?>"><?php esc_html_e( 'Go', 'woocommerce' ); ?></button> <?php wp_nonce_field( 'custom_range', 'wc_reports_nonce', false ); ?> </div> </form> </li> </ul> </div> <?php if ( empty( $hide_sidebar ) ) : ?> <div class="inside chart-with-sidebar"> <div class="chart-sidebar"> <?php if ( $legends = $this->get_chart_legend() ) : ?> <ul class="chart-legend"> <?php foreach ( $legends as $legend ) : ?> <?php // @codingStandardsIgnoreStart ?> <li style="border-color: <?php echo $legend['color']; ?>" <?php if ( isset( $legend['highlight_series'] ) ) echo 'class="highlight_series ' . ( isset( $legend['placeholder'] ) ? 'tips' : '' ) . '" data-series="' . esc_attr( $legend['highlight_series'] ) . '"'; ?> data-tip="<?php echo isset( $legend['placeholder'] ) ? $legend['placeholder'] : ''; ?>"> <?php echo $legend['title']; ?> </li> <?php // @codingStandardsIgnoreEnd ?> <?php endforeach; ?> </ul> <?php endif; ?> <ul class="chart-widgets"> <?php foreach ( $this->get_chart_widgets() as $widget ) : ?> <li class="chart-widget"> <?php if ( $widget['title'] ) : ?> <h4><?php echo esc_html( $widget['title'] ); ?></h4> <?php endif; ?> <?php call_user_func( $widget['callback'] ); ?> </li> <?php endforeach; ?> </ul> </div> <div class="main"> <?php $this->get_main_chart(); ?> </div> </div> <?php else : ?> <div class="inside"> <?php $this->get_main_chart(); ?> </div> <?php endif; ?> </div> </div> includes/admin/views/html-admin-page-status-tools.php 0000644 00000004102 15132754524 0016740 0 ustar 00 <?php /** * Admin View: Page - Status Tools * * @package WooCommerce */ use Automattic\WooCommerce\Utilities\ArrayUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } foreach ( $tools as $action_name => $tool ) { ?> <form id="<?php echo esc_attr( 'form_' . $action_name ); ?>" method="GET" action="<?php echo esc_attr( esc_url( admin_url( 'admin.php?foo=bar' ) ) ); ?>"> <?php wp_nonce_field( 'debug_action', '_wpnonce', false ); ?> <input type="hidden" name="page" value="wc-status"/> <input type="hidden" name="tab" value="tools"/> <input type="hidden" name="action" value="<?php echo esc_attr( $action_name ); ?>"/> </form> <?php } ?> <table class="wc_status_table wc_status_table--tools widefat" cellspacing="0"> <tbody class="tools"> <?php foreach ( $tools as $action_name => $tool ) : ?> <tr class="<?php echo sanitize_html_class( $action_name ); ?>"> <th> <strong class="name"><?php echo esc_html( $tool['name'] ); ?></strong> <p class="description"> <?php echo wp_kses_post( $tool['desc'] ); if ( ! is_null( ArrayUtil::get_value_or_default( $tool, 'selector' ) ) ) { $selector = $tool['selector']; if ( isset( $selector['description'] ) ) { echo '</p><p class="description">'; echo wp_kses_post( $selector['description'] ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo " <select style='width: 300px;' form='form_$action_name' id='selector_$action_name' data-allow_clear='true' class='${selector['class']}' name='${selector['name']}' data-placeholder='${selector['placeholder']}' data-action='${selector['search_action']}'></select>"; } ?> </p> </th> <td class="run-tool"> <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <input <?php echo ArrayUtil::is_truthy( $tool, 'disabled' ) ? 'disabled' : ''; ?> type="submit" form="<?php echo 'form_' . $action_name; ?>" class="button button-large" value="<?php echo esc_attr( $tool['button'] ); ?>" /> </td> </tr> <?php endforeach; ?> </tbody> </table> includes/admin/views/html-notice-updated.php 0000644 00000001224 15132754524 0015166 0 ustar 00 <?php /** * Admin View: Notice - Updated. * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="message" class="updated woocommerce-message wc-connect woocommerce-message--success"> <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'update', remove_query_arg( 'do_update_woocommerce' ) ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a> <p><?php esc_html_e( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ); ?></p> </div> includes/admin/views/html-notice-regenerating-lookup-table.php 0000644 00000002256 15132754524 0020614 0 ustar 00 <?php /** * Admin View: Notice - Regenerating product lookup table. * * @package WooCommerce\Admin */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; $pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=wc_update_product_lookup_tables&status=pending' ); $cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' ); $cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' ); ?> <div id="message" class="updated woocommerce-message"> <p> <strong><?php esc_html_e( 'WooCommerce is updating product data in the background', 'woocommerce' ); ?></strong><br> <?php esc_html_e( 'Product display, sorting, and reports may not be accurate until this finishes. It will take a few minutes and this notice will disappear when complete.', 'woocommerce' ); if ( $cron_disabled ) { echo '<br>' . esc_html__( 'Note: WP CRON has been disabled on your install which may prevent this update from completing.', 'woocommerce' ); } ?> <a href="<?php echo esc_url( $pending_actions_url ); ?>"><?php echo esc_html( $cron_cta ); ?></a> </p> </div> includes/admin/views/html-admin-dashboard-setup.php 0000644 00000002463 15132754524 0016442 0 ustar 00 <?php /** * Admin View: Dashboard - Finish Setup * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="dashboard-widget-finish-setup"> <span class='progress-wrapper'> <svg class="circle-progress" width="17" height="17" version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle r="6.5" cx="10" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="0"></circle> <circle class="bar" r="6.5" cx="190" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="<?php echo esc_attr( $circle_dashoffset ); ?>" transform='rotate(-90 100 100)'></circle> </svg> <span><?php echo esc_html_e( 'Step', 'woocommerce' ); ?> <?php echo esc_html( $completed_tasks_count ); ?> <?php echo esc_html_e( 'of', 'woocommerce' ); ?> <?php echo esc_html( $tasks_count ); ?></span> </span> <div class="description"> <div> <?php echo esc_html_e( 'You\'re almost there! Once you complete store setup you can start receiving orders.', 'woocommerce' ); ?> <div><a href='<?php echo esc_attr( $button_link ); ?>' class='button button-primary'><?php echo esc_html_e( 'Start selling', 'woocommerce' ); ?></a></div> </div> <img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/dashboard-widget-setup.png" /> </div> <div class="clear"></div> </div> includes/admin/class-wc-admin-addons.php 0000644 00000120605 15132754524 0014237 0 ustar 00 <?php /** * Addons Page * * @package WooCommerce\Admin * @version 2.5.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Admin_Addons Class. */ class WC_Admin_Addons { /** * Get featured for the addons screen * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @return array of objects */ public static function get_featured() { $featured = get_transient( 'wc_addons_featured' ); if ( false === $featured ) { $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['access_token'] ) ) { $headers['Authorization'] = 'Bearer ' . $auth['access_token']; } $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/featured', array( 'headers' => $headers, 'user-agent' => 'WooCommerce Addons Page', ) ); if ( ! is_wp_error( $raw_featured ) ) { $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); if ( $featured ) { set_transient( 'wc_addons_featured', $featured, DAY_IN_SECONDS ); } } } if ( is_object( $featured ) ) { self::output_featured_sections( $featured->sections ); return $featured; } } /** * Render featured products and banners using WCCOM's the Featured 2.0 Endpoint * * @return void */ public static function render_featured() { $featured = get_transient( 'wc_addons_featured_2' ); if ( false === $featured ) { $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['access_token'] ) ) { $headers['Authorization'] = 'Bearer ' . $auth['access_token']; } $parameter_string = ''; $country = WC()->countries->get_base_country(); if ( ! empty( $country ) ) { $parameter_string = '?' . http_build_query( array( 'country' => $country ) ); } // Important: WCCOM Extensions API v2.0 is used. $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, array( 'headers' => $headers, 'user-agent' => 'WooCommerce Addons Page', ) ); if ( ! is_wp_error( $raw_featured ) ) { $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); if ( $featured ) { set_transient( 'wc_addons_featured_2', $featured, DAY_IN_SECONDS ); } } } if ( ! empty( $featured ) ) { self::output_featured( $featured ); } } /** * Build url parameter string * * @param string $category Addon (sub) category. * @param string $term Search terms. * @param string $country Store country. * * @return string url parameter string */ public static function build_parameter_string( $category, $term, $country ) { $parameters = array( 'category' => $category, 'term' => $term, 'country' => $country, ); return '?' . http_build_query( $parameters ); } /** * Call API to get extensions * * @param string $category Addon (sub) category. * @param string $term Search terms. * @param string $country Store country. * * @return object of extensions and promotions. */ public static function get_extension_data( $category, $term, $country ) { $parameters = self::build_parameter_string( $category, $term, $country ); $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['access_token'] ) ) { $headers['Authorization'] = 'Bearer ' . $auth['access_token']; } $raw_extensions = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters, array( 'headers' => $headers ) ); if ( ! is_wp_error( $raw_extensions ) ) { $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); } return $addons; } /** * Get sections for the addons screen * * @return array of objects */ public static function get_sections() { $addon_sections = get_transient( 'wc_addons_sections' ); if ( false === ( $addon_sections ) ) { $raw_sections = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' ); if ( ! is_wp_error( $raw_sections ) ) { $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); if ( $addon_sections ) { set_transient( 'wc_addons_sections', $addon_sections, WEEK_IN_SECONDS ); } } } return apply_filters( 'woocommerce_addons_sections', $addon_sections ); } /** * Get section for the addons screen. * * @param string $section_id Required section ID. * * @return object|bool */ public static function get_section( $section_id ) { $sections = self::get_sections(); if ( isset( $sections[ $section_id ] ) ) { return $sections[ $section_id ]; } return false; } /** * Get section content for the addons screen. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param string $section_id Required section ID. * * @return array */ public static function get_section_data( $section_id ) { $section = self::get_section( $section_id ); $section_data = ''; if ( ! empty( $section->endpoint ) ) { $section_data = get_transient( 'wc_addons_section_' . $section_id ); if ( false === $section_data ) { $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) ); if ( ! is_wp_error( $raw_section ) ) { $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); if ( ! empty( $section_data->products ) ) { set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS ); } } } } return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id ); } /** * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active). * * @deprecated 5.9.0 No longer used in In-App Marketplace */ public static function output_storefront_button() { $template = get_option( 'template' ); $stylesheet = get_option( 'stylesheet' ); if ( 'storefront' === $template ) { if ( 'storefront' === $stylesheet ) { $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; $text = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' ); $utm_content = 'nostorefrontchildtheme'; } else { $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; $text = __( 'View more Storefront child themes', 'woocommerce' ); $utm_content = 'hasstorefrontchildtheme'; } } else { $url = 'https://woocommerce.com/storefront/'; $text = __( 'Need a theme? Try Storefront', 'woocommerce' ); $utm_content = 'nostorefront'; } $url = add_query_arg( array( 'utm_source' => 'addons', 'utm_medium' => 'product', 'utm_campaign' => 'woocommerceplugin', 'utm_content' => $utm_content, ), $url ); echo '<a href="' . esc_url( $url ) . '" class="add-new-h2">' . esc_html( $text ) . '</a>' . "\n"; } /** * Handles the outputting of a banner block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Banner data. */ public static function output_banner_block( $block ) { ?> <div class="addons-banner-block"> <h1><?php echo esc_html( $block->title ); ?></h1> <p><?php echo esc_html( $block->description ); ?></p> <div class="addons-banner-block-items"> <?php foreach ( $block->items as $item ) : ?> <?php if ( self::show_extension( $item ) ) : ?> <div class="addons-banner-block-item"> <div class="addons-banner-block-item-icon"> <img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" /> </div> <div class="addons-banner-block-item-content"> <h3><?php echo esc_html( $item->title ); ?></h3> <p><?php echo esc_html( $item->description ); ?></p> <?php self::output_button( $item->href, $item->button, 'addons-button-solid', $item->plugin ); ?> </div> </div> <?php endif; ?> <?php endforeach; ?> </div> </div> <?php } /** * Handles the outputting of a column. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Column data. */ public static function output_column( $block ) { if ( isset( $block->container ) && 'column_container_start' === $block->container ) { ?> <div class="addons-column-section"> <?php } if ( 'column_start' === $block->module ) { ?> <div class="addons-column"> <?php } else { ?> </div> <?php } if ( isset( $block->container ) && 'column_container_end' === $block->container ) { ?> </div> <?php } } /** * Handles the outputting of a column block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Column block data. */ public static function output_column_block( $block ) { ?> <div class="addons-column-block"> <h1><?php echo esc_html( $block->title ); ?></h1> <p><?php echo esc_html( $block->description ); ?></p> <?php foreach ( $block->items as $item ) : ?> <?php if ( self::show_extension( $item ) ) : ?> <div class="addons-column-block-item"> <div class="addons-column-block-item-icon"> <img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" /> </div> <div class="addons-column-block-item-content"> <h2><?php echo esc_html( $item->title ); ?></h2> <?php self::output_button( $item->href, $item->button, 'addons-button-solid', $item->plugin ); ?> <p><?php echo esc_html( $item->description ); ?></p> </div> </div> <?php endif; ?> <?php endforeach; ?> </div> <?php } /** * Handles the outputting of a small light block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Block data. */ public static function output_small_light_block( $block ) { ?> <div class="addons-small-light-block"> <img class="addons-img" src="<?php echo esc_url( $block->image ); ?>" /> <div class="addons-small-light-block-content"> <h1><?php echo esc_html( $block->title ); ?></h1> <p><?php echo esc_html( $block->description ); ?></p> <div class="addons-small-light-block-buttons"> <?php foreach ( $block->buttons as $button ) : ?> <?php self::output_button( $button->href, $button->text, 'addons-button-solid' ); ?> <?php endforeach; ?> </div> </div> </div> <?php } /** * Handles the outputting of a small dark block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Block data. */ public static function output_small_dark_block( $block ) { ?> <div class="addons-small-dark-block"> <h1><?php echo esc_html( $block->title ); ?></h1> <p><?php echo esc_html( $block->description ); ?></p> <div class="addons-small-dark-items"> <?php foreach ( $block->items as $item ) : ?> <div class="addons-small-dark-item"> <?php if ( ! empty( $item->image ) ) : ?> <div class="addons-small-dark-item-icon"> <img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" /> </div> <?php endif; ?> <?php self::output_button( $item->href, $item->button, 'addons-button-outline-white' ); ?> </div> <?php endforeach; ?> </div> </div> <?php } /** * Handles the outputting of the WooCommerce Services banner block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Block data. */ public static function output_wcs_banner_block( $block = array() ) { $is_active = is_plugin_active( 'woocommerce-services/woocommerce-services.php' ); $location = wc_get_base_location(); if ( ! in_array( $location['country'], array( 'US' ), true ) || $is_active || ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { return; } $button_url = wp_nonce_url( add_query_arg( array( 'install-addon' => 'woocommerce-services', ) ), 'install-addon_woocommerce-services' ); $defaults = array( 'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.jpg', 'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ), 'title' => __( 'Save time and money with WooCommerce Shipping', 'woocommerce' ), 'description' => __( 'Print discounted USPS and DHL labels straight from your WooCommerce dashboard and save on shipping.', 'woocommerce' ), 'button' => __( 'Free - Install now', 'woocommerce' ), 'href' => $button_url, 'logos' => array(), ); switch ( $location['country'] ) { case 'US': $local_defaults = array( 'logos' => array_merge( $defaults['logos'], array( array( 'link' => WC()->plugin_url() . '/assets/images/wcs-usps-logo.png', 'alt' => 'USPS logo', ), array( 'link' => WC()->plugin_url() . '/assets/images/wcs-dhlexpress-logo.png', 'alt' => 'DHL Express logo', ), ) ), ); break; default: $local_defaults = array(); } $block_data = array_merge( $defaults, $local_defaults, $block ); ?> <div class="addons-wcs-banner-block"> <div class="addons-wcs-banner-block-image is-full-image"> <img class="addons-img" src="<?php echo esc_url( $block_data['image'] ); ?>" alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>" /> </div> <div class="addons-wcs-banner-block-content"> <h1><?php echo esc_html( $block_data['title'] ); ?></h1> <p><?php echo esc_html( $block_data['description'] ); ?></p> <ul class="wcs-logos-container"> <?php foreach ( $block_data['logos'] as $logo ) : ?> <li> <img alt="<?php echo esc_attr( $logo['alt'] ); ?>" class="wcs-service-logo" src="<?php echo esc_url( $logo['link'] ); ?>" > </li> <?php endforeach; ?> </ul> <?php self::output_button( $block_data['href'], $block_data['button'], 'addons-button-outline-purple' ); ?> </div> </div> <?php } /** * Handles the outputting of the WooCommerce Pay banner block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Block data. */ public static function output_wcpay_banner_block( $block = array() ) { $is_active = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' ); $location = wc_get_base_location(); if ( ! in_array( $location['country'], array( 'US' ), true ) || $is_active || ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { return; } $button_url = wp_nonce_url( add_query_arg( array( 'install-addon' => 'woocommerce-payments', ) ), 'install-addon_woocommerce-payments' ); $defaults = array( 'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png', 'image_alt' => __( 'WooCommerce Payments', 'woocommerce' ), 'title' => __( 'Payments made simple, with no monthly fees — exclusively for WooCommerce stores.', 'woocommerce' ), 'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ), 'button' => __( 'Free - Install now', 'woocommerce' ), 'href' => $button_url, 'logos' => array(), ); $block_data = array_merge( $defaults, $block ); ?> <div class="addons-wcs-banner-block"> <div class="addons-wcs-banner-block-image"> <img class="addons-img" src="<?php echo esc_url( $block_data['image'] ); ?>" alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>" /> </div> <div class="addons-wcs-banner-block-content"> <h1><?php echo esc_html( $block_data['title'] ); ?></h1> <p><?php echo esc_html( $block_data['description'] ); ?></p> <?php self::output_button( $block_data['href'], $block_data['button'], 'addons-button-outline-purple' ); ?> </div> </div> <?php } /** * Output the HTML for the promotion block. * * @param array $promotion Array of promotion block data. * @return void */ public static function output_search_promotion_block( array $promotion ) { ?> <div class="addons-wcs-banner-block"> <div class="addons-wcs-banner-block-image"> <img class="addons-img" src="<?php echo esc_url( $promotion['image'] ); ?>" alt="<?php echo esc_attr( $promotion['image_alt'] ); ?>" /> </div> <div class="addons-wcs-banner-block-content"> <h1><?php echo esc_html( $promotion['title'] ); ?></h1> <p><?php echo esc_html( $promotion['description'] ); ?></p> <?php if ( ! empty( $promotion['actions'] ) ) { foreach ( $promotion['actions'] as $action ) { self::output_promotion_action( $action ); } } ?> </div> </div> <?php } /** * Handles the output of a full-width block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param array $section Section data. */ public static function output_promotion_block( $section ) { if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { return; } $section_object = (object) $section; if ( ! empty( $section_object->geowhitelist ) ) { $section_object->geowhitelist = explode( ',', $section_object->geowhitelist ); } if ( ! empty( $section_object->geoblacklist ) ) { $section_object->geoblacklist = explode( ',', $section_object->geoblacklist ); } if ( ! self::show_extension( $section_object ) ) { return; } ?> <div class="addons-banner-block addons-promotion-block"> <img class="addons-img" src="<?php echo esc_url( $section['image'] ); ?>" alt="<?php echo esc_attr( $section['image_alt'] ); ?>" /> <div class="addons-promotion-block-content"> <h1 class="addons-promotion-block-title"><?php echo esc_html( $section['title'] ); ?></h1> <div class="addons-promotion-block-description"> <?php echo wp_kses_post( $section['description'] ); ?> </div> <div class="addons-promotion-block-buttons"> <?php if ( $section['button_1'] ) { self::output_button( $section['button_1_href'], $section['button_1'], 'addons-button-expandable addons-button-solid', $section['plugin'] ); } if ( $section['button_2'] ) { self::output_button( $section['button_2_href'], $section['button_2'], 'addons-button-expandable addons-button-outline-purple', $section['plugin'] ); } ?> </div> </div> </div> <?php } /** * Handles the outputting of featured sections * * @param array $sections Section data. */ public static function output_featured_sections( $sections ) { foreach ( $sections as $section ) { switch ( $section->module ) { case 'banner_block': self::output_banner_block( $section ); break; case 'column_start': self::output_column( $section ); break; case 'column_end': self::output_column( $section ); break; case 'column_block': self::output_column_block( $section ); break; case 'small_light_block': self::output_small_light_block( $section ); break; case 'small_dark_block': self::output_small_dark_block( $section ); break; case 'wcs_banner_block': self::output_wcs_banner_block( (array) $section ); break; case 'wcpay_banner_block': self::output_wcpay_banner_block( (array) $section ); break; case 'promotion_block': self::output_promotion_block( (array) $section ); break; } } } /** * Handles the outputting of featured page * * @param array $blocks Featured page's blocks. */ private static function output_featured( $blocks ) { foreach ( $blocks as $block ) { $block_type = $block->type ?? null; switch ( $block_type ) { case 'group': self::output_group( $block ); break; case 'banner': self::output_banner( $block ); break; } } } /** * Render a group block including products * * @param mixed $block Block of the page for rendering. * * @return void */ private static function output_group( $block ) { $capacity = $block->capacity ?? 3; $product_list_classes = 3 === $capacity ? 'three-column' : 'two-column'; $product_list_classes = 'products addons-products-' . $product_list_classes; ?> <section class="addon-product-group"> <h1 class="addon-product-group-title"><?php echo esc_html( $block->title ); ?></h1> <div class="addon-product-group-description-container"> <?php if ( ! empty( $block->description ) ) : ?> <div class="addon-product-group-description"> <?php echo esc_html( $block->description ); ?> </div> <?php endif; ?> <?php if ( null !== $block->url ) : ?> <a class="addon-product-group-see-more" href="<?php echo esc_url( $block->url ); ?>"> <?php esc_html_e( 'See more', 'woocommerce' ); ?> </a> <?php endif; ?> </div> <div class="addon-product-group__items"> <ul class="<?php echo esc_attr( $product_list_classes ); ?>"> <?php $products = array_slice( $block->items, 0, $capacity ); foreach ( $products as $item ) { self::render_product_card( $item ); } ?> </ul> <div> </section> <?php } /** * Render a banner contains a product * * @param mixed $block Block of the page for rendering. * * @return void */ private static function output_banner( $block ) { if ( empty( $block->buttons ) ) { // Render a product-like banner. ?> <ul class="products"> <?php self::render_product_card( $block, $block->type ); ?> </ul> <?php } else { // Render a banner with buttons. ?> <ul class="products"> <li class="product addons-buttons-banner"> <div class="addons-buttons-banner-image" style="background-image:url(<?php echo esc_url( $block->image ); ?>)" title="<?php echo esc_attr( $block->image_alt ); ?>"></div> <div class="product-details addons-buttons-banner-details-container"> <div class="addons-buttons-banner-details"> <h2><?php echo esc_html( $block->title ); ?></h2> <p><?php echo wp_kses( $block->description, array() ); ?></p> </div> <div class="addons-buttons-banner-button-container"> <?php foreach ( $block->buttons as $button ) { $button_classes = array( 'button', 'addons-buttons-banner-button' ); $type = $button->type ?? null; if ( 'primary' === $type ) { $button_classes[] = 'addons-buttons-banner-button-primary'; } ?> <a class="<?php echo esc_attr( implode( ' ', $button_classes ) ); ?>" href="<?php echo esc_url( $button->href ); ?>"> <?php echo esc_html( $button->title ); ?> </a> <?php } ?> </div> </div> </li> </ul> <?php } } /** * Returns in-app-purchase URL params. */ public static function get_in_app_purchase_url_params() { // Get url (from path onward) for the current page, // so WCCOM "back" link returns user to where they were. $back_admin_path = add_query_arg( array() ); return array( 'wccom-site' => site_url(), 'wccom-back' => rawurlencode( $back_admin_path ), 'wccom-woo-version' => Constants::get_constant( 'WC_VERSION' ), 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), ); } /** * Add in-app-purchase URL params to link. * * Adds various url parameters to a url to support a streamlined * flow for obtaining and setting up WooCommerce extensons. * * @param string $url Destination URL. */ public static function add_in_app_purchase_url_params( $url ) { return add_query_arg( self::get_in_app_purchase_url_params(), $url ); } /** * Outputs a button. * * @param string $url Destination URL. * @param string $text Button label text. * @param string $style Button style class. * @param string $plugin The plugin the button is promoting. */ public static function output_button( $url, $text, $style, $plugin = '' ) { $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; $url = self::add_in_app_purchase_url_params( $url ); ?> <a class="addons-button <?php echo esc_attr( $style ); ?>" href="<?php echo esc_url( $url ); ?>"> <?php echo esc_html( $text ); ?> </a> <?php } /** * Output HTML for a promotion action. * * @param array $action Array of action properties. * * @return void */ public static function output_promotion_action( array $action ) { if ( empty( $action ) ) { return; } $style = ( ! empty( $action['primary'] ) && $action['primary'] ) ? 'addons-button-solid' : 'addons-button-outline-purple'; ?> <a class="addons-button <?php echo esc_attr( $style ); ?>" href="<?php echo esc_url( $action['url'] ); ?>"> <?php echo esc_html( $action['label'] ); ?> </a> <?php } /** * Handles output of the addons page in admin. */ public static function output() { $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '_featured'; $search = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { do_action( 'woocommerce_helper_output' ); return; } if ( isset( $_GET['install-addon'] ) ) { switch ( $_GET['install-addon'] ) { case 'woocommerce-services': self::install_woocommerce_services_addon(); break; case 'woocommerce-payments': self::install_woocommerce_payments_addon( $section ); break; default: // Do nothing. break; } } $sections = self::get_sections(); $theme = wp_get_theme(); $current_section = isset( $_GET['section'] ) ? $section : '_featured'; $promotions = array(); $addons = array(); if ( '_featured' !== $current_section ) { $category = $section ? $section : null; $term = $search ? $search : null; $country = WC()->countries->get_base_country(); $extension_data = self::get_extension_data( $category, $term, $country ); $addons = $extension_data->products; $promotions = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array(); } // We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions. if ( ! WC()->is_wc_admin_active() ) { $promotions = array(); } // Check for existence of promotions and evaluate out if we should show them. if ( ! empty( $promotions ) ) { foreach ( $promotions as $promo_id => $promotion ) { $evaluator = new PromotionRuleEngine\RuleEvaluator(); $passed = $evaluator->evaluate( $promotion->rules ); if ( ! $passed ) { unset( $promotions[ $promo_id ] ); } } // Transform promotions to the correct format ready for output. $promotions = self::format_promotions( $promotions ); } /** * Addon page view. * * @uses $addons * @uses $search * @uses $sections * @uses $theme * @uses $current_section */ include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php'; } /** * Install WooCommerce Services from Extensions screens. */ public static function install_woocommerce_services_addon() { check_admin_referer( 'install-addon_woocommerce-services' ); $services_plugin_id = 'woocommerce-services'; $services_plugin = array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'repo-slug' => 'woocommerce-services', ); WC_Install::background_installer( $services_plugin_id, $services_plugin ); wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); exit; } /** * Install WooCommerce Payments from the Extensions screens. * * @param string $section Optional. Extenstions tab. * * @return void */ public static function install_woocommerce_payments_addon( $section = '_featured' ) { check_admin_referer( 'install-addon_woocommerce-payments' ); $wcpay_plugin_id = 'woocommerce-payments'; $wcpay_plugin = array( 'name' => __( 'WooCommerce Payments', 'woocommerce' ), 'repo-slug' => 'woocommerce-payments', ); WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin ); do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section ); wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); exit; } /** * We're displaying page=wc-addons and page=wc-addons§ion=helper as two separate pages. * When we're on those pages, add body classes to distinguishe them. * * @param string $admin_body_class Unfiltered body class. * * @return string Body class with added class for Marketplace or My Subscriptions page. */ public static function filter_admin_body_classes( string $admin_body_class = '' ): string { if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { return " $admin_body_class woocommerce-page-wc-subscriptions "; } return " $admin_body_class woocommerce-page-wc-marketplace "; } /** * Take an action object and return the URL based on properties of the action. * * @param object $action Action object. * @return string URL. */ public static function get_action_url( $action ): string { if ( ! isset( $action->url ) ) { return ''; } if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) { return wc_admin_url( $action->url ); } if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) { if ( empty( $action->nonce ) ) { return ''; } return wp_nonce_url( admin_url( $action->url ), $action->nonce ); } return $action->url; } /** * Format the promotion data ready for display, ie fetch locales and actions. * * @param array $promotions Array of promotoin objects. * @return array Array of formatted promotions ready for output. */ public static function format_promotions( array $promotions ): array { $formatted_promotions = array(); foreach ( $promotions as $promotion ) { // Get the matching locale or fall back to en-US. $locale = PromotionRuleEngine\SpecRunner::get_locale( $promotion->locales ); if ( null === $locale ) { continue; } $promotion_actions = array(); if ( ! empty( $promotion->actions ) ) { foreach ( $promotion->actions as $action ) { $action_locale = PromotionRuleEngine\SpecRunner::get_action_locale( $action->locales ); $url = self::get_action_url( $action ); $promotion_actions[] = array( 'name' => $action->name, 'label' => $action_locale->label, 'url' => $url, 'primary' => isset( $action->is_primary ) ? $action->is_primary : false, ); } } $formatted_promotions[] = array( 'title' => $locale->title, 'description' => $locale->description, 'image' => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image, 'image_alt' => $locale->image_alt, 'actions' => $promotion_actions, ); } return $formatted_promotions; } /** * Map data from different endpoints to a universal format * * Search and featured products has a slightly different products' field names. * Mapping converts different data structures into a universal one for further processing. * * @param mixed $data Product Card Data. * * @return object Converted data. */ public static function map_product_card_data( $data ) { $mapped = (object) null; $type = $data->type ?? null; // Icon. $mapped->icon = $data->icon ?? null; if ( null === $mapped->icon && 'banner' === $type ) { // For product-related banners icon is a product's image. $mapped->icon = $data->image ?? null; } // URL. $mapped->url = $data->link ?? null; if ( empty( $mapped->url ) ) { $mapped->url = $data->url ?? null; } // Title. $mapped->title = $data->title ?? null; // Vendor Name. $mapped->vendor_name = $data->vendor_name ?? null; if ( empty( $mapped->vendor_name ) ) { $mapped->vendor_name = $data->vendorName ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } // Vendor URL. $mapped->vendor_url = $data->vendor_url ?? null; if ( empty( $mapped->vendor_url ) ) { $mapped->vendor_url = $data->vendorUrl ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } // Description. $mapped->description = $data->excerpt ?? null; if ( empty( $mapped->description ) ) { $mapped->description = $data->description ?? null; } $has_currency = ! empty( $data->currency ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // Is Free. if ( $has_currency ) { $mapped->is_free = 0 === (int) $data->price; } else { $mapped->is_free = '$0.00' === $data->price; } // Price. if ( $has_currency ) { $mapped->price = wc_price( $data->price, array( 'currency' => $data->currency ) ); } else { $mapped->price = $data->price; } // Rating. $mapped->rating = $data->rating ?? null; if ( null === $mapped->rating ) { $mapped->rating = $data->averageRating ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } // Reviews Count. $mapped->reviews_count = $data->reviews_count ?? null; if ( null === $mapped->reviews_count ) { $mapped->reviews_count = $data->reviewsCount ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } // Featured & Promoted product card. // Label. $mapped->label = $data->label ?? null; // Primary color. $mapped->primary_color = $data->primary_color ?? null; // Text color. $mapped->text_color = $data->text_color ?? null; // Button text. $mapped->button = $data->button ?? null; return $mapped; } /** * Render a product card * * There's difference in data structure (e.g. field names) between endpoints such as search and * featured. Inner mapping helps to use universal field names for further work. * * @param mixed $data Product data. * @param string $block_type Block type that's different from the default product card, e.g. a banner. * * @return void */ public static function render_product_card( $data, $block_type = null ) { $mapped = self::map_product_card_data( $data ); $product_url = self::add_in_app_purchase_url_params( $mapped->url ); $class_names = array( 'product' ); // Specify a class name according to $block_type (if it's specified). if ( null !== $block_type ) { $class_names[] = 'addons-product-' . $block_type; } $product_details_classes = 'product-details'; if ( 'banner' === $block_type ) { $product_details_classes .= ' addon-product-banner-details'; } if ( isset( $mapped->label ) && 'promoted' === $mapped->label ) { $product_details_classes .= ' promoted'; } elseif ( isset( $mapped->label ) && 'featured' === $mapped->label ) { $product_details_classes .= ' featured'; } if ( 'promoted' === $mapped->label && ! empty( $mapped->primary_color ) && ! empty( $mapped->text_color ) && ! empty( $mapped->button ) ) { // Promoted product card. ?> <li class="product"> <div class="<?php echo esc_attr( $product_details_classes ); ?>" style="border-top: 5px solid <?php echo esc_html( $mapped->primary_color ); ?>;"> <span class="label promoted"><?php esc_attr_e( 'Promoted', 'woocommerce' ); ?></span> <a href="<?php echo esc_url( $product_url ); ?>"> <h2><?php echo esc_html( $mapped->title ); ?></h2> </a> <p><?php echo wp_kses_post( $mapped->description ); ?></p> </div> <div class="product-footer-promoted"> <span class="icon"><img src="<?php echo esc_url( $mapped->icon ); ?>" /></span> <a class="addons-button addons-button-promoted" style="background: <?php echo esc_html( $mapped->primary_color ); ?>; color: <?php echo esc_html( $mapped->text_color ); ?>;" href="<?php echo esc_url( $product_url ); ?>"> <?php echo esc_html( $mapped->button ); ?> </a> </div> </li> <?php } else { // Normal or "featured" product card. ?> <li class="<?php echo esc_attr( implode( ' ', $class_names ) ); ?>"> <div class="<?php echo esc_attr( $product_details_classes ); ?>"> <div class="product-text-container"> <?php if ( isset( $mapped->label ) && 'featured' === $mapped->label ) { ?> <span class="label featured"><?php esc_attr_e( 'Featured', 'woocommerce' ); ?></span> <?php } ?> <a href="<?php echo esc_url( $product_url ); ?>"> <h2><?php echo esc_html( $mapped->title ); ?></h2> </a> <?php if ( ! empty( $mapped->vendor_name ) && ! empty( $mapped->vendor_url ) ) : ?> <div class="product-developed-by"> <?php $vendor_url = add_query_arg( array( 'utm_source' => 'extensionsscreen', 'utm_medium' => 'product', 'utm_campaign' => 'wcaddons', 'utm_content' => 'devpartner', ), $mapped->vendor_url ); printf( /* translators: %s vendor link */ esc_html__( 'Developed by %s', 'woocommerce' ), sprintf( '<a class="product-vendor-link" href="%1$s" target="_blank">%2$s</a>', esc_url_raw( $vendor_url ), esc_html( $mapped->vendor_name ) ) ); ?> </div> <?php endif; ?> <p><?php echo wp_kses_post( $mapped->description ); ?></p> </div> <?php if ( ! empty( $mapped->icon ) ) : ?> <span class="product-img-wrap"> <?php /* Show an icon if it exists */ ?> <img src="<?php echo esc_url( $mapped->icon ); ?>" /> </span> <?php endif; ?> </div> <div class="product-footer"> <div class="product-price-and-reviews-container"> <div class="product-price-block"> <?php if ( $mapped->is_free ) : ?> <span class="price"><?php esc_html_e( 'Free', 'woocommerce' ); ?></span> <?php else : ?> <span class="price"> <?php echo wp_kses( $mapped->price, array( 'span' => array( 'class' => array(), ), 'bdi' => array(), ) ); ?> </span> <span class="price-suffix"><?php esc_html_e( 'per year', 'woocommerce' ); ?></span> <?php endif; ?> </div> <?php if ( ! empty( $mapped->reviews_count ) && ! empty( $mapped->rating ) ) : ?> <?php /* Show rating and the number of reviews */ ?> <div class="product-reviews-block"> <?php for ( $index = 1; $index <= 5; ++$index ) : ?> <?php $rating_star_class = 'product-rating-star product-rating-star__' . self::get_star_class( $mapped->rating, $index ); ?> <div class="<?php echo esc_attr( $rating_star_class ); ?>"></div> <?php endfor; ?> <span class="product-reviews-count">(<?php echo (int) $mapped->reviews_count; ?>)</span> </div> <?php endif; ?> </div> <a class="button" href="<?php echo esc_url( $product_url ); ?>"> <?php esc_html_e( 'View details', 'woocommerce' ); ?> </a> </div> </li> <?php } } /** * Determine which class should be used for a rating star: * - golden * - half-filled (50/50 golden and gray) * - gray * * Consider ratings from 3.0 to 4.0 as an example * 3.0 will produce 3 stars * 3.1 to 3.5 will produce 3 stars and a half star * 3.6 to 4.0 will product 4 stars * * @param float $rating Rating of a product. * @param int $index Index of a star in a row. * * @return string CSS class to use. */ public static function get_star_class( $rating, $index ) { if ( $rating >= $index ) { // Rating more that current star to show. return 'fill'; } elseif ( abs( $index - 1 - floor( $rating ) ) < 0.0000001 && 0 < ( $rating - floor( $rating ) ) ) { // For rating more than x.0 and less than x.5 or equal it will show a half star. return 50 >= floor( ( $rating - floor( $rating ) ) * 100 ) ? 'half-fill' : 'fill'; } // Don't show a golden star otherwise. return 'no-fill'; } } includes/admin/class-wc-admin-setup-wizard.php 0000644 00000253332 15132754524 0015431 0 ustar 00 <?php /** * Setup Wizard Class * * Takes new users through some basic steps to setup their store. * * @package WooCommerce\Admin * @version 2.6.0 * @deprecated 4.6.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Admin_Setup_Wizard class. */ class WC_Admin_Setup_Wizard { /** * Current step * * @var string */ private $step = ''; /** * Steps for the setup wizard * * @var array */ private $steps = array(); /** * Actions to be executed after the HTTP response has completed * * @var array */ private $deferred_actions = array(); /** * Tweets user can optionally send after install * * @var array */ private $tweets = array( 'Someone give me woo-t, I just set up a new store with #WordPress and @WooCommerce!', 'Someone give me high five, I just set up a new store with #WordPress and @WooCommerce!', ); /** * The version of WordPress required to run the WooCommerce Admin plugin * * @var string */ private $wc_admin_plugin_minimum_wordpress_version = '5.3'; /** * Hook in tabs. * * @deprecated 4.6.0 */ public function __construct() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Add admin menus/screens. * * @deprecated 4.6.0 */ public function admin_menus() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); add_dashboard_page( '', '', 'manage_options', 'wc-setup', '' ); } /** * The theme "extra" should only be shown if the current user can modify themes * and the store doesn't already have a WooCommerce theme. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_theme() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $support_woocommerce = current_theme_supports( 'woocommerce' ) && ! wc_is_wp_default_theme_active(); return ( current_user_can( 'install_themes' ) && current_user_can( 'switch_themes' ) && ! is_multisite() && ! $support_woocommerce ); } /** * The "automated tax" extra should only be shown if the current user can * install plugins and the store is in a supported country. * * @deprecated 4.6.0 */ protected function should_show_automated_tax() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( ! current_user_can( 'install_plugins' ) ) { return false; } $country_code = WC()->countries->get_base_country(); // https://developers.taxjar.com/api/reference/#countries . $tax_supported_countries = array_merge( array( 'US', 'CA', 'AU' ), WC()->countries->get_european_union_countries() ); return in_array( $country_code, $tax_supported_countries, true ); } /** * Should we show the MailChimp install option? * True only if the user can install plugins. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_mailchimp() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return current_user_can( 'install_plugins' ); } /** * Should we show the Facebook install option? * True only if the user can install plugins, * and up until the end date of the recommendation. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_facebook() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return current_user_can( 'install_plugins' ); } /** * Is the WooCommerce Admin actively included in the WooCommerce core? * Based on presence of a basic WC Admin function. * * @deprecated 4.6.0 * @return boolean */ protected function is_wc_admin_active() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return function_exists( 'wc_admin_url' ); } /** * Should we show the WooCommerce Admin install option? * True only if the user can install plugins, * and is running the correct version of WordPress. * * @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version * * @deprecated 4.6.0 * @return boolean */ protected function should_show_wc_admin() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' ); return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active(); } /** * Should we show the new WooCommerce Admin onboarding experience? * * @deprecated 4.6.0 * @return boolean */ protected function should_show_wc_admin_onboarding() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package. // This filter will allow for forcing the old wizard while we migrate e2e tests. return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false ); } /** * Should we display the 'Recommended' step? * True if at least one of the recommendations will be displayed. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_recommended_step() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return $this->should_show_theme() || $this->should_show_automated_tax() || $this->should_show_mailchimp() || $this->should_show_facebook() || $this->should_show_wc_admin(); } /** * Register/enqueue scripts and styles for the Setup Wizard. * * Hooked onto 'admin_enqueue_scripts'. * * @deprecated 4.6.0 */ public function enqueue_scripts() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Show the setup wizard. * * @deprecated 4.6.0 */ public function setup_wizard() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok. return; } $default_steps = array( 'new_onboarding' => array( 'name' => '', 'view' => array( $this, 'wc_setup_new_onboarding' ), 'handler' => array( $this, 'wc_setup_new_onboarding_save' ), ), 'store_setup' => array( 'name' => __( 'Store setup', 'woocommerce' ), 'view' => array( $this, 'wc_setup_store_setup' ), 'handler' => array( $this, 'wc_setup_store_setup_save' ), ), 'payment' => array( 'name' => __( 'Payment', 'woocommerce' ), 'view' => array( $this, 'wc_setup_payment' ), 'handler' => array( $this, 'wc_setup_payment_save' ), ), 'shipping' => array( 'name' => __( 'Shipping', 'woocommerce' ), 'view' => array( $this, 'wc_setup_shipping' ), 'handler' => array( $this, 'wc_setup_shipping_save' ), ), 'recommended' => array( 'name' => __( 'Recommended', 'woocommerce' ), 'view' => array( $this, 'wc_setup_recommended' ), 'handler' => array( $this, 'wc_setup_recommended_save' ), ), 'activate' => array( 'name' => __( 'Activate', 'woocommerce' ), 'view' => array( $this, 'wc_setup_activate' ), 'handler' => array( $this, 'wc_setup_activate_save' ), ), 'next_steps' => array( 'name' => __( 'Ready!', 'woocommerce' ), 'view' => array( $this, 'wc_setup_ready' ), 'handler' => '', ), ); // Hide the new/improved onboarding experience screen if the user is not part of the a/b test. if ( ! $this->should_show_wc_admin_onboarding() ) { unset( $default_steps['new_onboarding'] ); } // Hide recommended step if nothing is going to be shown there. if ( ! $this->should_show_recommended_step() ) { unset( $default_steps['recommended'] ); } // Hide shipping step if the store is selling digital products only. if ( 'virtual' === get_option( 'woocommerce_product_type' ) ) { unset( $default_steps['shipping'] ); } // Hide activate section when the user does not have capabilities to install plugins, think multiside admins not being a super admin. if ( ! current_user_can( 'install_plugins' ) ) { unset( $default_steps['activate'] ); } $this->steps = apply_filters( 'woocommerce_setup_wizard_steps', $default_steps ); $this->step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // WPCS: CSRF ok, input var ok. // @codingStandardsIgnoreStart if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->step ]['handler'] ) ) { call_user_func( $this->steps[ $this->step ]['handler'], $this ); } // @codingStandardsIgnoreEnd ob_start(); $this->setup_wizard_header(); $this->setup_wizard_steps(); $this->setup_wizard_content(); $this->setup_wizard_footer(); exit; } /** * Get the URL for the next step's screen. * * @param string $step slug (default: current step). * @return string URL for next step if a next step exists. * Admin URL if it's the last step. * Empty string on failure. * * @deprecated 4.6.0 * @since 3.0.0 */ public function get_next_step_link( $step = '' ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( ! $step ) { $step = $this->step; } $keys = array_keys( $this->steps ); if ( end( $keys ) === $step ) { return admin_url(); } $step_index = array_search( $step, $keys, true ); if ( false === $step_index ) { return ''; } return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) ); } /** * Setup Wizard Header. * * @deprecated 4.6.0 */ public function setup_wizard_header() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // same as default WP from wp-admin/admin-header.php. $wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) ); set_current_screen(); ?> <!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta name="viewport" content="width=device-width" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title><?php esc_html_e( 'WooCommerce › Setup Wizard', 'woocommerce' ); ?></title> <?php do_action( 'admin_enqueue_scripts' ); ?> <?php wp_print_scripts( 'wc-setup' ); ?> <?php do_action( 'admin_print_styles' ); ?> <?php do_action( 'admin_head' ); ?> </head> <body class="wc-setup wp-core-ui <?php echo esc_attr( 'wc-setup-step__' . $this->step ); ?> <?php echo esc_attr( $wp_version_class ); ?>"> <h1 class="wc-logo"><a href="https://woocommerce.com/"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/woocommerce_logo.png" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" /></a></h1> <?php } /** * Setup Wizard Footer. * * @deprecated 4.6.0 */ public function setup_wizard_footer() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $current_step = $this->step; ?> <?php if ( 'new_onboarding' === $current_step || 'store-setup' === $current_step ) : ?> <a class="wc-setup-footer-links" href="<?php echo esc_url( admin_url() ); ?>"><?php esc_html_e( 'Not right now', 'woocommerce' ); ?></a> <?php elseif ( 'recommended' === $current_step || 'activate' === $current_step ) : ?> <a class="wc-setup-footer-links" href="<?php echo esc_url( $this->get_next_step_link() ); ?>"><?php esc_html_e( 'Skip this step', 'woocommerce' ); ?></a> <?php endif; ?> <?php do_action( 'woocommerce_setup_footer' ); ?> </body> </html> <?php } /** * Output the steps. * * @deprecated 4.6.0 */ public function setup_wizard_steps() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $output_steps = $this->steps; $selected_features = array_filter( $this->wc_setup_activate_get_feature_list() ); // Hide the activate step if Jetpack is already active, unless WooCommerce Services // features are selected, or unless the Activate step was already taken. if ( class_exists( 'Jetpack' ) && Jetpack::is_active() && empty( $selected_features ) && 'yes' !== get_transient( 'wc_setup_activated' ) ) { unset( $output_steps['activate'] ); } unset( $output_steps['new_onboarding'] ); ?> <ol class="wc-setup-steps"> <?php foreach ( $output_steps as $step_key => $step ) { $is_completed = array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ); if ( $step_key === $this->step ) { ?> <li class="active"><?php echo esc_html( $step['name'] ); ?></li> <?php } elseif ( $is_completed ) { ?> <li class="done"> <a href="<?php echo esc_url( add_query_arg( 'step', $step_key, remove_query_arg( 'activate_error' ) ) ); ?>"><?php echo esc_html( $step['name'] ); ?></a> </li> <?php } else { ?> <li><?php echo esc_html( $step['name'] ); ?></li> <?php } } ?> </ol> <?php } /** * Output the content for the current step. * * @deprecated 4.6.0 */ public function setup_wizard_content() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); echo '<div class="wc-setup-content">'; if ( ! empty( $this->steps[ $this->step ]['view'] ) ) { call_user_func( $this->steps[ $this->step ]['view'], $this ); } echo '</div>'; } /** * Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin. * * @deprecated 4.6.0 */ public function wc_setup_new_onboarding() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?> <div class="wc-setup-step__new_onboarding-wrapper"> <p class="wc-setup-step__new_onboarding-welcome"><?php esc_html_e( 'Welcome to', 'woocommerce' ); ?></p> <h1 class="wc-logo"><a href="https://woocommerce.com/"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/woocommerce_logo.png" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" /></a></h1> <p><?php esc_html_e( 'Get your store up and running more quickly with our new and improved setup experience', 'woocommerce' ); ?></p> <form method="post" class="activate-new-onboarding"> <?php wp_nonce_field( 'wc-setup' ); ?> <input type="hidden" name="save_step" value="new_onboarding" /> <p class="wc-setup-actions step"> <button class="button-primary button button-large" value="<?php esc_attr_e( 'Yes please', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Yes please', 'woocommerce' ); ?></button> </p> </form> <?php if ( ! $this->is_wc_admin_active() ) : ?> <p class="wc-setup-step__new_onboarding-plugin-info"><?php esc_html_e( 'The "WooCommerce Admin" plugin will be installed and activated', 'woocommerce' ); ?></p> <?php endif; ?> </div> <?php } /** * Installs WooCommerce admin and redirects to the new onboarding experience. * * @deprecated 4.6.0 */ public function wc_setup_new_onboarding_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Initial "store setup" step. * Location, product type, page setup, and tracking opt-in. */ public function wc_setup_store_setup() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $address = WC()->countries->get_base_address(); $address_2 = WC()->countries->get_base_address_2(); $city = WC()->countries->get_base_city(); $state = WC()->countries->get_base_state(); $country = WC()->countries->get_base_country(); $postcode = WC()->countries->get_base_postcode(); $currency = get_option( 'woocommerce_currency', 'USD' ); $product_type = get_option( 'woocommerce_product_type', 'both' ); $sell_in_person = get_option( 'woocommerce_sell_in_person', 'none_selected' ); if ( empty( $country ) ) { $user_location = WC_Geolocation::geolocate_ip(); $country = $user_location['country']; $state = $user_location['state']; } $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; $currency_by_country = wp_list_pluck( $locale_info, 'currency_code' ); ?> <form method="post" class="address-step"> <input type="hidden" name="save_step" value="store_setup" /> <?php wp_nonce_field( 'wc-setup' ); ?> <p class="store-setup"><?php esc_html_e( 'The following wizard will help you configure your store and get you started quickly.', 'woocommerce' ); ?></p> <div class="store-address-container"> <label for="store_country" class="location-prompt"><?php esc_html_e( 'Where is your store based?', 'woocommerce' ); ?></label> <select id="store_country" name="store_country" required data-placeholder="<?php esc_attr_e( 'Choose a country / region…', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="location-input wc-enhanced-select dropdown"> <?php foreach ( WC()->countries->get_countries() as $code => $label ) : ?> <option <?php selected( $code, $country ); ?> value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $label ); ?></option> <?php endforeach; ?> </select> <label class="location-prompt" for="store_address"><?php esc_html_e( 'Address', 'woocommerce' ); ?></label> <input type="text" id="store_address" class="location-input" name="store_address" required value="<?php echo esc_attr( $address ); ?>" /> <label class="location-prompt" for="store_address_2"><?php esc_html_e( 'Address line 2', 'woocommerce' ); ?></label> <input type="text" id="store_address_2" class="location-input" name="store_address_2" value="<?php echo esc_attr( $address_2 ); ?>" /> <div class="city-and-postcode"> <div> <label class="location-prompt" for="store_city"><?php esc_html_e( 'City', 'woocommerce' ); ?></label> <input type="text" id="store_city" class="location-input" name="store_city" required value="<?php echo esc_attr( $city ); ?>" /> </div> <div class="store-state-container hidden"> <label for="store_state" class="location-prompt"> <?php esc_html_e( 'State', 'woocommerce' ); ?> </label> <select id="store_state" name="store_state" data-placeholder="<?php esc_attr_e( 'Choose a state…', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'State', 'woocommerce' ); ?>" class="location-input wc-enhanced-select dropdown"></select> </div> <div> <label class="location-prompt" for="store_postcode"><?php esc_html_e( 'Postcode / ZIP', 'woocommerce' ); ?></label> <input type="text" id="store_postcode" class="location-input" name="store_postcode" required value="<?php echo esc_attr( $postcode ); ?>" /> </div> </div> </div> <div class="store-currency-container"> <label class="location-prompt" for="currency_code"> <?php esc_html_e( 'What currency do you accept payments in?', 'woocommerce' ); ?> </label> <select id="currency_code" name="currency_code" required data-placeholder="<?php esc_attr_e( 'Choose a currency…', 'woocommerce' ); ?>" class="location-input wc-enhanced-select dropdown" > <option value=""><?php esc_html_e( 'Choose a currency…', 'woocommerce' ); ?></option> <?php foreach ( get_woocommerce_currencies() as $code => $name ) : ?> <option value="<?php echo esc_attr( $code ); ?>" <?php selected( $currency, $code ); ?>> <?php $symbol = get_woocommerce_currency_symbol( $code ); if ( $symbol === $code ) { /* translators: 1: currency name 2: currency code */ echo esc_html( sprintf( __( '%1$s (%2$s)', 'woocommerce' ), $name, $code ) ); } else { /* translators: 1: currency name 2: currency symbol, 3: currency code */ echo esc_html( sprintf( __( '%1$s (%2$s %3$s)', 'woocommerce' ), $name, get_woocommerce_currency_symbol( $code ), $code ) ); } ?> </option> <?php endforeach; ?> </select> <script type="text/javascript"> var wc_setup_currencies = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $currency_by_country ) ); ?>' ) ); var wc_base_state = "<?php echo esc_js( $state ); ?>"; </script> </div> <div class="product-type-container"> <label class="location-prompt" for="product_type"> <?php esc_html_e( 'What type of products do you plan to sell?', 'woocommerce' ); ?> </label> <select id="product_type" name="product_type" required class="location-input wc-enhanced-select dropdown"> <option value="both" <?php selected( $product_type, 'both' ); ?>><?php esc_html_e( 'I plan to sell both physical and digital products', 'woocommerce' ); ?></option> <option value="physical" <?php selected( $product_type, 'physical' ); ?>><?php esc_html_e( 'I plan to sell physical products', 'woocommerce' ); ?></option> <option value="virtual" <?php selected( $product_type, 'virtual' ); ?>><?php esc_html_e( 'I plan to sell digital products', 'woocommerce' ); ?></option> </select> </div> <div class="sell-in-person-container"> <input type="checkbox" id="woocommerce_sell_in_person" name="sell_in_person" value="yes" <?php checked( $sell_in_person, true ); ?> /> <label class="location-prompt" for="woocommerce_sell_in_person"> <?php esc_html_e( 'I will also be selling products or services in person.', 'woocommerce' ); ?> </label> </div> <input type="checkbox" id="wc_tracker_checkbox" name="wc_tracker_checkbox" value="yes" <?php checked( 'yes', get_option( 'woocommerce_allow_tracking', 'no' ) ); ?> /> <?php $this->tracking_modal(); ?> <p class="wc-setup-actions step"> <button class="button-primary button button-large" value="<?php esc_attr_e( "Let's go!", 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'woocommerce' ); ?></button> </p> </form> <?php } /** * Template for the usage tracking modal. * * @deprecated 4.6.0 */ public function tracking_modal() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?> <script type="text/template" id="tmpl-wc-modal-tracking-setup"> <div class="wc-backbone-modal woocommerce-tracker"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Help improve WooCommerce with usage tracking', 'woocommerce' ); ?></h1> </header> <article> <p> <?php printf( wp_kses( /* translators: %1$s: usage tracking help link */ __( 'Learn more about how usage tracking works, and how you\'ll be helping in our <a href="%1$s" target="_blank">usage tracking documentation</a>.', 'woocommerce' ), array( 'a' => array( 'href' => array(), 'target' => array(), ), ) ), 'https://woocommerce.com/usage-tracking/' ); ?> </p> <p class="woocommerce-tracker-checkbox"> <input type="checkbox" id="wc_tracker_checkbox_dialog" name="wc_tracker_checkbox_dialog" value="yes" <?php checked( 'yes', get_option( 'woocommerce_allow_tracking', 'no' ) ); ?> /> <label for="wc_tracker_checkbox_dialog"><?php esc_html_e( 'Enable usage tracking and help improve WooCommerce', 'woocommerce' ); ?></label> </p> </article> <footer> <div class="inner"> <button class="button button-primary button-large" id="wc_tracker_submit" aria-label="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <?php } /** * Save initial store settings. * * @deprecated 4.6.0 */ public function wc_setup_store_setup_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Finishes replying to the client, but keeps the process running for further (async) code execution. * * @see https://core.trac.wordpress.org/ticket/41358 . */ protected function close_http_connection() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // Only 1 PHP process can access a session object at a time, close this so the next request isn't kept waiting. // @codingStandardsIgnoreStart if ( session_id() ) { session_write_close(); } // @codingStandardsIgnoreEnd wc_set_time_limit( 0 ); // fastcgi_finish_request is the cleanest way to send the response and keep the script running, but not every server has it. if ( is_callable( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } else { // Fallback: send headers and flush buffers. if ( ! headers_sent() ) { header( 'Connection: close' ); } @ob_end_flush(); // @codingStandardsIgnoreLine. flush(); } } /** * Function called after the HTTP request is finished, so it's executed without the client having to wait for it. * * @see WC_Admin_Setup_Wizard::install_plugin * @see WC_Admin_Setup_Wizard::install_theme * * @deprecated 4.6.0 */ public function run_deferred_actions() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->close_http_connection(); foreach ( $this->deferred_actions as $action ) { $action['func']( ...$action['args'] ); // Clear the background installation flag if this is a plugin. if ( isset( $action['func'][1] ) && 'background_installer' === $action['func'][1] && isset( $action['args'][0] ) ) { delete_option( 'woocommerce_setup_background_installing_' . $action['args'][0] ); } } } /** * Helper method to queue the background install of a plugin. * * @param string $plugin_id Plugin id used for background install. * @param array $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php. * * @deprecated 4.6.0 */ protected function install_plugin( $plugin_id, $plugin_info ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // Make sure we don't trigger multiple simultaneous installs. if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) { return; } $plugin_file = isset( $plugin_info['file'] ) ? $plugin_info['file'] : $plugin_info['repo-slug'] . '.php'; if ( is_plugin_active( $plugin_info['repo-slug'] . '/' . $plugin_file ) ) { return; } if ( empty( $this->deferred_actions ) ) { add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); } array_push( $this->deferred_actions, array( 'func' => array( 'WC_Install', 'background_installer' ), 'args' => array( $plugin_id, $plugin_info ), ) ); // Set the background installation flag for this plugin. update_option( 'woocommerce_setup_background_installing_' . $plugin_id, true ); } /** * Helper method to queue the background install of a theme. * * @param string $theme_id Theme id used for background install. * * @deprecated 4.6.0 */ protected function install_theme( $theme_id ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( empty( $this->deferred_actions ) ) { add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); } array_push( $this->deferred_actions, array( 'func' => array( 'WC_Install', 'theme_background_installer' ), 'args' => array( $theme_id ), ) ); } /** * Helper method to install Jetpack. * * @deprecated 4.6.0 */ protected function install_jetpack() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->install_plugin( 'jetpack', array( 'name' => __( 'Jetpack', 'woocommerce' ), 'repo-slug' => 'jetpack', ) ); } /** * Helper method to install WooCommerce Services and its Jetpack dependency. * * @deprecated 4.6.0 */ protected function install_woocommerce_services() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->install_jetpack(); $this->install_plugin( 'woocommerce-services', array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'repo-slug' => 'woocommerce-services', ) ); } /** * Retrieve info for missing WooCommerce Services and/or Jetpack plugin. * * @deprecated 4.6.0 * @return array */ protected function get_wcs_requisite_plugins() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $plugins = array(); if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) { $plugins[] = array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'slug' => 'woocommerce-services', ); } if ( ! is_plugin_active( 'jetpack/jetpack.php' ) && ! get_option( 'woocommerce_setup_background_installing_jetpack' ) ) { $plugins[] = array( 'name' => __( 'Jetpack', 'woocommerce' ), 'slug' => 'jetpack', ); } return $plugins; } /** * Plugin install info message markup with heading. * * @deprecated 4.6.0 */ public function plugin_install_info() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?> <span class="plugin-install-info"> <span class="plugin-install-info-label"><?php esc_html_e( 'The following plugins will be installed and activated for you:', 'woocommerce' ); ?></span> <span class="plugin-install-info-list"></span> </span> <?php } /** * Get shipping methods based on country code. * * @param string $country_code Country code. * @param string $currency_code Currency code. * * @deprecated 4.6.0 * @return array */ protected function get_wizard_shipping_methods( $country_code, $currency_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $shipping_methods = array( 'flat_rate' => array( 'name' => __( 'Flat Rate', 'woocommerce' ), 'description' => __( 'Set a fixed price to cover shipping costs.', 'woocommerce' ), 'settings' => array( 'cost' => array( 'type' => 'text', 'default_value' => __( 'Cost', 'woocommerce' ), 'description' => __( 'What would you like to charge for flat rate shipping?', 'woocommerce' ), 'required' => true, ), ), ), 'free_shipping' => array( 'name' => __( 'Free Shipping', 'woocommerce' ), 'description' => __( "Don't charge for shipping.", 'woocommerce' ), ), ); return $shipping_methods; } /** * Render the available shipping methods for a given country code. * * @param string $country_code Country code. * @param string $currency_code Currency code. * @param string $input_prefix Input prefix. * * @deprecated 4.6.0 */ protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $selected = 'flat_rate'; $shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code ); ?> <div class="wc-wizard-shipping-method-select"> <div class="wc-wizard-shipping-method-dropdown"> <select id="<?php echo esc_attr( "{$input_prefix}[method]" ); ?>" name="<?php echo esc_attr( "{$input_prefix}[method]" ); ?>" class="method wc-enhanced-select" data-plugins="<?php echo wc_esc_json( wp_json_encode( $this->get_wcs_requisite_plugins() ) ); ?>" > <?php foreach ( $shipping_methods as $method_id => $method ) : ?> <option value="<?php echo esc_attr( $method_id ); ?>" <?php selected( $selected, $method_id ); ?>><?php echo esc_html( $method['name'] ); ?></option> <?php endforeach; ?> </select> </div> <div class="shipping-method-descriptions"> <?php foreach ( $shipping_methods as $method_id => $method ) : ?> <p class="shipping-method-description <?php echo esc_attr( $method_id ); ?> <?php echo $method_id !== $selected ? 'hide' : ''; ?>"> <?php echo esc_html( $method['description'] ); ?> </p> <?php endforeach; ?> </div> </div> <div class="shipping-method-settings"> <?php foreach ( $shipping_methods as $method_id => $method ) : ?> <?php if ( empty( $method['settings'] ) ) { continue; } ?> <div class="shipping-method-setting <?php echo esc_attr( $method_id ); ?> <?php echo $method_id !== $selected ? 'hide' : ''; ?>"> <?php foreach ( $method['settings'] as $setting_id => $setting ) : ?> <?php $method_setting_id = "{$input_prefix}[{$method_id}][{$setting_id}]"; ?> <input type="<?php echo esc_attr( $setting['type'] ); ?>" placeholder="<?php echo esc_attr( $setting['default_value'] ); ?>" id="<?php echo esc_attr( $method_setting_id ); ?>" name="<?php echo esc_attr( $method_setting_id ); ?>" class="<?php echo esc_attr( $setting['required'] ? 'shipping-method-required-field' : '' ); ?>" <?php echo ( $method_id === $selected && $setting['required'] ) ? 'required' : ''; ?> /> <p class="description"> <?php echo esc_html( $setting['description'] ); ?> </p> <?php endforeach; ?> </div> <?php endforeach; ?> </div> <?php } /** * Render a product weight unit dropdown. * * @deprecated 4.6.0 * @return string */ protected function get_product_weight_selection() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $weight_unit = get_option( 'woocommerce_weight_unit' ); ob_start(); ?> <span class="wc-setup-shipping-unit"> <select id="weight_unit" name="weight_unit" class="wc-enhanced-select"> <option value="kg" <?php selected( $weight_unit, 'kg' ); ?>><?php esc_html_e( 'Kilograms', 'woocommerce' ); ?></option> <option value="g" <?php selected( $weight_unit, 'g' ); ?>><?php esc_html_e( 'Grams', 'woocommerce' ); ?></option> <option value="lbs" <?php selected( $weight_unit, 'lbs' ); ?>><?php esc_html_e( 'Pounds', 'woocommerce' ); ?></option> <option value="oz" <?php selected( $weight_unit, 'oz' ); ?>><?php esc_html_e( 'Ounces', 'woocommerce' ); ?></option> </select> </span> <?php return ob_get_clean(); } /** * Render a product dimension unit dropdown. * * @deprecated 4.6.0 * @return string */ protected function get_product_dimension_selection() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); ob_start(); ?> <span class="wc-setup-shipping-unit"> <select id="dimension_unit" name="dimension_unit" class="wc-enhanced-select"> <option value="m" <?php selected( $dimension_unit, 'm' ); ?>><?php esc_html_e( 'Meters', 'woocommerce' ); ?></option> <option value="cm" <?php selected( $dimension_unit, 'cm' ); ?>><?php esc_html_e( 'Centimeters', 'woocommerce' ); ?></option> <option value="mm" <?php selected( $dimension_unit, 'mm' ); ?>><?php esc_html_e( 'Millimeters', 'woocommerce' ); ?></option> <option value="in" <?php selected( $dimension_unit, 'in' ); ?>><?php esc_html_e( 'Inches', 'woocommerce' ); ?></option> <option value="yd" <?php selected( $dimension_unit, 'yd' ); ?>><?php esc_html_e( 'Yards', 'woocommerce' ); ?></option> </select> </span> <?php return ob_get_clean(); } /** * Shipping. * * @deprecated 4.6.0 */ public function wc_setup_shipping() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $country_code = WC()->countries->get_base_country(); $country_name = WC()->countries->countries[ $country_code ]; $prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name; $currency_code = get_woocommerce_currency(); $existing_zones = WC_Shipping_Zones::get_zones(); $intro_text = ''; if ( empty( $existing_zones ) ) { $intro_text = sprintf( /* translators: %s: country name including the 'the' prefix if needed */ __( "We've created two Shipping Zones - for %s and for the rest of the world. Below you can set Flat Rate shipping costs for these Zones or offer Free Shipping.", 'woocommerce' ), $prefixed_country_name ); } $is_wcs_labels_supported = $this->is_wcs_shipping_labels_supported_country( $country_code ); $is_shipstation_supported = $this->is_shipstation_supported_country( $country_code ); ?> <h1><?php esc_html_e( 'Shipping', 'woocommerce' ); ?></h1> <?php if ( $intro_text ) : ?> <p><?php echo wp_kses_post( $intro_text ); ?></p> <?php endif; ?> <form method="post"> <?php if ( $is_wcs_labels_supported || $is_shipstation_supported ) : ?> <ul class="wc-setup-shipping-recommended"> <?php if ( $is_wcs_labels_supported ) : $this->display_recommended_item( array( 'type' => 'woocommerce_services', 'title' => __( 'Did you know you can print shipping labels at home?', 'woocommerce' ), 'description' => __( 'Use WooCommerce Shipping (powered by WooCommerce Services & Jetpack) to save time at the post office by printing your shipping labels at home.', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-woocommerce-services-icon.png', 'img_alt' => __( 'WooCommerce Services icon', 'woocommerce' ), 'plugins' => $this->get_wcs_requisite_plugins(), ) ); elseif ( $is_shipstation_supported ) : $this->display_recommended_item( array( 'type' => 'shipstation', 'title' => __( 'Did you know you can print shipping labels at home?', 'woocommerce' ), 'description' => __( 'We recommend using ShipStation to save time at the post office by printing your shipping labels at home. Try ShipStation free for 30 days.', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-shipstation-icon.png', 'img_alt' => __( 'ShipStation icon', 'woocommerce' ), 'plugins' => array( array( 'name' => __( 'ShipStation', 'woocommerce' ), 'slug' => 'woocommerce-shipstation-integration', ), ), ) ); endif; ?> </ul> <?php endif; ?> <?php if ( empty( $existing_zones ) ) : ?> <ul class="wc-wizard-services shipping"> <li class="wc-wizard-service-item"> <div class="wc-wizard-service-name"> <p><?php echo esc_html_e( 'Shipping Zone', 'woocommerce' ); ?></p> </div> <div class="wc-wizard-service-description"> <p><?php echo esc_html_e( 'Shipping Method', 'woocommerce' ); ?></p> </div> </li> <li class="wc-wizard-service-item"> <div class="wc-wizard-service-name"> <p><?php echo esc_html( $country_name ); ?></p> </div> <div class="wc-wizard-service-description"> <?php $this->shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[domestic]' ); ?> </div> <div class="wc-wizard-service-enable"> <span class="wc-wizard-service-toggle"> <input id="shipping_zones[domestic][enabled]" type="checkbox" name="shipping_zones[domestic][enabled]" value="yes" checked="checked" class="wc-wizard-shipping-method-enable" data-plugins="true" /> <label for="shipping_zones[domestic][enabled]"> </span> </div> </li> <li class="wc-wizard-service-item"> <div class="wc-wizard-service-name"> <p><?php echo esc_html_e( 'Locations not covered by your other zones', 'woocommerce' ); ?></p> </div> <div class="wc-wizard-service-description"> <?php $this->shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[intl]' ); ?> </div> <div class="wc-wizard-service-enable"> <span class="wc-wizard-service-toggle"> <input id="shipping_zones[intl][enabled]" type="checkbox" name="shipping_zones[intl][enabled]" value="yes" checked="checked" class="wc-wizard-shipping-method-enable" data-plugins="true" /> <label for="shipping_zones[intl][enabled]"> </span> </div> </li> <li class="wc-wizard-service-info"> <p> <?php printf( wp_kses( /* translators: %1$s: live rates tooltip text, %2$s: shipping extensions URL */ __( 'If you\'d like to offer <span class="help_tip" data-tip="%1$s">live rates</span> from a specific carrier (e.g. UPS) you can find a variety of extensions available for WooCommerce <a href="%2$s" target="_blank">here</a>.', 'woocommerce' ), array( 'span' => array( 'class' => array(), 'data-tip' => array(), ), 'a' => array( 'href' => array(), 'target' => array(), ), ) ), esc_attr__( 'A live rate is the exact cost to ship an order, quoted directly from the shipping carrier.', 'woocommerce' ), 'https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/shipping-carriers/' ); ?> </p> </li> </ul> <?php endif; ?> <div class="wc-setup-shipping-units"> <p> <?php echo wp_kses( sprintf( /* translators: %1$s: weight unit dropdown, %2$s: dimension unit dropdown */ esc_html__( 'We\'ll use %1$s for product weight and %2$s for product dimensions.', 'woocommerce' ), $this->get_product_weight_selection(), $this->get_product_dimension_selection() ), array( 'span' => array( 'class' => array(), ), 'select' => array( 'id' => array(), 'name' => array(), 'class' => array(), ), 'option' => array( 'value' => array(), 'selected' => array(), ), ) ); ?> </p> </div> <p class="wc-setup-actions step"> <?php $this->plugin_install_info(); ?> <button class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button> <?php wp_nonce_field( 'wc-setup' ); ?> </p> </form> <?php } /** * Save shipping options. * * @deprecated 4.6.0 */ public function wc_setup_shipping_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Is Stripe country supported * https://stripe.com/global . * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_stripe_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $stripe_supported_countries = array( 'AU', 'AT', 'BE', 'CA', 'DK', 'FI', 'FR', 'DE', 'HK', 'IE', 'JP', 'LU', 'NL', 'NZ', 'NO', 'SG', 'ES', 'SE', 'CH', 'GB', 'US', ); return in_array( $country_code, $stripe_supported_countries, true ); } /** * Is PayPal currency supported. * * @param string $currency Currency code. * @return boolean * * @deprecated 4.6.0 */ protected function is_paypal_supported_currency( $currency ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_currencies = array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR', ); return in_array( $currency, $supported_currencies, true ); } /** * Is Klarna Checkout country supported. * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_klarna_checkout_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_countries = array( 'SE', // Sweden. 'FI', // Finland. 'NO', // Norway. 'NL', // Netherlands. ); return in_array( $country_code, $supported_countries, true ); } /** * Is Klarna Payments country supported. * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_klarna_payments_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_countries = array( 'DK', // Denmark. 'DE', // Germany. 'AT', // Austria. ); return in_array( $country_code, $supported_countries, true ); } /** * Is Square country supported * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_square_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $square_supported_countries = array( 'US', 'CA', 'JP', 'GB', 'AU', ); return in_array( $country_code, $square_supported_countries, true ); } /** * Is eWAY Payments country supported * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_eway_payments_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_countries = array( 'AU', // Australia. 'NZ', // New Zealand. ); return in_array( $country_code, $supported_countries, true ); } /** * Is ShipStation country supported * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_shipstation_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_countries = array( 'AU', // Australia. 'CA', // Canada. 'GB', // United Kingdom. ); return in_array( $country_code, $supported_countries, true ); } /** * Is WooCommerce Services shipping label country supported * * @param string $country_code Country code. * * @deprecated 4.6.0 */ protected function is_wcs_shipping_labels_supported_country( $country_code ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $supported_countries = array( 'US', // United States. ); return in_array( $country_code, $supported_countries, true ); } /** * Helper method to retrieve the current user's email address. * * @deprecated 4.6.0 * @return string Email address */ protected function get_current_user_email() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $current_user = wp_get_current_user(); $user_email = $current_user->user_email; return $user_email; } /** * Array of all possible "in cart" gateways that can be offered. * * @deprecated 4.6.0 * @return array */ protected function get_wizard_available_in_cart_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $user_email = $this->get_current_user_email(); $stripe_description = '<p>' . sprintf( /* translators: %s: URL */ __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay. <a href="%s" target="_blank">Learn more</a>.', 'woocommerce' ), 'https://woocommerce.com/products/stripe/' ) . '</p>'; $paypal_checkout_description = '<p>' . sprintf( /* translators: %s: URL */ __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. <a href="%s" target="_blank">Learn more</a>.', 'woocommerce' ), 'https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/' ) . '</p>'; $klarna_checkout_description = '<p>' . sprintf( /* translators: %s: URL */ __( 'Full checkout experience with pay now, pay later and slice it. No credit card numbers, no passwords, no worries. <a href="%s" target="_blank">Learn more about Klarna</a>.', 'woocommerce' ), 'https://woocommerce.com/products/klarna-checkout/' ) . '</p>'; $klarna_payments_description = '<p>' . sprintf( /* translators: %s: URL */ __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries. <a href="%s" target="_blank">Learn more about Klarna</a>.', 'woocommerce' ), 'https://woocommerce.com/products/klarna-payments/ ' ) . '</p>'; $square_description = '<p>' . sprintf( /* translators: %s: URL */ __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place. <a href="%s" target="_blank">Learn more about Square</a>.', 'woocommerce' ), 'https://woocommerce.com/products/square/' ) . '</p>'; return array( 'stripe' => array( 'name' => __( 'WooCommerce Stripe Gateway', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/stripe.png', 'description' => $stripe_description, 'class' => 'checked stripe-logo', 'repo-slug' => 'woocommerce-gateway-stripe', 'settings' => array( 'create_account' => array( 'label' => __( 'Set up Stripe for me using this email:', 'woocommerce' ), 'type' => 'checkbox', 'value' => 'yes', 'default' => 'yes', 'placeholder' => '', 'required' => false, 'plugins' => $this->get_wcs_requisite_plugins(), ), 'email' => array( 'label' => __( 'Stripe email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'Stripe email address', 'woocommerce' ), 'required' => true, ), ), ), 'ppec_paypal' => array( 'name' => __( 'WooCommerce PayPal Checkout Gateway', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal.png', 'description' => $paypal_checkout_description, 'enabled' => false, 'class' => 'checked paypal-logo', 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', 'settings' => array( 'reroute_requests' => array( 'label' => __( 'Set up PayPal for me using this email:', 'woocommerce' ), 'type' => 'checkbox', 'value' => 'yes', 'default' => 'yes', 'placeholder' => '', 'required' => false, 'plugins' => $this->get_wcs_requisite_plugins(), ), 'email' => array( 'label' => __( 'Direct payments to email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'Email address to receive payments', 'woocommerce' ), 'required' => true, ), ), ), 'paypal' => array( 'name' => __( 'PayPal Standard', 'woocommerce' ), 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), 'image' => '', 'settings' => array( 'email' => array( 'label' => __( 'PayPal email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'PayPal email address', 'woocommerce' ), 'required' => true, ), ), ), 'klarna_checkout' => array( 'name' => __( 'Klarna Checkout for WooCommerce', 'woocommerce' ), 'description' => $klarna_checkout_description, 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', 'enabled' => true, 'class' => 'klarna-logo', 'repo-slug' => 'klarna-checkout-for-woocommerce', ), 'klarna_payments' => array( 'name' => __( 'Klarna Payments for WooCommerce', 'woocommerce' ), 'description' => $klarna_payments_description, 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', 'enabled' => true, 'class' => 'klarna-logo', 'repo-slug' => 'klarna-payments-for-woocommerce', ), 'square' => array( 'name' => __( 'WooCommerce Square', 'woocommerce' ), 'description' => $square_description, 'image' => WC()->plugin_url() . '/assets/images/square-black.png', 'class' => 'square-logo', 'enabled' => false, 'repo-slug' => 'woocommerce-square', ), 'eway' => array( 'name' => __( 'WooCommerce eWAY Gateway', 'woocommerce' ), 'description' => __( 'The eWAY extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/eway-logo.jpg', 'enabled' => false, 'class' => 'eway-logo', 'repo-slug' => 'woocommerce-gateway-eway', ), 'payfast' => array( 'name' => __( 'WooCommerce PayFast Gateway', 'woocommerce' ), 'description' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs.', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/payfast.png', 'class' => 'payfast-logo', 'enabled' => false, 'repo-slug' => 'woocommerce-payfast-gateway', 'file' => 'gateway-payfast.php', ), ); } /** * Simple array of "in cart" gateways to show in wizard. * * @deprecated 4.6.0 * @return array */ public function get_wizard_in_cart_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $gateways = $this->get_wizard_available_in_cart_payment_gateways(); $country = WC()->countries->get_base_country(); $currency = get_woocommerce_currency(); $can_stripe = $this->is_stripe_supported_country( $country ); $can_eway = $this->is_eway_payments_supported_country( $country ); $can_payfast = ( 'ZA' === $country ); // South Africa. $can_paypal = $this->is_paypal_supported_currency( $currency ); if ( ! current_user_can( 'install_plugins' ) ) { return $can_paypal ? array( 'paypal' => $gateways['paypal'] ) : array(); } $klarna_or_square = false; if ( $this->is_klarna_checkout_supported_country( $country ) ) { $klarna_or_square = 'klarna_checkout'; } elseif ( $this->is_klarna_payments_supported_country( $country ) ) { $klarna_or_square = 'klarna_payments'; } elseif ( $this->is_square_supported_country( $country ) && get_option( 'woocommerce_sell_in_person' ) ) { $klarna_or_square = 'square'; } $offered_gateways = array(); if ( $can_stripe ) { $gateways['stripe']['enabled'] = true; $gateways['stripe']['featured'] = true; $offered_gateways += array( 'stripe' => $gateways['stripe'] ); } elseif ( $can_paypal ) { $gateways['ppec_paypal']['enabled'] = true; } if ( $klarna_or_square ) { if ( in_array( $klarna_or_square, array( 'klarna_checkout', 'klarna_payments' ), true ) ) { $gateways[ $klarna_or_square ]['enabled'] = true; $gateways[ $klarna_or_square ]['featured'] = false; $offered_gateways += array( $klarna_or_square => $gateways[ $klarna_or_square ], ); } else { $offered_gateways += array( $klarna_or_square => $gateways[ $klarna_or_square ], ); } } if ( $can_paypal ) { $offered_gateways += array( 'ppec_paypal' => $gateways['ppec_paypal'] ); } if ( $can_eway ) { $offered_gateways += array( 'eway' => $gateways['eway'] ); } if ( $can_payfast ) { $offered_gateways += array( 'payfast' => $gateways['payfast'] ); } return $offered_gateways; } /** * Simple array of "manual" gateways to show in wizard. * * @deprecated 4.6.0 * @return array */ public function get_wizard_manual_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $gateways = array( 'cheque' => array( 'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ), 'image' => '', 'class' => '', ), 'bacs' => array( 'name' => __( 'Bank transfer (BACS) payments', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), 'image' => '', 'class' => '', ), 'cod' => array( 'name' => __( 'Cash on delivery', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), 'image' => '', 'class' => '', ), ); return $gateways; } /** * Display service item in list. * * @param int $item_id Item ID. * @param array $item_info Item info array. * * @deprecated 4.6.0 */ public function display_service_item( $item_id, $item_info ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $item_class = 'wc-wizard-service-item'; if ( isset( $item_info['class'] ) ) { $item_class .= ' ' . $item_info['class']; } $previously_saved_settings = get_option( 'woocommerce_' . $item_id . '_settings' ); // Show the user-saved state if it was previously saved. // Otherwise, rely on the item info. if ( is_array( $previously_saved_settings ) ) { $should_enable_toggle = ( isset( $previously_saved_settings['enabled'] ) && 'yes' === $previously_saved_settings['enabled'] ) ? true : ( isset( $item_info['enabled'] ) && $item_info['enabled'] ); } else { $should_enable_toggle = isset( $item_info['enabled'] ) && $item_info['enabled']; } $plugins = null; if ( isset( $item_info['repo-slug'] ) ) { $plugin = array( 'slug' => $item_info['repo-slug'], 'name' => $item_info['name'], ); $plugins = array( $plugin ); } ?> <li class="<?php echo esc_attr( $item_class ); ?>"> <div class="wc-wizard-service-name"> <?php if ( ! empty( $item_info['image'] ) ) : ?> <img src="<?php echo esc_attr( $item_info['image'] ); ?>" alt="<?php echo esc_attr( $item_info['name'] ); ?>" /> <?php else : ?> <p><?php echo esc_html( $item_info['name'] ); ?></p> <?php endif; ?> </div> <div class="wc-wizard-service-enable"> <span class="wc-wizard-service-toggle <?php echo esc_attr( $should_enable_toggle ? '' : 'disabled' ); ?>" tabindex="0"> <input id="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>" type="checkbox" name="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>-enabled" value="yes" <?php checked( $should_enable_toggle ); ?> data-plugins="<?php echo wc_esc_json( wp_json_encode( $plugins ) ); ?>" /> <label for="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>"> </span> </div> <div class="wc-wizard-service-description"> <?php echo wp_kses_post( wpautop( $item_info['description'] ) ); ?> <?php if ( ! empty( $item_info['settings'] ) ) : ?> <div class="wc-wizard-service-settings <?php echo $should_enable_toggle ? '' : 'hide'; ?>"> <?php foreach ( $item_info['settings'] as $setting_id => $setting ) : ?> <?php $is_checkbox = 'checkbox' === $setting['type']; if ( $is_checkbox ) { $checked = false; if ( isset( $previously_saved_settings[ $setting_id ] ) ) { $checked = 'yes' === $previously_saved_settings[ $setting_id ]; } elseif ( false === $previously_saved_settings && isset( $setting['default'] ) ) { $checked = 'yes' === $setting['default']; } } if ( 'email' === $setting['type'] ) { $value = empty( $previously_saved_settings[ $setting_id ] ) ? $setting['value'] : $previously_saved_settings[ $setting_id ]; } ?> <?php $input_id = $item_id . '_' . $setting_id; ?> <div class="<?php echo esc_attr( 'wc-wizard-service-setting-' . $input_id ); ?>"> <label for="<?php echo esc_attr( $input_id ); ?>" class="<?php echo esc_attr( $input_id ); ?>" > <?php echo esc_html( $setting['label'] ); ?> </label> <input type="<?php echo esc_attr( $setting['type'] ); ?>" id="<?php echo esc_attr( $input_id ); ?>" class="<?php echo esc_attr( 'payment-' . $setting['type'] . '-input' ); ?>" name="<?php echo esc_attr( $input_id ); ?>" value="<?php echo esc_attr( isset( $value ) ? $value : $setting['value'] ); ?>" placeholder="<?php echo esc_attr( $setting['placeholder'] ); ?>" <?php echo ( $setting['required'] ) ? 'required' : ''; ?> <?php echo $is_checkbox ? checked( isset( $checked ) && $checked, true, false ) : ''; ?> data-plugins="<?php echo wc_esc_json( wp_json_encode( isset( $setting['plugins'] ) ? $setting['plugins'] : null ) ); ?>" /> <?php if ( ! empty( $setting['description'] ) ) : ?> <span class="wc-wizard-service-settings-description"><?php echo esc_html( $setting['description'] ); ?></span> <?php endif; ?> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> </li> <?php } /** * Is it a featured service? * * @param array $service Service info array. * * @deprecated 4.6.0 * @return boolean */ public function is_featured_service( $service ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return ! empty( $service['featured'] ); } /** * Is this a non featured service? * * @param array $service Service info array. * * @deprecated 4.6.0 * @return boolean */ public function is_not_featured_service( $service ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return ! $this->is_featured_service( $service ); } /** * Payment Step. * * @deprecated 4.6.0 */ public function wc_setup_payment() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) ); $in_cart_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) ); $manual_gateways = $this->get_wizard_manual_payment_gateways(); ?> <h1><?php esc_html_e( 'Payment', 'woocommerce' ); ?></h1> <form method="post" class="wc-wizard-payment-gateway-form"> <p> <?php printf( wp_kses( /* translators: %s: Link */ __( 'WooCommerce can accept both online and offline payments. <a href="%s" target="_blank">Additional payment methods</a> can be installed later.', 'woocommerce' ), array( 'a' => array( 'href' => array(), 'target' => array(), ), ) ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=payment-gateways' ) ) ); ?> </p> <?php if ( $featured_gateways ) : ?> <ul class="wc-wizard-services featured"> <?php foreach ( $featured_gateways as $gateway_id => $gateway ) { $this->display_service_item( $gateway_id, $gateway ); } ?> </ul> <?php endif; ?> <?php if ( $in_cart_gateways ) : ?> <ul class="wc-wizard-services in-cart"> <?php foreach ( $in_cart_gateways as $gateway_id => $gateway ) { $this->display_service_item( $gateway_id, $gateway ); } ?> </ul> <?php endif; ?> <ul class="wc-wizard-services manual"> <li class="wc-wizard-services-list-toggle closed"> <div class="wc-wizard-service-name"> <?php esc_html_e( 'Offline Payments', 'woocommerce' ); ?> </div> <div class="wc-wizard-service-description"> <?php esc_html_e( 'Collect payments from customers offline.', 'woocommerce' ); ?> </div> <div class="wc-wizard-service-enable" tabindex="0"> <input class="wc-wizard-service-list-toggle" id="wc-wizard-service-list-toggle" type="checkbox"> <label for="wc-wizard-service-list-toggle"></label> </div> </li> <?php foreach ( $manual_gateways as $gateway_id => $gateway ) { $this->display_service_item( $gateway_id, $gateway ); } ?> </ul> <p class="wc-setup-actions step"> <?php $this->plugin_install_info(); ?> <button type="submit" class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button> <?php wp_nonce_field( 'wc-setup' ); ?> </p> </form> <?php } /** * Payment Step save. * * @deprecated 4.6.0 */ public function wc_setup_payment_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } protected function display_recommended_item( $item_info ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $type = $item_info['type']; $title = $item_info['title']; $description = $item_info['description']; $img_url = $item_info['img_url']; $img_alt = $item_info['img_alt']; ?> <li class="recommended-item checkbox"> <input id="<?php echo esc_attr( 'wc_recommended_' . $type ); ?>" type="checkbox" name="<?php echo esc_attr( 'setup_' . $type ); ?>" value="yes" checked data-plugins="<?php echo wc_esc_json( wp_json_encode( isset( $item_info['plugins'] ) ? $item_info['plugins'] : null ) ); ?>" /> <label for="<?php echo esc_attr( 'wc_recommended_' . $type ); ?>"> <img src="<?php echo esc_url( $img_url ); ?>" class="<?php echo esc_attr( 'recommended-item-icon-' . $type ); ?> recommended-item-icon" alt="<?php echo esc_attr( $img_alt ); ?>" /> <div class="recommended-item-description-container"> <h3><?php echo esc_html( $title ); ?></h3> <p><?php echo wp_kses( $description, array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), 'em' => array(), ) ); ?></p> </div> </label> </li> <?php } /** * Recommended step * * @deprecated 4.6.0 */ public function wc_setup_recommended() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?> <h1><?php esc_html_e( 'Recommended for All WooCommerce Stores', 'woocommerce' ); ?></h1> <p> <?php esc_html_e( 'Enhance your store with these recommended free features.', 'woocommerce' ); ?> </p> <form method="post"> <ul class="recommended-step"> <?php if ( $this->should_show_theme() ) : $theme = wp_get_theme(); $theme_name = $theme['Name']; $this->display_recommended_item( array( 'type' => 'storefront_theme', 'title' => __( 'Storefront Theme', 'woocommerce' ), 'description' => sprintf( __( 'Design your store with deep WooCommerce integration. If toggled on, we’ll install <a href="https://woocommerce.com/storefront/" target="_blank" rel="noopener noreferrer">Storefront</a>, and your current theme <em>%s</em> will be deactivated.', 'woocommerce' ), $theme_name ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-storefront-icon.svg', 'img_alt' => __( 'Storefront icon', 'woocommerce' ), ) ); endif; if ( $this->should_show_automated_tax() ) : $this->display_recommended_item( array( 'type' => 'automated_taxes', 'title' => __( 'Automated Taxes', 'woocommerce' ), 'description' => __( 'Save time and errors with automated tax calculation and collection at checkout. Powered by WooCommerce Services and Jetpack.', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-taxes-icon.svg', 'img_alt' => __( 'automated taxes icon', 'woocommerce' ), 'plugins' => $this->get_wcs_requisite_plugins(), ) ); endif; if ( $this->should_show_wc_admin() ) : $this->display_recommended_item( array( 'type' => 'wc_admin', 'title' => __( 'WooCommerce Admin', 'woocommerce' ), 'description' => __( 'Manage your store\'s reports and monitor key metrics with a new and improved interface and dashboard.', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-woocommerce-admin-icon.svg', 'img_alt' => __( 'WooCommerce Admin icon', 'woocommerce' ), 'plugins' => array( array( 'name' => __( 'WooCommerce Admin', 'woocommerce' ), 'slug' => 'woocommerce-admin' ) ), ) ); endif; if ( $this->should_show_mailchimp() ) : $this->display_recommended_item( array( 'type' => 'mailchimp', 'title' => __( 'Mailchimp', 'woocommerce' ), 'description' => __( 'Join the 16 million customers who use Mailchimp. Sync list and store data to send automated emails, and targeted campaigns.', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-mailchimp-icon.svg', 'img_alt' => __( 'Mailchimp icon', 'woocommerce' ), 'plugins' => array( array( 'name' => __( 'Mailchimp for WooCommerce', 'woocommerce' ), 'slug' => 'mailchimp-for-woocommerce' ) ), ) ); endif; if ( $this->should_show_facebook() ) : $this->display_recommended_item( array( 'type' => 'facebook', 'title' => __( 'Facebook', 'woocommerce' ), 'description' => __( 'Enjoy all Facebook products combined in one extension: pixel tracking, catalog sync, messenger chat, shop functionality and Instagram shopping (coming soon)!', 'woocommerce' ), 'img_url' => WC()->plugin_url() . '/assets/images/obw-facebook-icon.svg', 'img_alt' => __( 'Facebook icon', 'woocommerce' ), 'plugins' => array( array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'slug' => 'facebook-for-woocommerce' ) ), ) ); endif; ?> </ul> <p class="wc-setup-actions step"> <?php $this->plugin_install_info(); ?> <button type="submit" class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button> <?php wp_nonce_field( 'wc-setup' ); ?> </p> </form> <?php } /** * Recommended step save. * * @deprecated 4.6.0 */ public function wc_setup_recommended_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Go to the next step if Jetpack was connected. */ protected function wc_setup_activate_actions() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( isset( $_GET['from'] ) && 'wpcom' === $_GET['from'] && class_exists( 'Jetpack' ) && Jetpack::is_active() ) { wp_redirect( esc_url_raw( remove_query_arg( 'from', $this->get_next_step_link() ) ) ); exit; } } /** * * @deprecated 4.6.0 */ protected function wc_setup_activate_get_feature_list() { $features = array(); $stripe_settings = get_option( 'woocommerce_stripe_settings', false ); $stripe_enabled = is_array( $stripe_settings ) && isset( $stripe_settings['create_account'] ) && 'yes' === $stripe_settings['create_account'] && isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled']; $ppec_settings = get_option( 'woocommerce_ppec_paypal_settings', false ); $ppec_enabled = is_array( $ppec_settings ) && isset( $ppec_settings['reroute_requests'] ) && 'yes' === $ppec_settings['reroute_requests'] && isset( $ppec_settings['enabled'] ) && 'yes' === $ppec_settings['enabled']; $features['payment'] = $stripe_enabled || $ppec_enabled; $features['taxes'] = (bool) get_option( 'woocommerce_setup_automated_taxes', false ); $features['labels'] = (bool) get_option( 'woocommerce_setup_shipping_labels', false ); return $features; } /** * * @deprecated 4.6.0 */ protected function wc_setup_activate_get_feature_list_str() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $features = $this->wc_setup_activate_get_feature_list(); if ( $features['payment'] && $features['taxes'] && $features['labels'] ) { return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' ); } else if ( $features['payment'] && $features['taxes'] ) { return __( 'payment setup and automated taxes', 'woocommerce' ); } else if ( $features['payment'] && $features['labels'] ) { return __( 'payment setup and discounted shipping labels', 'woocommerce' ); } else if ( $features['payment'] ) { return __( 'payment setup', 'woocommerce' ); } else if ( $features['taxes'] && $features['labels'] ) { return __( 'automated taxes and discounted shipping labels', 'woocommerce' ); } else if ( $features['taxes'] ) { return __( 'automated taxes', 'woocommerce' ); } else if ( $features['labels'] ) { return __( 'discounted shipping labels', 'woocommerce' ); } return false; } /** * Activate step. * * @deprecated 4.6.0 */ public function wc_setup_activate() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->wc_setup_activate_actions(); $jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active(); $has_jetpack_error = false; if ( isset( $_GET['activate_error'] ) ) { $has_jetpack_error = true; $title = __( "Sorry, we couldn't connect your store to Jetpack", 'woocommerce' ); $error_message = $this->get_activate_error_message( sanitize_text_field( wp_unslash( $_GET['activate_error'] ) ) ); $description = $error_message; } else { $feature_list = $this->wc_setup_activate_get_feature_list_str(); $description = false; if ( $feature_list ) { if ( ! $jetpack_connected ) { /* translators: %s: list of features, potentially comma separated */ $description_base = __( 'Your store is almost ready! To activate services like %s, just connect with Jetpack.', 'woocommerce' ); } else { $description_base = __( 'Thanks for using Jetpack! Your store is almost ready: to activate services like %s, just connect your store.', 'woocommerce' ); } $description = sprintf( $description_base, $feature_list ); } if ( ! $jetpack_connected ) { $title = $feature_list ? __( 'Connect your store to Jetpack', 'woocommerce' ) : __( 'Connect your store to Jetpack to enable extra features', 'woocommerce' ); $button_text = __( 'Continue with Jetpack', 'woocommerce' ); } elseif ( $feature_list ) { $title = __( 'Connect your store to activate WooCommerce Services', 'woocommerce' ); $button_text = __( 'Continue with WooCommerce Services', 'woocommerce' ); } else { wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); exit; } } ?> <h1><?php echo esc_html( $title ); ?></h1> <p><?php echo esc_html( $description ); ?></p> <?php if ( $jetpack_connected ) : ?> <div class="activate-splash"> <img class="jetpack-logo" src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/jetpack_horizontal_logo.png' ); ?>" alt="<?php esc_attr_e( 'Jetpack logo', 'woocommerce' ); ?>" /> <img class="wcs-notice" src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/wcs-notice.png' ); ?>" /> </div> <?php else : ?> <img class="jetpack-logo" src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/jetpack_vertical_logo.png' ); ?>" alt="<?php esc_attr_e( 'Jetpack logo', 'woocommerce' ); ?>" /> <?php endif; ?> <?php if ( $has_jetpack_error ) : ?> <p class="wc-setup-actions step"> <a href="<?php echo esc_url( $this->get_next_step_link() ); ?>" class="button-primary button button-large" > <?php esc_html_e( 'Finish setting up your store', 'woocommerce' ); ?> </a> </p> <?php else : ?> <p class="jetpack-terms"> <?php printf( wp_kses_post( __( 'By connecting your site you agree to our fascinating <a href="%1$s" target="_blank">Terms of Service</a> and to <a href="%2$s" target="_blank">share details</a> with WordPress.com', 'woocommerce' ) ), 'https://wordpress.com/tos', 'https://jetpack.com/support/what-data-does-jetpack-sync' ); ?> </p> <form method="post" class="activate-jetpack"> <p class="wc-setup-actions step"> <button type="submit" class="button-primary button button-large" value="<?php echo esc_attr( $button_text ); ?>"><?php echo esc_html( $button_text ); ?></button> </p> <input type="hidden" name="save_step" value="activate" /> <?php wp_nonce_field( 'wc-setup' ); ?> </form> <?php if ( ! $jetpack_connected ) : ?> <h3 class="jetpack-reasons"> <?php echo esc_html( $description ? __( "Bonus reasons you'll love Jetpack", 'woocommerce' ) : __( "Reasons you'll love Jetpack", 'woocommerce' ) ); ?> </h3> <ul class="wc-wizard-features"> <li class="wc-wizard-feature-item"> <p class="wc-wizard-feature-name"> <strong><?php esc_html_e( 'Better security', 'woocommerce' ); ?></strong> </p> <p class="wc-wizard-feature-description"> <?php esc_html_e( 'Protect your store from unauthorized access.', 'woocommerce' ); ?> </p> </li> <li class="wc-wizard-feature-item"> <p class="wc-wizard-feature-name"> <strong><?php esc_html_e( 'Store stats', 'woocommerce' ); ?></strong> </p> <p class="wc-wizard-feature-description"> <?php esc_html_e( 'Get insights on how your store is doing, including total sales, top products, and more.', 'woocommerce' ); ?> </p> </li> <li class="wc-wizard-feature-item"> <p class="wc-wizard-feature-name"> <strong><?php esc_html_e( 'Store monitoring', 'woocommerce' ); ?></strong> </p> <p class="wc-wizard-feature-description"> <?php esc_html_e( 'Get an alert if your store is down for even a few minutes.', 'woocommerce' ); ?> </p> </li> <li class="wc-wizard-feature-item"> <p class="wc-wizard-feature-name"> <strong><?php esc_html_e( 'Product promotion', 'woocommerce' ); ?></strong> </p> <p class="wc-wizard-feature-description"> <?php esc_html_e( "Share new items on social media the moment they're live in your store.", 'woocommerce' ); ?> </p> </li> </ul> <?php endif; ?> <?php endif; ?> <?php } /** * * @deprecated 4.6.0 */ protected function get_all_activate_errors() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return array( 'default' => __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ), 'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ), 'register_http_request_failed' => __( "Sorry! We couldn't contact Jetpack just now 😭. Please make sure that your site is visible over the internet, and that it accepts incoming and outgoing requests via curl. You can also try to connect to Jetpack again, and if you run into any more issues, please contact support.", 'woocommerce' ), 'siteurl_private_ip_dev' => __( "Your site might be on a private network. Jetpack can only connect to public sites. Please make sure your site is visible over the internet, and then try connecting again 🙏." , 'woocommerce' ), ); } /** * * @deprecated 4.6.0 */ protected function get_activate_error_message( $code = '' ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $errors = $this->get_all_activate_errors(); return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default']; } /** * Activate step save. * * Install, activate, and launch connection flow for Jetpack. * * @deprecated 4.6.0 */ public function wc_setup_activate_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Final step. * * @deprecated 4.6.0 */ public function wc_setup_ready() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // We've made it! Don't prompt the user to run the wizard again. WC_Admin_Notices::remove_notice( 'install', true ); $user_email = $this->get_current_user_email(); $docs_url = 'https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_source=setupwizard&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin'; $help_text = sprintf( /* translators: %1$s: link to docs */ __( 'Visit WooCommerce.com to learn more about <a href="%1$s" target="_blank">getting started</a>.', 'woocommerce' ), $docs_url ); ?> <h1><?php esc_html_e( "You're ready to start selling!", 'woocommerce' ); ?></h1> <div class="woocommerce-message woocommerce-newsletter"> <p><?php esc_html_e( "We're here for you — get tips, product updates, and inspiration straight to your mailbox.", 'woocommerce' ); ?></p> <form action="//woocommerce.us8.list-manage.com/subscribe/post?u=2c1434dc56f9506bf3c3ecd21&id=13860df971&SIGNUPPAGE=plugin" method="post" target="_blank" novalidate> <div class="newsletter-form-container"> <input class="newsletter-form-email" type="email" value="<?php echo esc_attr( $user_email ); ?>" name="EMAIL" placeholder="<?php esc_attr_e( 'Email address', 'woocommerce' ); ?>" required > <p class="wc-setup-actions step newsletter-form-button-container"> <button type="submit" value="<?php esc_attr_e( 'Yes please!', 'woocommerce' ); ?>" name="subscribe" id="mc-embedded-subscribe" class="button-primary button newsletter-form-button" ><?php esc_html_e( 'Yes please!', 'woocommerce' ); ?></button> </p> </div> </form> </div> <ul class="wc-wizard-next-steps"> <li class="wc-wizard-next-step-item"> <div class="wc-wizard-next-step-description"> <p class="next-step-heading"><?php esc_html_e( 'Next step', 'woocommerce' ); ?></p> <h3 class="next-step-description"><?php esc_html_e( 'Create some products', 'woocommerce' ); ?></h3> <p class="next-step-extra-info"><?php esc_html_e( "You're ready to add products to your store.", 'woocommerce' ); ?></p> </div> <div class="wc-wizard-next-step-action"> <p class="wc-setup-actions step"> <a class="button button-primary button-large" href="<?php echo esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ); ?>"> <?php esc_html_e( 'Create a product', 'woocommerce' ); ?> </a> </p> </div> </li> <li class="wc-wizard-next-step-item"> <div class="wc-wizard-next-step-description"> <p class="next-step-heading"><?php esc_html_e( 'Have an existing store?', 'woocommerce' ); ?></p> <h3 class="next-step-description"><?php esc_html_e( 'Import products', 'woocommerce' ); ?></h3> <p class="next-step-extra-info"><?php esc_html_e( 'Transfer existing products to your new store — just import a CSV file.', 'woocommerce' ); ?></p> </div> <div class="wc-wizard-next-step-action"> <p class="wc-setup-actions step"> <a class="button button-large" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ); ?>"> <?php esc_html_e( 'Import products', 'woocommerce' ); ?> </a> </p> </div> </li> <li class="wc-wizard-additional-steps"> <div class="wc-wizard-next-step-description"> <p class="next-step-heading"><?php esc_html_e( 'You can also:', 'woocommerce' ); ?></p> </div> <div class="wc-wizard-next-step-action"> <p class="wc-setup-actions step"> <a class="button button-large" href="<?php echo esc_url( admin_url() ); ?>"> <?php esc_html_e( 'Visit Dashboard', 'woocommerce' ); ?> </a> <a class="button button-large" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings' ) ); ?>"> <?php esc_html_e( 'Review Settings', 'woocommerce' ); ?> </a> <a class="button button-large" href="<?php echo esc_url( add_query_arg( array( 'autofocus' => array( 'panel' => 'woocommerce' ), 'url' => wc_get_page_permalink( 'shop' ) ), admin_url( 'customize.php' ) ) ); ?>"> <?php esc_html_e( 'View & Customize', 'woocommerce' ); ?> </a> </p> </div> </li> </ul> <p class="next-steps-help-text"><?php echo wp_kses_post( $help_text ); ?></p> <?php } } includes/admin/marketplace-suggestions/templates/html-product-data-extensions.php 0000644 00000003046 15132754524 0024552 0 ustar 00 <?php /** * The marketplace suggestions tab HTML in the product tabs * * @package WooCommerce\Classes * @since 3.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="marketplace_suggestions" class="panel woocommerce_options_panel hidden"> <?php WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-header' ); WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-body' ); WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-footer' ); ?> <div class="marketplace-suggestions-metabox-nosuggestions-placeholder hidden"> <img src="https://woocommerce.com/wp-content/plugins/wccom-plugins/marketplace-suggestions/icons/get_more_options.svg" class="marketplace-suggestion-icon"> <div class="marketplace-suggestion-placeholder-content"> <h4><?php esc_html_e( 'Enhance your products', 'woocommerce' ); ?></h4> <p><?php esc_html_e( 'Extensions can add new functionality to your product pages that make your store stand out', 'woocommerce' ); ?></p> </div> <a href="https://woocommerce.com/product-category/woocommerce-extensions/?utm_source=editproduct&utm_campaign=marketplacesuggestions&utm_medium=product" target="blank" class="button"><?php esc_html_e( 'Browse the Marketplace', 'woocommerce' ); ?></a><br /> <a class="marketplace-suggestion-manage-link" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ) ); ?>"><?php esc_html_e( 'Manage suggestions', 'woocommerce' ); ?></a> </div> </div> includes/admin/marketplace-suggestions/class-wc-marketplace-suggestions.php 0000644 00000013716 15132754524 0023405 0 ustar 00 <?php /** * Marketplace suggestions * * Behaviour for displaying in-context suggestions for marketplace extensions. * * @package WooCommerce\Classes * @since 3.6.0 */ defined( 'ABSPATH' ) || exit; /** * Marketplace suggestions core behaviour. */ class WC_Marketplace_Suggestions { /** * Initialise. */ public static function init() { if ( ! self::allow_suggestions() ) { return; } // Add suggestions to the product tabs. add_action( 'woocommerce_product_data_tabs', array( __CLASS__, 'product_data_tabs' ) ); add_action( 'woocommerce_product_data_panels', array( __CLASS__, 'product_data_panels' ) ); // Register ajax api handlers. add_action( 'wp_ajax_woocommerce_add_dismissed_marketplace_suggestion', array( __CLASS__, 'post_add_dismissed_suggestion_handler' ) ); // Register hooks for rendering suggestions container markup. add_action( 'wc_marketplace_suggestions_products_empty_state', array( __CLASS__, 'render_products_list_empty_state' ) ); add_action( 'wc_marketplace_suggestions_orders_empty_state', array( __CLASS__, 'render_orders_list_empty_state' ) ); } /** * Product data tabs filter * * Adds a new Extensions tab to the product data meta box. * * @param array $tabs Existing tabs. * * @return array */ public static function product_data_tabs( $tabs ) { $tabs['marketplace-suggestions'] = array( 'label' => _x( 'Get more options', 'Marketplace suggestions', 'woocommerce' ), 'target' => 'marketplace_suggestions', 'class' => array(), 'priority' => 1000, ); return $tabs; } /** * Render additional panels in the product data metabox. */ public static function product_data_panels() { include dirname( __FILE__ ) . '/templates/html-product-data-extensions.php'; } /** * Return an array of suggestions the user has dismissed. */ public static function get_dismissed_suggestions() { $dismissed_suggestions = array(); $dismissed_suggestions_data = get_user_meta( get_current_user_id(), 'wc_marketplace_suggestions_dismissed_suggestions', true ); if ( $dismissed_suggestions_data ) { $dismissed_suggestions = $dismissed_suggestions_data; if ( ! is_array( $dismissed_suggestions ) ) { $dismissed_suggestions = array(); } } return $dismissed_suggestions; } /** * POST handler for adding a dismissed suggestion. */ public static function post_add_dismissed_suggestion_handler() { if ( ! check_ajax_referer( 'add_dismissed_marketplace_suggestion' ) ) { wp_die(); } $post_data = wp_unslash( $_POST ); $suggestion_slug = sanitize_text_field( $post_data['slug'] ); if ( ! $suggestion_slug ) { wp_die(); } $dismissed_suggestions = self::get_dismissed_suggestions(); if ( in_array( $suggestion_slug, $dismissed_suggestions, true ) ) { wp_die(); } $dismissed_suggestions[] = $suggestion_slug; update_user_meta( get_current_user_id(), 'wc_marketplace_suggestions_dismissed_suggestions', $dismissed_suggestions ); wp_die(); } /** * Render suggestions containers in products list empty state. */ public static function render_products_list_empty_state() { self::render_suggestions_container( 'products-list-empty-header' ); self::render_suggestions_container( 'products-list-empty-body' ); self::render_suggestions_container( 'products-list-empty-footer' ); } /** * Render suggestions containers in orders list empty state. */ public static function render_orders_list_empty_state() { self::render_suggestions_container( 'orders-list-empty-header' ); self::render_suggestions_container( 'orders-list-empty-body' ); self::render_suggestions_container( 'orders-list-empty-footer' ); } /** * Render a suggestions container element, with the specified context. * * @param string $context Suggestion context name (rendered as a css class). */ public static function render_suggestions_container( $context ) { include dirname( __FILE__ ) . '/views/container.php'; } /** * Should suggestions be displayed? * * @param string $screen_id The current admin screen. * * @return bool */ public static function show_suggestions_for_screen( $screen_id ) { // We only show suggestions on certain admin screens. if ( ! in_array( $screen_id, array( 'edit-product', 'edit-shop_order', 'product' ), true ) ) { return false; } return self::allow_suggestions(); } /** * Should suggestions be displayed? * * @return bool */ public static function allow_suggestions() { // We currently only support English suggestions. $locale = get_locale(); $suggestion_locales = array( 'en_AU', 'en_CA', 'en_GB', 'en_NZ', 'en_US', 'en_ZA', ); if ( ! in_array( $locale, $suggestion_locales, true ) ) { return false; } // Suggestions are only displayed if user can install plugins. if ( ! current_user_can( 'install_plugins' ) ) { return false; } // Suggestions may be disabled via a setting under Accounts & Privacy. if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) { return false; } // User can disabled all suggestions via filter. return apply_filters( 'woocommerce_allow_marketplace_suggestions', true ); } /** * Pull suggestion data from options. This is retrieved from a remote endpoint. * * @return array of json API data */ public static function get_suggestions_api_data() { $data = get_option( 'woocommerce_marketplace_suggestions', array() ); // If the options have never been updated, or were updated over a week ago, queue update. if ( empty( $data['updated'] ) || ( time() - WEEK_IN_SECONDS ) > $data['updated'] ) { $next = WC()->queue()->get_next( 'woocommerce_update_marketplace_suggestions' ); if ( ! $next ) { WC()->queue()->cancel_all( 'woocommerce_update_marketplace_suggestions' ); WC()->queue()->schedule_single( time(), 'woocommerce_update_marketplace_suggestions' ); } } return ! empty( $data['suggestions'] ) ? $data['suggestions'] : array(); } } WC_Marketplace_Suggestions::init(); includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php 0000644 00000004045 15132754524 0022472 0 ustar 00 <?php /** * Marketplace suggestions updater * * Uses WC_Queue to ensure marketplace suggestions data is up to date and cached locally. * * @package WooCommerce\Classes * @since 3.6.0 */ defined( 'ABSPATH' ) || exit; /** * Marketplace Suggestions Updater */ class WC_Marketplace_Updater { /** * Setup. */ public static function load() { add_action( 'init', array( __CLASS__, 'init' ) ); } /** * Schedule events and hook appropriate actions. */ public static function init() { add_action( 'woocommerce_update_marketplace_suggestions', array( __CLASS__, 'update_marketplace_suggestions' ) ); } /** * Fetches new marketplace data, updates wc_marketplace_suggestions. */ public static function update_marketplace_suggestions() { $data = get_option( 'woocommerce_marketplace_suggestions', array( 'suggestions' => array(), 'updated' => time(), ) ); $data['updated'] = time(); $url = 'https://woocommerce.com/wp-json/wccom/marketplace-suggestions/1.0/suggestions.json'; $request = wp_safe_remote_get( $url ); if ( is_wp_error( $request ) ) { self::retry(); return update_option( 'woocommerce_marketplace_suggestions', $data, false ); } $body = wp_remote_retrieve_body( $request ); if ( empty( $body ) ) { self::retry(); return update_option( 'woocommerce_marketplace_suggestions', $data, false ); } $body = json_decode( $body, true ); if ( empty( $body ) || ! is_array( $body ) ) { self::retry(); return update_option( 'woocommerce_marketplace_suggestions', $data, false ); } $data['suggestions'] = $body; return update_option( 'woocommerce_marketplace_suggestions', $data, false ); } /** * Used when an error has occured when fetching suggestions. * Re-schedules the job earlier than the main weekly one. */ public static function retry() { WC()->queue()->cancel_all( 'woocommerce_update_marketplace_suggestions' ); WC()->queue()->schedule_single( time() + DAY_IN_SECONDS, 'woocommerce_update_marketplace_suggestions' ); } } WC_Marketplace_Updater::load(); includes/admin/marketplace-suggestions/views/container.php 0000644 00000000437 15132754524 0020146 0 ustar 00 <?php /** * Marketplace suggestions container * * @package WooCommerce\Templates * @version 3.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="marketplace-suggestions-container" data-marketplace-suggestions-context="<?php echo esc_attr( $context ); ?>" > </div> includes/admin/reports/class-wc-report-sales-by-category.php 0000644 00000032403 15132754524 0020240 0 ustar 00 <?php /** * Sales by category report functionality * * @package WooCommerce\Admin\Reporting */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Sales_By_Category * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Sales_By_Category extends WC_Admin_Report { /** * Chart colors. * * @var array */ public $chart_colours = array(); /** * Categories ids. * * @var array */ public $show_categories = array(); /** * Item sales. * * @var array */ private $item_sales = array(); /** * Item sales and times. * * @var array */ private $item_sales_and_times = array(); /** * Constructor. */ public function __construct() { if ( isset( $_GET['show_categories'] ) ) { $this->show_categories = is_array( $_GET['show_categories'] ) ? array_map( 'absint', $_GET['show_categories'] ) : array( absint( $_GET['show_categories'] ) ); } } /** * Get all product ids in a category (and its children). * * @param int $category_id Category ID. * @return array */ public function get_products_in_category( $category_id ) { $term_ids = get_term_children( $category_id, 'product_cat' ); $term_ids[] = $category_id; $product_ids = get_objects_in_term( $term_ids, 'product_cat' ); return array_unique( apply_filters( 'woocommerce_report_sales_by_category_get_products_in_category', $product_ids, $category_id ) ); } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { if ( empty( $this->show_categories ) ) { return array(); } $legend = array(); $index = 0; foreach ( $this->show_categories as $category ) { $category = get_term( $category, 'product_cat' ); $total = 0; $product_ids = $this->get_products_in_category( $category->term_id ); foreach ( $product_ids as $id ) { if ( isset( $this->item_sales[ $id ] ) ) { $total += $this->item_sales[ $id ]; } } $legend[] = array( /* translators: 1: total items sold 2: category name */ 'title' => sprintf( __( '%1$s sales in %2$s', 'woocommerce' ), '<strong>' . wc_price( $total ) . '</strong>', $category->name ), 'color' => isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0], 'highlight_series' => $index, ); $index++; } return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( '#3498db', '#34495e', '#1abc9c', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#27ae60', '#f39c12', '#d35400', '#c0392b' ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); // Get item sales data. if ( ! empty( $this->show_categories ) ) { $order_items = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => 'ID, product_id, post_date', 'query_type' => 'get_results', 'filter_range' => true, ) ); $this->item_sales = array(); $this->item_sales_and_times = array(); if ( is_array( $order_items ) ) { foreach ( $order_items as $order_item ) { switch ( $this->chart_groupby ) { case 'day': $time = strtotime( gmdate( 'Ymd', strtotime( $order_item->post_date ) ) ) * 1000; break; case 'month': default: $time = strtotime( gmdate( 'Ym', strtotime( $order_item->post_date ) ) . '01' ) * 1000; break; } $this->item_sales_and_times[ $time ][ $order_item->product_id ] = isset( $this->item_sales_and_times[ $time ][ $order_item->product_id ] ) ? $this->item_sales_and_times[ $time ][ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount; $this->item_sales[ $order_item->product_id ] = isset( $this->item_sales[ $order_item->product_id ] ) ? $this->item_sales[ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount; } } } include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { return array( array( 'title' => __( 'Categories', 'woocommerce' ), 'callback' => array( $this, 'category_widget' ), ), ); } /** * Output category widget. */ public function category_widget() { $categories = get_terms( 'product_cat', array( 'orderby' => 'name' ) ); ?> <form method="GET"> <div> <select multiple="multiple" data-placeholder="<?php esc_attr_e( 'Select categories…', 'woocommerce' ); ?>" class="wc-enhanced-select" id="show_categories" name="show_categories[]" style="width: 205px;"> <?php $r = array(); $r['pad_counts'] = 1; $r['hierarchical'] = 1; $r['hide_empty'] = 1; $r['value'] = 'id'; $r['selected'] = $this->show_categories; include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; echo wc_walk_category_dropdown_tree( $categories, 0, $r ); // @codingStandardsIgnoreLine ?> </select> <?php // @codingStandardsIgnoreStart ?> <a href="#" class="select_none"><?php esc_html_e( 'None', 'woocommerce' ); ?></a> <a href="#" class="select_all"><?php esc_html_e( 'All', 'woocommerce' ); ?></a> <button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button> <input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( wp_unslash( $_GET['range'] ) ) : ''; ?>" /> <input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" /> <input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" /> <input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( wp_unslash( $_GET['page'] ) ) : ''; ?>" /> <input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( wp_unslash( $_GET['tab'] ) ) : ''; ?>" /> <input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( wp_unslash( $_GET['report'] ) ) : ''; ?>" /> <?php // @codingStandardsIgnoreEnd ?> </div> <script type="text/javascript"> jQuery(function(){ // Select all/None jQuery( '.chart-widget' ).on( 'click', '.select_all', function() { jQuery(this).closest( 'div' ).find( 'select option' ).attr( 'selected', 'selected' ); jQuery(this).closest( 'div' ).find('select').trigger( 'change' ); return false; }); jQuery( '.chart-widget').on( 'click', '.select_none', function() { jQuery(this).closest( 'div' ).find( 'select option' ).prop( 'selected', false ); jQuery(this).closest( 'div' ).find('select').trigger( 'change' ); return false; }); }); </script> </form> <?php } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="chart" data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>" data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>" > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Get the main chart. */ public function get_main_chart() { global $wp_locale; if ( empty( $this->show_categories ) ) { ?> <div class="chart-container"> <p class="chart-prompt"><?php esc_html_e( 'Choose a category to view stats', 'woocommerce' ); ?></p> </div> <?php } else { $chart_data = array(); $index = 0; foreach ( $this->show_categories as $category ) { $category = get_term( $category, 'product_cat' ); $product_ids = $this->get_products_in_category( $category->term_id ); $category_chart_data = array(); for ( $i = 0; $i <= $this->chart_interval; $i ++ ) { $interval_total = 0; switch ( $this->chart_groupby ) { case 'day': $time = strtotime( gmdate( 'Ymd', strtotime( "+{$i} DAY", $this->start_date ) ) ) * 1000; break; case 'month': default: $time = strtotime( gmdate( 'Ym', strtotime( "+{$i} MONTH", $this->start_date ) ) . '01' ) * 1000; break; } foreach ( $product_ids as $id ) { if ( isset( $this->item_sales_and_times[ $time ][ $id ] ) ) { $interval_total += $this->item_sales_and_times[ $time ][ $id ]; } } $category_chart_data[] = array( $time, (float) wc_format_decimal( $interval_total, wc_get_price_decimals() ) ); } $chart_data[ $category->term_id ]['category'] = $category->name; $chart_data[ $category->term_id ]['data'] = $category_chart_data; $index++; } ?> <div class="chart-container"> <div class="chart-placeholder main"></div> </div> <?php // @codingStandardsIgnoreStart ?> <script type="text/javascript"> var main_chart; jQuery(function(){ var drawGraph = function( highlight ) { var series = [ <?php $index = 0; foreach ( $chart_data as $data ) { $color = isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0]; $width = $this->barwidth / sizeof( $chart_data ); $offset = ( $width * $index ); $series = $data['data']; foreach ( $series as $key => $series_data ) { $series[ $key ][0] = $series_data[0] + $offset; } $series = wp_json_encode( $series ); echo '{ label: "' . esc_js( $data['category'] ) . '", data: JSON.parse( decodeURIComponent( "' . rawurlencode( $series ) . '" ) ), color: "' . $color . '", bars: { fillColor: "' . $color . '", fill: true, show: true, lineWidth: 1, align: "center", barWidth: ' . $width * 0.75 . ', stack: false }, ' . $this->get_currency_tooltip() . ', enable_tooltip: true, prepend_label: true },'; $index++; } ?> ]; if ( highlight !== 'undefined' && series[ highlight ] ) { highlight_series = series[ highlight ]; highlight_series.color = '#9c5d90'; if ( highlight_series.bars ) { highlight_series.bars.fillColor = '#9c5d90'; } if ( highlight_series.lines ) { highlight_series.lines.lineWidth = 5; } } main_chart = jQuery.plot( jQuery('.chart-placeholder.main'), series, { legend: { show: false }, grid: { color: '#aaa', borderColor: 'transparent', borderWidth: 0, hoverable: true }, xaxes: [ { color: '#aaa', reserveSpace: true, position: "bottom", tickColor: 'transparent', mode: "time", timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ), tickLength: 1, minTickSize: [1, "<?php echo $this->chart_groupby; ?>"], tickSize: [1, "<?php echo $this->chart_groupby; ?>"], font: { color: "#aaa" } } ], yaxes: [ { min: 0, tickDecimals: 2, color: 'transparent', font: { color: "#aaa" } } ], } ); jQuery('.chart-placeholder').trigger( 'resize' ); } drawGraph(); jQuery('.highlight_series').on( 'mouseenter', function() { drawGraph( jQuery(this).data('series') ); } ).on( 'mouseleave', function() { drawGraph(); } ); }); </script> <?php // @codingStandardsIgnoreEnd ?> <?php } } } includes/admin/reports/class-wc-report-sales-by-product.php 0000644 00000050515 15132754524 0020107 0 ustar 00 <?php /** * Sales By Product Reporting * * @package WooCommerce\Admin\Reporting */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Sales_By_Product * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Sales_By_Product extends WC_Admin_Report { /** * Chart colors. * * @var array */ public $chart_colours = array(); /** * Product ids. * * @var array */ public $product_ids = array(); /** * Product ids with titles. * * @var array */ public $product_ids_titles = array(); /** * Constructor. */ public function __construct() { // @codingStandardsIgnoreStart if ( isset( $_GET['product_ids'] ) && is_array( $_GET['product_ids'] ) ) { $this->product_ids = array_filter( array_map( 'absint', $_GET['product_ids'] ) ); } elseif ( isset( $_GET['product_ids'] ) ) { $this->product_ids = array_filter( array( absint( $_GET['product_ids'] ) ) ); } // @codingStandardsIgnoreEnd } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { if ( empty( $this->product_ids ) ) { return array(); } $legend = array(); $total_sales = $this->get_order_report_data( array( 'data' => array( '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $total_items = absint( $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ) ); $legend[] = array( /* translators: %s: total items sold */ 'title' => sprintf( __( '%s sales for the selected items', 'woocommerce' ), '<strong>' . wc_price( $total_sales ) . '</strong>' ), 'color' => $this->chart_colours['sales_amount'], 'highlight_series' => 1, ); $legend[] = array( /* translators: %s: total items purchased */ 'title' => sprintf( __( '%s purchases for the selected items', 'woocommerce' ), '<strong>' . ( $total_items ) . '</strong>' ), 'color' => $this->chart_colours['item_count'], 'highlight_series' => 0, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'sales_amount' => '#3498db', 'item_count' => '#d4d9dc', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); if ( ! empty( $this->product_ids ) ) { $widgets[] = array( 'title' => __( 'Showing reports for:', 'woocommerce' ), 'callback' => array( $this, 'current_filters' ), ); } $widgets[] = array( 'title' => '', 'callback' => array( $this, 'products_widget' ), ); return $widgets; } /** * Output current filters. */ public function current_filters() { $this->product_ids_titles = array(); foreach ( $this->product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $this->product_ids_titles[] = $product->get_formatted_name(); } else { $this->product_ids_titles[] = '#' . $product_id; } } echo '<p><strong>' . wp_kses_post( implode( ', ', $this->product_ids_titles ) ) . '</strong></p>'; echo '<p><a class="button" href="' . esc_url( remove_query_arg( 'product_ids' ) ) . '">' . esc_html__( 'Reset', 'woocommerce' ) . '</a></p>'; } /** * Output products widget. */ public function products_widget() { ?> <h4 class="section_title"><span><?php esc_html_e( 'Product search', 'woocommerce' ); ?></span></h4> <div class="section"> <form method="GET"> <div> <?php // @codingStandardsIgnoreStart ?> <select class="wc-product-search" style="width:203px;" multiple="multiple" id="product_ids" name="product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations"></select> <button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button> <input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( $_GET['range'] ) : ''; ?>" /> <input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( $_GET['start_date'] ) : ''; ?>" /> <input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( $_GET['end_date'] ) : ''; ?>" /> <input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( $_GET['page'] ) : ''; ?>" /> <input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( $_GET['tab'] ) : ''; ?>" /> <input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( $_GET['report'] ) : ''; ?>" /> <?php wp_nonce_field( 'custom_range', 'wc_reports_nonce', false ); ?> <?php // @codingStandardsIgnoreEnd ?> </div> </form> </div> <h4 class="section_title"><span><?php esc_html_e( 'Top sellers', 'woocommerce' ); ?></span></h4> <div class="section"> <table cellspacing="0"> <?php $top_sellers = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); if ( $top_sellers ) { // @codingStandardsIgnoreStart foreach ( $top_sellers as $product ) { echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '"> <td class="count">' . esc_html( $product->order_item_qty ) . '</td> <td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td> <td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '</td> </tr>'; } // @codingStandardsIgnoreEnd } else { echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>'; } ?> </table> </div> <h4 class="section_title"><span><?php esc_html_e( 'Top freebies', 'woocommerce' ); ?></span></h4> <div class="section"> <table cellspacing="0"> <?php $top_freebies = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'where_meta' => array( array( 'type' => 'order_item_meta', 'meta_key' => '_line_subtotal', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => '0', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => '=', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( $top_freebies ) { // @codingStandardsIgnoreStart foreach ( $top_freebies as $product ) { echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '"> <td class="count">' . esc_html( $product->order_item_qty ) . '</td> <td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td> <td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '</td> </tr>'; } // @codingStandardsIgnoreEnd } else { echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>'; } ?> </table> </div> <h4 class="section_title"><span><?php esc_html_e( 'Top earners', 'woocommerce' ); ?></span></h4> <div class="section"> <table cellspacing="0"> <?php $top_earners = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_total', ), ), 'order_by' => 'order_item_total DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); if ( $top_earners ) { // @codingStandardsIgnoreStart foreach ( $top_earners as $product ) { echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '"> <td class="count">' . wc_price( $product->order_item_total ) . '</td> <td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td> <td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'sales' ) . '</td> </tr>'; } // @codingStandardsIgnoreEnd } else { echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>'; } ?> </table> </div> <script type="text/javascript"> jQuery( '.section_title' ).on( 'click', function() { var next_section = jQuery( this ).next( '.section' ); if ( jQuery( next_section ).is( ':visible' ) ) { return false; } jQuery( '.section:visible' ).slideUp(); jQuery( '.section_title' ).removeClass( 'open' ); jQuery( this ).addClass( 'open' ).next( '.section' ).slideDown(); return false; } ); jQuery( '.section' ).slideUp( 100, function() { <?php if ( empty( $this->product_ids ) ) : ?> jQuery( '.section_title:eq(1)' ).trigger( 'click' ); <?php endif; ?> } ); </script> <?php } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_html( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="chart" data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>" data-groupby="<?php echo $this->chart_groupby; ?>"<?php // @codingStandardsIgnoreLine ?> > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Get the main chart. */ public function get_main_chart() { global $wp_locale; if ( empty( $this->product_ids ) ) { ?> <div class="chart-container"> <p class="chart-prompt"><?php esc_html_e( 'Choose a product to view stats', 'woocommerce' ); ?></p> </div> <?php } else { // Get orders and dates in range - we want the SUM of order totals, COUNT of order items, COUNT of orders, and the date. $order_item_counts = $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'group_by' => 'product_id,' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $order_item_amounts = $this->get_order_report_data( array( 'data' => array( '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'group_by' => 'product_id, ' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); // Prepare data for report. $order_item_counts = $this->prepare_chart_data( $order_item_counts, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $order_item_amounts = $this->prepare_chart_data( $order_item_amounts, 'post_date', 'order_item_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_item_counts' => array_values( $order_item_counts ), 'order_item_amounts' => array_values( $order_item_amounts ), ) ); ?> <div class="chart-container"> <div class="chart-placeholder main"></div> </div> <?php // @codingStandardsIgnoreStart ?> <script type="text/javascript"> var main_chart; jQuery(function(){ var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) ); var drawGraph = function( highlight ) { var series = [ { label: "<?php echo esc_js( __( 'Number of items sold', 'woocommerce' ) ) ?>", data: order_data.order_item_counts, color: '<?php echo $this->chart_colours['item_count']; ?>', bars: { fillColor: '<?php echo $this->chart_colours['item_count']; ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo $this->barwidth; ?> * 0.5, align: 'center' }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Sales amount', 'woocommerce' ) ) ?>", data: order_data.order_item_amounts, yaxis: 2, color: '<?php echo $this->chart_colours['sales_amount']; ?>', points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 4, fill: false }, shadowSize: 0, <?php echo $this->get_currency_tooltip(); ?> } ]; if ( highlight !== 'undefined' && series[ highlight ] ) { highlight_series = series[ highlight ]; highlight_series.color = '#9c5d90'; if ( highlight_series.bars ) highlight_series.bars.fillColor = '#9c5d90'; if ( highlight_series.lines ) { highlight_series.lines.lineWidth = 5; } } main_chart = jQuery.plot( jQuery('.chart-placeholder.main'), series, { legend: { show: false }, grid: { color: '#aaa', borderColor: 'transparent', borderWidth: 0, hoverable: true }, xaxes: [ { color: '#aaa', position: "bottom", tickColor: 'transparent', mode: "time", timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ), tickLength: 1, minTickSize: [1, "<?php echo $this->chart_groupby; ?>"], font: { color: "#aaa" } } ], yaxes: [ { min: 0, minTickSize: 1, tickDecimals: 0, color: '#ecf0f1', font: { color: "#aaa" } }, { position: "right", min: 0, tickDecimals: 2, alignTicksWithAxis: 1, color: 'transparent', font: { color: "#aaa" } } ], } ); jQuery('.chart-placeholder').trigger( 'resize' ); } drawGraph(); jQuery('.highlight_series').on( 'mouseenter', function() { drawGraph( jQuery(this).data('series') ); } ).on( 'mouseleave', function() { drawGraph(); } ); }); </script> <?php // @codingStandardsIgnoreEnd } } } includes/admin/reports/class-wc-report-stock.php 0000644 00000011007 15132754524 0016026 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * WC_Report_Stock. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Stock extends WP_List_Table { /** * Max items. * * @var int */ protected $max_items; /** * Constructor. */ public function __construct() { parent::__construct( array( 'singular' => 'stock', 'plural' => 'stock', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { _e( 'No products found.', 'woocommerce' ); } /** * Don't need this. * * @param string $position */ public function display_tablenav( $position ) { if ( 'top' !== $position ) { parent::display_tablenav( $position ); } } /** * Output the report. */ public function output_report() { $this->prepare_items(); echo '<div id="poststuff" class="woocommerce-reports-wide">'; $this->display(); echo '</div>'; } /** * Get column value. * * @param mixed $item * @param string $column_name */ public function column_default( $item, $column_name ) { global $product; if ( ! $product || $product->get_id() !== $item->id ) { $product = wc_get_product( $item->id ); } if ( ! $product ) { return; } switch ( $column_name ) { case 'product': if ( $sku = $product->get_sku() ) { echo esc_html( $sku ) . ' - '; } echo esc_html( $product->get_name() ); // Get variation data. if ( $product->is_type( 'variation' ) ) { echo '<div class="description">' . wp_kses_post( wc_get_formatted_variation( $product, true ) ) . '</div>'; } break; case 'parent': if ( $item->parent ) { echo esc_html( get_the_title( $item->parent ) ); } else { echo '-'; } break; case 'stock_status': if ( $product->is_on_backorder() ) { $stock_html = '<mark class="onbackorder">' . __( 'On backorder', 'woocommerce' ) . '</mark>'; } elseif ( $product->is_in_stock() ) { $stock_html = '<mark class="instock">' . __( 'In stock', 'woocommerce' ) . '</mark>'; } else { $stock_html = '<mark class="outofstock">' . __( 'Out of stock', 'woocommerce' ) . '</mark>'; } echo apply_filters( 'woocommerce_admin_stock_html', $stock_html, $product ); break; case 'stock_level': echo esc_html( $product->get_stock_quantity() ); break; case 'wc_actions': ?><p> <?php $actions = array(); $action_id = $product->is_type( 'variation' ) ? $item->parent : $item->id; $actions['edit'] = array( 'url' => admin_url( 'post.php?post=' . $action_id . '&action=edit' ), 'name' => __( 'Edit', 'woocommerce' ), 'action' => 'edit', ); if ( $product->is_visible() ) { $actions['view'] = array( 'url' => get_permalink( $action_id ), 'name' => __( 'View', 'woocommerce' ), 'action' => 'view', ); } $actions = apply_filters( 'woocommerce_admin_stock_report_product_actions', $actions, $product ); foreach ( $actions as $action ) { printf( '<a class="button tips %1$s" href="%2$s" data-tip="%3$s">%4$s</a>', esc_attr( $action['action'] ), esc_url( $action['url'] ), sprintf( esc_attr__( '%s product', 'woocommerce' ), $action['name'] ), esc_html( $action['name'] ) ); } ?> </p> <?php break; } } /** * Get columns. * * @return array */ public function get_columns() { $columns = array( 'product' => __( 'Product', 'woocommerce' ), 'parent' => __( 'Parent', 'woocommerce' ), 'stock_level' => __( 'Units in stock', 'woocommerce' ), 'stock_status' => __( 'Stock status', 'woocommerce' ), 'wc_actions' => __( 'Actions', 'woocommerce' ), ); return $columns; } /** * Prepare customer list items. */ public function prepare_items() { $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); $current_page = absint( $this->get_pagenum() ); $per_page = apply_filters( 'woocommerce_admin_stock_report_products_per_page', 20 ); $this->get_items( $current_page, $per_page ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $this->max_items, 'per_page' => $per_page, 'total_pages' => ceil( $this->max_items / $per_page ), ) ); } } includes/admin/reports/class-wc-report-coupon-usage.php 0000644 00000042723 15132754524 0017321 0 ustar 00 <?php /** * Coupon usage report functionality * * @package WooCommerce\Admin\Reports */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Coupon_Usage * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Coupon_Usage extends WC_Admin_Report { /** * Chart colors. * * @var array */ public $chart_colours = array(); /** * Coupon codes. * * @var array */ public $coupon_codes = array(); /** * Constructor. */ public function __construct() { if ( isset( $_GET['coupon_codes'] ) && is_array( $_GET['coupon_codes'] ) ) { $this->coupon_codes = array_filter( array_map( 'sanitize_text_field', wp_unslash( $_GET['coupon_codes'] ) ) ); } elseif ( isset( $_GET['coupon_codes'] ) ) { $this->coupon_codes = array_filter( array( sanitize_text_field( wp_unslash( $_GET['coupon_codes'] ) ) ) ); } } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { $legend = array(); $total_discount_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); $total_coupons_query = array( 'data' => array( 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'order_coupon_count', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); if ( ! empty( $this->coupon_codes ) ) { $coupon_code_query = array( 'type' => 'order_item', 'key' => 'order_item_name', 'value' => $this->coupon_codes, 'operator' => 'IN', ); $total_discount_query['where'][] = $coupon_code_query; $total_coupons_query['where'][] = $coupon_code_query; } $total_discount = $this->get_order_report_data( $total_discount_query ); $total_coupons = absint( $this->get_order_report_data( $total_coupons_query ) ); $legend[] = array( /* translators: %s: discount amount */ 'title' => sprintf( __( '%s discounts in total', 'woocommerce' ), '<strong>' . wc_price( $total_discount ) . '</strong>' ), 'color' => $this->chart_colours['discount_amount'], 'highlight_series' => 1, ); $legend[] = array( /* translators: %s: coupons amount */ 'title' => sprintf( __( '%s coupons used in total', 'woocommerce' ), '<strong>' . $total_coupons . '</strong>' ), 'color' => $this->chart_colours['coupon_count'], 'highlight_series' => 0, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'discount_amount' => '#3498db', 'coupon_count' => '#d4d9dc', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); $widgets[] = array( 'title' => '', 'callback' => array( $this, 'coupons_widget' ), ); return $widgets; } /** * Output coupons widget. */ public function coupons_widget() { ?> <h4 class="section_title"><span><?php esc_html_e( 'Filter by coupon', 'woocommerce' ); ?></span></h4> <div class="section"> <form method="GET"> <div> <?php $used_coupons = $this->get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'distinct' => true, 'name' => 'order_item_name', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_col', 'filter_range' => false, ) ); if ( ! empty( $used_coupons ) && is_array( $used_coupons ) ) : ?> <select id="coupon_codes" name="coupon_codes" class="wc-enhanced-select" data-placeholder="<?php esc_attr_e( 'Choose coupons…', 'woocommerce' ); ?>" style="width:100%;"> <option value=""><?php esc_html_e( 'All coupons', 'woocommerce' ); ?></option> <?php foreach ( $used_coupons as $coupon ) { echo '<option value="' . esc_attr( $coupon ) . '"' . wc_selected( $coupon, $this->coupon_codes ) . '>' . esc_html( $coupon ) . '</option>'; } ?> </select> <?php // @codingStandardsIgnoreStart ?> <button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button> <input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( wp_unslash( $_GET['range'] ) ) : ''; ?>" /> <input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" /> <input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" /> <input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( wp_unslash( $_GET['page'] ) ) : ''; ?>" /> <input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( wp_unslash( $_GET['tab'] ) ) : ''; ?>" /> <input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( wp_unslash( $_GET['report'] ) ) : ''; ?>" /> <?php // @codingStandardsIgnoreEnd ?> <?php else : ?> <span><?php esc_html_e( 'No used coupons found', 'woocommerce' ); ?></span> <?php endif; ?> </div> </form> </div> <h4 class="section_title"><span><?php esc_html_e( 'Most popular', 'woocommerce' ); ?></span></h4> <div class="section"> <table cellspacing="0"> <?php $most_popular = $this->get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'name' => 'coupon_code', ), 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'coupon_count', ), ), 'where' => array( array( 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'order_by' => 'coupon_count DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( ! empty( $most_popular ) && is_array( $most_popular ) ) { foreach ( $most_popular as $coupon ) { echo '<tr class="' . ( in_array( $coupon->coupon_code, $this->coupon_codes ) ? 'active' : '' ) . '"> <td class="count" width="1%">' . esc_html( $coupon->coupon_count ) . '</td> <td class="name"><a href="' . esc_url( add_query_arg( 'coupon_codes', $coupon->coupon_code ) ) . '">' . esc_html( $coupon->coupon_code ) . '</a></td> </tr>'; } } else { echo '<tr><td colspan="2">' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '</td></tr>'; } ?> </table> </div> <h4 class="section_title"><span><?php esc_html_e( 'Most discount', 'woocommerce' ); ?></span></h4> <div class="section"> <table cellspacing="0"> <?php $most_discount = $this->get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'name' => 'coupon_code', ), 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), ), 'where' => array( array( 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'order_by' => 'discount_amount DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( ! empty( $most_discount ) && is_array( $most_discount ) ) { foreach ( $most_discount as $coupon ) { // @codingStandardsIgnoreStart echo '<tr class="' . ( in_array( $coupon->coupon_code, $this->coupon_codes ) ? 'active' : '' ) . '"> <td class="count" width="1%">' . wc_price( $coupon->discount_amount ) . '</td> <td class="name"><a href="' . esc_url( add_query_arg( 'coupon_codes', $coupon->coupon_code ) ) . '">' . esc_html( $coupon->coupon_code ) . '</a></td> </tr>'; // @codingStandardsIgnoreEnd } } else { echo '<tr><td colspan="3">' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '</td></tr>'; } ?> </table> </div> <script type="text/javascript"> jQuery( '.section_title' ).on( 'click', function() { var next_section = jQuery( this ).next( '.section' ); if ( jQuery( next_section ).is( ':visible' ) ) { return false; } jQuery( '.section:visible' ).slideUp(); jQuery( '.section_title' ).removeClass( 'open' ); jQuery( this ).addClass( 'open' ).next( '.section' ).slideDown(); return false; } ); jQuery( '.section' ).slideUp( 100, function() { <?php if ( empty( $this->coupon_codes ) ) : ?> jQuery( '.section_title:eq(1)' ).trigger( 'click' ); <?php else : ?> jQuery( '.section_title:eq(0)' ).trigger( 'click' ); <?php endif; ?> } ); </script> <?php } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="chart" data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>" data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>" > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Get the main chart. */ public function get_main_chart() { global $wp_locale; // Get orders and dates in range - we want the SUM of order totals, COUNT of order items, COUNT of orders, and the date. $order_coupon_counts_query = array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'order_coupon_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); $order_discount_amounts_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); if ( ! empty( $this->coupon_codes ) ) { $coupon_code_query = array( 'type' => 'order_item', 'key' => 'order_item_name', 'value' => $this->coupon_codes, 'operator' => 'IN', ); $order_coupon_counts_query['where'][] = $coupon_code_query; $order_discount_amounts_query['where'][] = $coupon_code_query; } $order_coupon_counts = $this->get_order_report_data( $order_coupon_counts_query ); $order_discount_amounts = $this->get_order_report_data( $order_discount_amounts_query ); // Prepare data for report. $order_coupon_counts = $this->prepare_chart_data( $order_coupon_counts, 'post_date', 'order_coupon_count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $order_discount_amounts = $this->prepare_chart_data( $order_discount_amounts, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_coupon_counts' => array_values( $order_coupon_counts ), 'order_discount_amounts' => array_values( $order_discount_amounts ), ) ); ?> <div class="chart-container"> <div class="chart-placeholder main"></div> </div> <script type="text/javascript"> var main_chart; jQuery(function(){ var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) ); var drawGraph = function( highlight ) { var series = [ { label: "<?php echo esc_js( __( 'Number of coupons used', 'woocommerce' ) ); ?>", data: order_data.order_coupon_counts, color: '<?php echo esc_js( $this->chart_colours['coupon_count'] ); ?>', bars: { fillColor: '<?php echo esc_js( $this->chart_colours['coupon_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Discount amount', 'woocommerce' ) ); ?>", data: order_data.order_discount_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['discount_amount'] ); ?>', points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 4, fill: false }, shadowSize: 0, <?php echo $this->get_currency_tooltip(); ?><?php // @codingStandardsIgnoreLine ?> } ]; if ( highlight !== 'undefined' && series[ highlight ] ) { highlight_series = series[ highlight ]; highlight_series.color = '#9c5d90'; if ( highlight_series.bars ) highlight_series.bars.fillColor = '#9c5d90'; if ( highlight_series.lines ) { highlight_series.lines.lineWidth = 5; } } main_chart = jQuery.plot( jQuery('.chart-placeholder.main'), series, { legend: { show: false }, grid: { color: '#aaa', borderColor: 'transparent', borderWidth: 0, hoverable: true }, xaxes: [ { color: '#aaa', position: "bottom", tickColor: 'transparent', mode: "time", timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ), tickLength: 1, minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"], font: { color: "#aaa" } } ], yaxes: [ { min: 0, minTickSize: 1, tickDecimals: 0, color: '#ecf0f1', font: { color: "#aaa" } }, { position: "right", min: 0, tickDecimals: 2, alignTicksWithAxis: 1, color: 'transparent', font: { color: "#aaa" } } ], } ); jQuery('.chart-placeholder').trigger( 'resize' ); } drawGraph(); jQuery('.highlight_series').on( 'mouseenter', function() { drawGraph( jQuery(this).data('series') ); } ).on( 'mouseleave', function() { drawGraph(); } ); }); </script> <?php } } includes/admin/reports/class-wc-report-taxes-by-code.php 0000644 00000020111 15132754524 0017343 0 ustar 00 <?php /** * Taxes by tax code report. * * @package WooCommerce\Admin\Reports */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Taxes_By_Code * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Taxes_By_Code extends WC_Admin_Report { /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { return array(); } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'last_month'; ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="table" > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'last_month'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get the main chart. */ public function get_main_chart() { global $wpdb; $query_data = array( 'order_item_name' => array( 'type' => 'order_item', 'function' => '', 'name' => 'tax_rate', ), 'tax_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'tax_amount', ), 'shipping_tax_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'shipping_tax_amount', ), 'rate_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'rate_id', ), 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_id', ), ); $query_where = array( array( 'key' => 'order_item_type', 'value' => 'tax', 'operator' => '=', ), array( 'key' => 'order_item_name', 'value' => '', 'operator' => '!=', ), ); // We exclude on-hold orders as they are still pending payment. $tax_rows_orders = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'refunded' ), ) ); $tax_rows_partial_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored. ) ); $tax_rows_full_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'refunded' ), ) ); // Merge. $tax_rows = array(); foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) { $key = $tax_row->rate_id; $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_orders' => 0, ); $tax_rows[ $key ]->total_orders += 1; $tax_rows[ $key ]->tax_rate = $tax_row->tax_rate; $tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); $tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); } foreach ( $tax_rows_full_refunds as $tax_row ) { $key = $tax_row->rate_id; $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_orders' => 0, ); $tax_rows[ $key ]->tax_rate = $tax_row->tax_rate; $tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); $tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); } ?> <table class="widefat"> <thead> <tr> <th><?php esc_html_e( 'Tax', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Rate', 'woocommerce' ); ?></th> <th class="total_row"><?php esc_html_e( 'Number of orders', 'woocommerce' ); ?></th> <th class="total_row"><?php esc_html_e( 'Tax amount', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the sum of the "Tax rows" tax amount within your orders.', 'woocommerce' ) ); ?></th> <th class="total_row"><?php esc_html_e( 'Shipping tax amount', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the sum of the "Tax rows" shipping tax amount within your orders.', 'woocommerce' ) ); ?></th> <th class="total_row"><?php esc_html_e( 'Total tax', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the total tax for the rate (shipping tax + product tax).', 'woocommerce' ) ); ?></th> </tr> </thead> <?php if ( ! empty( $tax_rows ) ) : ?> <tbody> <?php foreach ( $tax_rows as $rate_id => $tax_row ) { $rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); ?> <tr> <th scope="row"><?php echo wp_kses_post( apply_filters( 'woocommerce_reports_taxes_tax_rate', $tax_row->tax_rate, $rate_id, $tax_row ) ); ?></th> <td><?php echo wp_kses_post( apply_filters( 'woocommerce_reports_taxes_rate', $rate, $rate_id, $tax_row ) ); ?>%</td> <td class="total_row"><?php echo esc_html( $tax_row->total_orders ); ?></td> <td class="total_row"><?php echo wc_price( $tax_row->tax_amount ); // phpcs:ignore ?></td> <td class="total_row"><?php echo wc_price( $tax_row->shipping_tax_amount ); // phpcs:ignore ?></td> <td class="total_row"><?php echo wc_price( $tax_row->tax_amount + $tax_row->shipping_tax_amount ); // phpcs:ignore ?></td> </tr> <?php } ?> </tbody> <tfoot> <tr> <th scope="row" colspan="3"><?php esc_html_e( 'Total', 'woocommerce' ); ?></th> <th class="total_row"><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) ) ); // phpcs:ignore ?></th> <th class="total_row"><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) ) ) ); // phpcs:ignore ?></th> <th class="total_row"><strong><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) + array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) ) ) ); // phpcs:ignore ?></strong></th> </tr> </tfoot> <?php else : ?> <tbody> <tr> <td><?php esc_html_e( 'No taxes found in this period', 'woocommerce' ); ?></td> </tr> </tbody> <?php endif; ?> </table> <?php } } includes/admin/reports/class-wc-report-out-of-stock.php 0000644 00000003173 15132754524 0017242 0 ustar 00 <?php /** * WC_Report_Out_Of_Stock. * * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Report_Stock' ) ) { require_once dirname( __FILE__ ) . '/class-wc-report-stock.php'; } /** * WC_Report_Out_Of_Stock class. */ class WC_Report_Out_Of_Stock extends WC_Report_Stock { /** * No items found text. */ public function no_items() { esc_html_e( 'No out of stock products found.', 'woocommerce' ); } /** * Get Products matching stock criteria. * * @param int $current_page Current page number. * @param int $per_page How many results to show per page. */ public function get_items( $current_page, $per_page ) { global $wpdb; $this->max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_out_of_stock_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity <= %d ", $stock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } includes/admin/reports/class-wc-report-downloads.php 0000644 00000024720 15132754524 0016703 0 ustar 00 <?php /** * Download report. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Reports * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * WC_Report_Downloads. */ class WC_Report_Downloads extends WP_List_Table { /** * Max items. * * @var int */ protected $max_items; /** * Constructor. */ public function __construct() { parent::__construct( array( 'singular' => 'download', 'plural' => 'downloads', 'ajax' => false, ) ); } /** * Don't need this. * * @param string $position Top or bottom. */ public function display_tablenav( $position ) { if ( 'top' !== $position ) { parent::display_tablenav( $position ); } } /** * Output the report. */ public function output_report() { $this->prepare_items(); // Subtitle for permission if set. if ( ! empty( $_GET['permission_id'] ) ) { // WPCS: input var ok. $permission_id = absint( $_GET['permission_id'] ); // WPCS: input var ok. // Load the permission, order, etc. so we can render more information. $permission = null; $product = null; try { $permission = new WC_Customer_Download( $permission_id ); $product = wc_get_product( $permission->product_id ); } catch ( Exception $e ) { wp_die( sprintf( esc_html__( 'Permission #%d not found.', 'woocommerce' ), esc_html( $permission_id ) ) ); } } echo '<h1>' . esc_html__( 'Customer downloads', 'woocommerce' ); $filters = $this->get_filter_vars(); $filter_list = array(); $filter_names = array( 'product_id' => __( 'Product', 'woocommerce' ), 'download_id' => __( 'File ID', 'woocommerce' ), 'permission_id' => __( 'Permission ID', 'woocommerce' ), 'order_id' => __( 'Order', 'woocommerce' ), 'user_id' => __( 'User', 'woocommerce' ), 'user_ip_address' => __( 'IP address', 'woocommerce' ), ); foreach ( $filters as $key => $value ) { if ( is_null( $value ) ) { continue; } switch ( $key ) { case 'order_id': $order = wc_get_order( $value ); if ( $order ) { $display_value = _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number(); } else { break 2; } break; case 'product_id': $product = wc_get_product( $value ); if ( $product ) { $display_value = $product->get_formatted_name(); } else { break 2; } break; default: $display_value = $value; break; } $filter_list[] = $filter_names[ $key ] . ' ' . $display_value . ' <a href="' . esc_url( remove_query_arg( $key ) ) . '" class="woocommerce-reports-remove-filter">×</a>'; } echo '</h1>'; echo '<div id="active-filters" class="woocommerce-reports-wide"><h2>'; echo esc_html__( 'Active filters', 'woocommerce' ) . ': '; echo $filter_list ? wp_kses_post( implode( ', ', $filter_list ) ) : ''; echo '</h2></div>'; echo '<div id="poststuff" class="woocommerce-reports-wide">'; $this->display(); echo '</div>'; } /** * Get column value. * * @param mixed $item Item being displayed. * @param string $column_name Column name. */ public function column_default( $item, $column_name ) { $permission = null; $product = null; try { $permission = new WC_Customer_Download( $item->permission_id ); $product = wc_get_product( $permission->product_id ); } catch ( Exception $e ) { // Ok to continue rendering other information even if permission and/or product is not found. return; } switch ( $column_name ) { case 'timestamp': echo esc_html( $item->timestamp ); break; case 'product': if ( ! empty( $product ) ) { edit_post_link( esc_html( $product->get_formatted_name() ), '', '', $product->get_id(), 'view-link' ); echo '<div class="row-actions">'; echo '<a href="' . esc_url( add_query_arg( 'product_id', $product->get_id() ) ) . '">' . esc_html__( 'Filter by product', 'woocommerce' ) . '</a>'; echo '</div>'; } break; case 'file': if ( ! empty( $permission ) && ! empty( $product ) ) { // File information. $file = $product->get_file( $permission->get_download_id() ); if ( false === $file ) { echo esc_html__( 'File does not exist', 'woocommerce' ); } else { echo esc_html( $file->get_name() . ' - ' . basename( $file->get_file() ) ); echo '<div class="row-actions">'; echo '<a href="' . esc_url( add_query_arg( 'download_id', $permission->get_download_id() ) ) . '">' . esc_html__( 'Filter by file', 'woocommerce' ) . '</a>'; echo '</div>'; } } break; case 'order': if ( ! empty( $permission ) && ( $order = wc_get_order( $permission->order_id ) ) ) { edit_post_link( esc_html( _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() ), '', '', $permission->order_id, 'view-link' ); echo '<div class="row-actions">'; echo '<a href="' . esc_url( add_query_arg( 'order_id', $order->get_id() ) ) . '">' . esc_html__( 'Filter by order', 'woocommerce' ) . '</a>'; echo '</div>'; } break; case 'user': if ( $item->user_id > 0 ) { $user = get_user_by( 'id', $item->user_id ); if ( ! empty( $user ) ) { echo '<a href="' . esc_url( get_edit_user_link( $item->user_id ) ) . '">' . esc_html( $user->display_name ) . '</a>'; echo '<div class="row-actions">'; echo '<a href="' . esc_url( add_query_arg( 'user_id', $item->user_id ) ) . '">' . esc_html__( 'Filter by user', 'woocommerce' ) . '</a>'; echo '</div>'; } } else { esc_html_e( 'Guest', 'woocommerce' ); } break; case 'user_ip_address': echo esc_html( $item->user_ip_address ); echo '<div class="row-actions">'; echo '<a href="' . esc_url( add_query_arg( 'user_ip_address', $item->user_ip_address ) ) . '">' . esc_html__( 'Filter by IP address', 'woocommerce' ) . '</a>'; echo '</div>'; break; } } /** * Get columns. * * @return array */ public function get_columns() { $columns = array( 'timestamp' => __( 'Timestamp', 'woocommerce' ), 'product' => __( 'Product', 'woocommerce' ), 'file' => __( 'File', 'woocommerce' ), 'order' => __( 'Order', 'woocommerce' ), 'user' => __( 'User', 'woocommerce' ), 'user_ip_address' => __( 'IP address', 'woocommerce' ), ); return $columns; } /** * Prepare download list items. */ public function prepare_items() { $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); $current_page = absint( $this->get_pagenum() ); // Allow filtering per_page value, but ensure it's at least 1. $per_page = max( 1, apply_filters( 'woocommerce_admin_downloads_report_downloads_per_page', 20 ) ); $this->get_items( $current_page, $per_page ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $this->max_items, 'per_page' => $per_page, 'total_pages' => ceil( $this->max_items / $per_page ), ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No customer downloads found.', 'woocommerce' ); } /** * Get filters from querystring. * * @return object */ protected function get_filter_vars() { $product_id = ! empty( $_GET['product_id'] ) ? absint( wp_unslash( $_GET['product_id'] ) ) : null; // WPCS: input var ok. $download_id = ! empty( $_GET['download_id'] ) ? wc_clean( wp_unslash( $_GET['download_id'] ) ) : null; // WPCS: input var ok. $permission_id = ! empty( $_GET['permission_id'] ) ? absint( wp_unslash( $_GET['permission_id'] ) ) : null; // WPCS: input var ok. $order_id = ! empty( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : null; // WPCS: input var ok. $user_id = ! empty( $_GET['user_id'] ) ? absint( wp_unslash( $_GET['user_id'] ) ) : null; // WPCS: input var ok. $user_ip_address = ! empty( $_GET['user_ip_address'] ) ? wc_clean( wp_unslash( $_GET['user_ip_address'] ) ) : null; // WPCS: input var ok. return (object) array( 'product_id' => $product_id, 'download_id' => $download_id, 'permission_id' => $permission_id, 'order_id' => $order_id, 'user_id' => $user_id, 'user_ip_address' => $user_ip_address, ); } /** * Get downloads matching criteria. * * @param int $current_page Current viewed page. * @param int $per_page How many results to show per page. */ public function get_items( $current_page, $per_page ) { global $wpdb; $this->max_items = 0; $this->items = array(); $filters = $this->get_filter_vars(); // Get downloads from database. $table = $wpdb->prefix . WC_Customer_Download_Log_Data_Store::get_table_name(); $query_from = " FROM {$table} as downloads "; if ( ! is_null( $filters->product_id ) || ! is_null( $filters->download_id ) || ! is_null( $filters->order_id ) ) { $query_from .= " LEFT JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions on downloads.permission_id = permissions.permission_id "; } $query_from .= ' WHERE 1=1 '; if ( ! is_null( $filters->product_id ) ) { $query_from .= $wpdb->prepare( ' AND product_id = %d ', $filters->product_id ); } if ( ! is_null( $filters->download_id ) ) { $query_from .= $wpdb->prepare( ' AND download_id = %s ', $filters->download_id ); } if ( ! is_null( $filters->order_id ) ) { $query_from .= $wpdb->prepare( ' AND order_id = %d ', $filters->order_id ); } if ( ! is_null( $filters->permission_id ) ) { $query_from .= $wpdb->prepare( ' AND downloads.permission_id = %d ', $filters->permission_id ); } if ( ! is_null( $filters->user_id ) ) { $query_from .= $wpdb->prepare( ' AND downloads.user_id = %d ', $filters->user_id ); } if ( ! is_null( $filters->user_ip_address ) ) { $query_from .= $wpdb->prepare( ' AND user_ip_address = %s ', $filters->user_ip_address ); } $query_from = apply_filters( 'woocommerce_report_downloads_query_from', $query_from ); $query_order = $wpdb->prepare( 'ORDER BY timestamp DESC LIMIT %d, %d;', ( $current_page - 1 ) * $per_page, $per_page ); $this->items = $wpdb->get_results( "SELECT * {$query_from} {$query_order}" ); // WPCS: cache ok, db call ok, unprepared SQL ok. $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT download_log_id ) {$query_from};" ); // WPCS: cache ok, db call ok, unprepared SQL ok. } } includes/admin/reports/class-wc-report-sales-by-date.php 0000644 00000076512 15132754524 0017351 0 ustar 00 <?php /** * WC_Report_Sales_By_Date * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Sales_By_Date */ class WC_Report_Sales_By_Date extends WC_Admin_Report { /** * Chart colors. * * @var array */ public $chart_colours = array(); /** * The report data. * * @var stdClass */ private $report_data; /** * Get report data. * * @return stdClass */ public function get_report_data() { if ( empty( $this->report_data ) ) { $this->query_report_data(); } return $this->report_data; } /** * Get all data needed for this report and store in the class. */ private function query_report_data() { $this->report_data = new stdClass(); $this->report_data->order_counts = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'count', 'distinct' => true, ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $this->report_data->coupons = (array) $this->get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'function' => '', 'name' => 'order_item_name', ), 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); // All items from orders - even those refunded. $this->report_data->order_items = (array) $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'line_item', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * Get total of fully refunded items. */ $this->report_data->refunded_order_items = absint( $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'line_item', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'refunded' ), ) ) ); /** * Order totals by date. Charts should show GROSS amounts to avoid going -ve. */ $this->report_data->orders = (array) $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping', ), '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_tax', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping_tax', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * If an order is 100% refunded we should look at the parent's totals, but the refunds dates. * We also need to ensure each parent order's values are only counted/summed once. */ $this->report_data->full_refunds = (array) $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_refund', ), '_order_shipping' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_shipping', ), '_order_tax' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_tax', ), '_order_shipping_tax' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_shipping_tax', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => 'posts.post_parent', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'refunded' ), ) ); foreach ( $this->report_data->full_refunds as $key => $order ) { $total_refund = is_numeric( $order->total_refund ) ? $order->total_refund : 0; $total_shipping = is_numeric( $order->total_shipping ) ? $order->total_shipping : 0; $total_tax = is_numeric( $order->total_tax ) ? $order->total_tax : 0; $total_shipping_tax = is_numeric( $order->total_shipping_tax ) ? $order->total_shipping_tax : 0; $this->report_data->full_refunds[ $key ]->net_refund = $total_refund - ( $total_shipping + $total_tax + $total_shipping_tax ); } /** * Partial refunds. This includes line items, shipping and taxes. Not grouped by date. */ $this->report_data->partial_refunds = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'refund_id', ), '_refund_amount' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_refund', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), 'order_item_type' => array( 'type' => 'order_item', 'function' => '', 'name' => 'item_type', 'join_type' => 'LEFT', ), '_order_total' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping', 'join_type' => 'LEFT', ), '_order_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_tax', 'join_type' => 'LEFT', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping_tax', 'join_type' => 'LEFT', ), '_qty' => array( 'type' => 'order_item_meta', 'function' => 'SUM', 'name' => 'order_item_count', 'join_type' => 'LEFT', ), ), 'group_by' => 'refund_id', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ), ) ); foreach ( $this->report_data->partial_refunds as $key => $order ) { $this->report_data->partial_refunds[ $key ]->net_refund = $order->total_refund - ( $order->total_shipping + $order->total_tax + $order->total_shipping_tax ); } /** * Refund lines - all partial refunds on all order types so we can plot full AND partial refunds on the chart. */ $this->report_data->refund_lines = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'refund_id', ), '_refund_amount' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_refund', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), 'order_item_type' => array( 'type' => 'order_item', 'function' => '', 'name' => 'item_type', 'join_type' => 'LEFT', ), '_order_total' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping', 'join_type' => 'LEFT', ), '_order_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_tax', 'join_type' => 'LEFT', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping_tax', 'join_type' => 'LEFT', ), '_qty' => array( 'type' => 'order_item_meta', 'function' => 'SUM', 'name' => 'order_item_count', 'join_type' => 'LEFT', ), ), 'group_by' => 'refund_id', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * Total up refunds. Note: when an order is fully refunded, a refund line will be added. */ $this->report_data->total_tax_refunded = 0; $this->report_data->total_shipping_refunded = 0; $this->report_data->total_shipping_tax_refunded = 0; $this->report_data->total_refunds = 0; $this->report_data->refunded_orders = array_merge( $this->report_data->partial_refunds, $this->report_data->full_refunds ); foreach ( $this->report_data->refunded_orders as $key => $value ) { $this->report_data->total_tax_refunded += floatval( $value->total_tax < 0 ? $value->total_tax * -1 : $value->total_tax ); $this->report_data->total_refunds += floatval( $value->total_refund ); $this->report_data->total_shipping_tax_refunded += floatval( $value->total_shipping_tax < 0 ? $value->total_shipping_tax * -1 : $value->total_shipping_tax ); $this->report_data->total_shipping_refunded += floatval( $value->total_shipping < 0 ? $value->total_shipping * -1 : $value->total_shipping ); // Only applies to parial. if ( isset( $value->order_item_count ) ) { $this->report_data->refunded_order_items += floatval( $value->order_item_count < 0 ? $value->order_item_count * -1 : $value->order_item_count ); } } // Totals from all orders - including those refunded. Subtract refunded amounts. $this->report_data->total_tax = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_tax' ) ) - $this->report_data->total_tax_refunded, 2 ); $this->report_data->total_shipping = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping' ) ) - $this->report_data->total_shipping_refunded, 2 ); $this->report_data->total_shipping_tax = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping_tax' ) ) - $this->report_data->total_shipping_tax_refunded, 2 ); // Total the refunds and sales amounts. Sales subract refunds. Note - total_sales also includes shipping costs. $this->report_data->total_sales = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_sales' ) ) - $this->report_data->total_refunds, 2 ); $this->report_data->net_sales = wc_format_decimal( $this->report_data->total_sales - $this->report_data->total_shipping - max( 0, $this->report_data->total_tax ) - max( 0, $this->report_data->total_shipping_tax ), 2 ); // Calculate average based on net. $this->report_data->average_sales = wc_format_decimal( $this->report_data->net_sales / ( $this->chart_interval + 1 ), 2 ); $this->report_data->average_total_sales = wc_format_decimal( $this->report_data->total_sales / ( $this->chart_interval + 1 ), 2 ); // Total orders and discounts also includes those which have been refunded at some point. $this->report_data->total_coupons = number_format( array_sum( wp_list_pluck( $this->report_data->coupons, 'discount_amount' ) ), 2, '.', '' ); $this->report_data->total_refunded_orders = absint( count( $this->report_data->full_refunds ) ); // Total orders in this period, even if refunded. $this->report_data->total_orders = absint( array_sum( wp_list_pluck( $this->report_data->order_counts, 'count' ) ) ); // Item items ordered in this period, even if refunded. $this->report_data->total_items = absint( array_sum( wp_list_pluck( $this->report_data->order_items, 'order_item_count' ) ) ); // 3rd party filtering of report data $this->report_data = apply_filters( 'woocommerce_admin_report_data', $this->report_data ); } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { $legend = array(); $data = $this->get_report_data(); switch ( $this->chart_groupby ) { case 'day': $average_total_sales_title = sprintf( /* translators: %s: average total sales */ __( '%s average gross daily sales', 'woocommerce' ), '<strong>' . wc_price( $data->average_total_sales ) . '</strong>' ); $average_sales_title = sprintf( /* translators: %s: average sales */ __( '%s average net daily sales', 'woocommerce' ), '<strong>' . wc_price( $data->average_sales ) . '</strong>' ); break; case 'month': default: $average_total_sales_title = sprintf( /* translators: %s: average total sales */ __( '%s average gross monthly sales', 'woocommerce' ), '<strong>' . wc_price( $data->average_total_sales ) . '</strong>' ); $average_sales_title = sprintf( /* translators: %s: average sales */ __( '%s average net monthly sales', 'woocommerce' ), '<strong>' . wc_price( $data->average_sales ) . '</strong>' ); break; } $legend[] = array( 'title' => sprintf( /* translators: %s: total sales */ __( '%s gross sales in this period', 'woocommerce' ), '<strong>' . wc_price( $data->total_sales ) . '</strong>' ), 'placeholder' => __( 'This is the sum of the order totals after any refunds and including shipping and taxes.', 'woocommerce' ), 'color' => $this->chart_colours['sales_amount'], 'highlight_series' => 6, ); if ( $data->average_total_sales > 0 ) { $legend[] = array( 'title' => $average_total_sales_title, 'color' => $this->chart_colours['average'], 'highlight_series' => 2, ); } $legend[] = array( 'title' => sprintf( /* translators: %s: net sales */ __( '%s net sales in this period', 'woocommerce' ), '<strong>' . wc_price( $data->net_sales ) . '</strong>' ), 'placeholder' => __( 'This is the sum of the order totals after any refunds and excluding shipping and taxes.', 'woocommerce' ), 'color' => $this->chart_colours['net_sales_amount'], 'highlight_series' => 7, ); if ( $data->average_sales > 0 ) { $legend[] = array( 'title' => $average_sales_title, 'color' => $this->chart_colours['net_average'], 'highlight_series' => 3, ); } $legend[] = array( 'title' => sprintf( /* translators: %s: total orders */ __( '%s orders placed', 'woocommerce' ), '<strong>' . $data->total_orders . '</strong>' ), 'color' => $this->chart_colours['order_count'], 'highlight_series' => 1, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total items */ __( '%s items purchased', 'woocommerce' ), '<strong>' . $data->total_items . '</strong>' ), 'color' => $this->chart_colours['item_count'], 'highlight_series' => 0, ); $legend[] = array( 'title' => sprintf( /* translators: 1: total refunds 2: total refunded orders 3: refunded items */ _n( '%1$s refunded %2$d order (%3$d item)', '%1$s refunded %2$d orders (%3$d items)', $this->report_data->total_refunded_orders, 'woocommerce' ), '<strong>' . wc_price( $data->total_refunds ) . '</strong>', $this->report_data->total_refunded_orders, $this->report_data->refunded_order_items ), 'color' => $this->chart_colours['refund_amount'], 'highlight_series' => 8, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total shipping */ __( '%s charged for shipping', 'woocommerce' ), '<strong>' . wc_price( $data->total_shipping ) . '</strong>' ), 'color' => $this->chart_colours['shipping_amount'], 'highlight_series' => 5, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total coupons */ __( '%s worth of coupons used', 'woocommerce' ), '<strong>' . wc_price( $data->total_coupons ) . '</strong>' ), 'color' => $this->chart_colours['coupon_amount'], 'highlight_series' => 4, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'sales_amount' => '#b1d4ea', 'net_sales_amount' => '#3498db', 'average' => '#b1d4ea', 'net_average' => '#3498db', 'order_count' => '#dbe1e3', 'item_count' => '#ecf0f1', 'shipping_amount' => '#5cc488', 'coupon_amount' => '#f1c40f', 'refund_amount' => '#e74c3c', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="chart" data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>" data-exclude_series="2" data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>" > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Round our totals correctly. * * @param array|string $amount Chart total. * * @return array|string */ private function round_chart_totals( $amount ) { if ( is_array( $amount ) ) { return array( $amount[0], wc_format_decimal( $amount[1], wc_get_price_decimals() ) ); } else { return wc_format_decimal( $amount, wc_get_price_decimals() ); } } /** * Get the main chart. */ public function get_main_chart() { global $wp_locale; // Prepare data for report. $data = array( 'order_counts' => $this->prepare_chart_data( $this->report_data->order_counts, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'order_item_counts' => $this->prepare_chart_data( $this->report_data->order_items, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'order_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_sales', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'coupon_amounts' => $this->prepare_chart_data( $this->report_data->coupons, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'shipping_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'refund_amounts' => $this->prepare_chart_data( $this->report_data->refund_lines, 'post_date', 'total_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'net_refund_amounts' => $this->prepare_chart_data( $this->report_data->refunded_orders, 'post_date', 'net_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'shipping_tax_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'tax_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'net_order_amounts' => array(), 'gross_order_amounts' => array(), ); foreach ( $data['order_amounts'] as $order_amount_key => $order_amount_value ) { $data['gross_order_amounts'][ $order_amount_key ] = $order_amount_value; $data['gross_order_amounts'][ $order_amount_key ][1] -= $data['refund_amounts'][ $order_amount_key ][1]; $data['net_order_amounts'][ $order_amount_key ] = $order_amount_value; // Subtract the sum of the values from net order amounts. $data['net_order_amounts'][ $order_amount_key ][1] -= $data['net_refund_amounts'][ $order_amount_key ][1] + $data['shipping_amounts'][ $order_amount_key ][1] + $data['shipping_tax_amounts'][ $order_amount_key ][1] + $data['tax_amounts'][ $order_amount_key ][1]; } // 3rd party filtering of report data. $data = apply_filters( 'woocommerce_admin_report_chart_data', $data ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_counts' => array_values( $data['order_counts'] ), 'order_item_counts' => array_values( $data['order_item_counts'] ), 'order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['order_amounts'] ) ), 'gross_order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['gross_order_amounts'] ) ), 'net_order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['net_order_amounts'] ) ), 'shipping_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['shipping_amounts'] ) ), 'coupon_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['coupon_amounts'] ) ), 'refund_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['refund_amounts'] ) ), ) ); ?> <div class="chart-container"> <div class="chart-placeholder main"></div> </div> <script type="text/javascript"> var main_chart; jQuery(function(){ var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) ); var drawGraph = function( highlight ) { var series = [ { label: "<?php echo esc_js( __( 'Number of items sold', 'woocommerce' ) ); ?>", data: order_data.order_item_counts, color: '<?php echo esc_js( $this->chart_colours['item_count'] ); ?>', bars: { fillColor: '<?php echo esc_js( $this->chart_colours['item_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Number of orders', 'woocommerce' ) ); ?>", data: order_data.order_counts, color: '<?php echo esc_js( $this->chart_colours['order_count'] ); ?>', bars: { fillColor: '<?php echo esc_js( $this->chart_colours['order_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Average gross sales amount', 'woocommerce' ) ); ?>", data: [ [ <?php echo esc_js( min( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_total_sales ); ?> ], [ <?php echo esc_js( max( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_total_sales ); ?> ] ], yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['average'] ); ?>', points: { show: false }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Average net sales amount', 'woocommerce' ) ); ?>", data: [ [ <?php echo esc_js( min( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_sales ); ?> ], [ <?php echo esc_js( max( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_sales ); ?> ] ], yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['net_average'] ); ?>', points: { show: false }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, hoverable: false }, { label: "<?php echo esc_js( __( 'Coupon amount', 'woocommerce' ) ); ?>", data: order_data.coupon_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['coupon_amount'] ); ?>', points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> }, { label: "<?php echo esc_js( __( 'Shipping amount', 'woocommerce' ) ); ?>", data: order_data.shipping_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['shipping_amount'] ); ?>', points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>" }, { label: "<?php echo esc_js( __( 'Gross sales amount', 'woocommerce' ) ); ?>", data: order_data.gross_order_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['sales_amount'] ); ?>', points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> }, { label: "<?php echo esc_js( __( 'Net sales amount', 'woocommerce' ) ); ?>", data: order_data.net_order_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['net_sales_amount'] ); ?>', points: { show: true, radius: 6, lineWidth: 4, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 5, fill: false }, shadowSize: 0, <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> }, { label: "<?php echo esc_js( __( 'Refund amount', 'woocommerce' ) ); ?>", data: order_data.refund_amounts, yaxis: 2, color: '<?php echo esc_js( $this->chart_colours['refund_amount'] ); ?>', points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 2, fill: false }, shadowSize: 0, prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>" }, ]; if ( highlight !== 'undefined' && series[ highlight ] ) { highlight_series = series[ highlight ]; highlight_series.color = '#9c5d90'; if ( highlight_series.bars ) { highlight_series.bars.fillColor = '#9c5d90'; } if ( highlight_series.lines ) { highlight_series.lines.lineWidth = 5; } } main_chart = jQuery.plot( jQuery('.chart-placeholder.main'), series, { legend: { show: false }, grid: { color: '#aaa', borderColor: 'transparent', borderWidth: 0, hoverable: true }, xaxes: [ { color: '#aaa', position: "bottom", tickColor: 'transparent', mode: "time", timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ), tickLength: 1, minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"], font: { color: "#aaa" } } ], yaxes: [ { min: 0, minTickSize: 1, tickDecimals: 0, color: '#d4d9dc', font: { color: "#aaa" } }, { position: "right", min: 0, tickDecimals: 2, alignTicksWithAxis: 1, color: 'transparent', font: { color: "#aaa" } } ], } ); jQuery('.chart-placeholder').trigger( 'resize' ); } drawGraph(); jQuery('.highlight_series').on( 'mouseenter', function() { drawGraph( jQuery(this).data('series') ); } ).on( 'mouseleave', function() { drawGraph(); } ); }); </script> <?php } } includes/admin/reports/class-wc-report-customers.php 0000644 00000027071 15132754524 0016737 0 ustar 00 <?php /** * Class WC_Report_Customers file. * * @package WooCommerce\Reports */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Report_Customers * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Customers extends WC_Admin_Report { /** * Chart colors. * * @var array */ public $chart_colours = array(); /** * Customers. * * @var array */ public $customers = array(); /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { $legend = array(); $legend[] = array( /* translators: %s: signups amount */ 'title' => sprintf( __( '%s signups in this period', 'woocommerce' ), '<strong>' . count( $this->customers ) . '</strong>' ), 'color' => $this->chart_colours['signups'], 'highlight_series' => 2, ); return $legend; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); $widgets[] = array( 'title' => '', 'callback' => array( $this, 'customers_vs_guests' ), ); return $widgets; } /** * Output customers vs guests chart. */ public function customers_vs_guests() { $customer_order_totals = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '>', ), ), 'filter_range' => true, ) ); $guest_order_totals = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '=', ), ), 'filter_range' => true, ) ); ?> <div class="chart-container"> <div class="chart-placeholder customers_vs_guests pie-chart" style="height:200px"></div> <ul class="pie-chart-legend"> <li style="border-color: <?php echo esc_attr( $this->chart_colours['customers'] ); ?>"><?php esc_html_e( 'Customer sales', 'woocommerce' ); ?></li> <li style="border-color: <?php echo esc_attr( $this->chart_colours['guests'] ); ?>"><?php esc_html_e( 'Guest sales', 'woocommerce' ); ?></li> </ul> </div> <script type="text/javascript"> jQuery(function(){ jQuery.plot( jQuery('.chart-placeholder.customers_vs_guests'), [ { label: '<?php esc_html_e( 'Customer orders', 'woocommerce' ); ?>', data: "<?php echo esc_html( $customer_order_totals->total_orders ); ?>", color: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>' }, { label: '<?php esc_html_e( 'Guest orders', 'woocommerce' ); ?>', data: "<?php echo esc_html( $guest_order_totals->total_orders ); ?>", color: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>' } ], { grid: { hoverable: true }, series: { pie: { show: true, radius: 1, innerRadius: 0.6, label: { show: false } }, enable_tooltip: true, append_tooltip: "<?php echo esc_html( ' ' . __( 'orders', 'woocommerce' ) ); ?>", }, legend: { show: false } } ); jQuery('.chart-placeholder.customers_vs_guests').trigger( 'resize' ); }); </script> <?php } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'signups' => '#3498db', 'customers' => '#1abc9c', 'guests' => '#8fdece', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $admin_users = new WP_User_Query( array( 'role' => 'administrator', 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', 'fields' => 'ID', ) ); $users_query = new WP_User_Query( apply_filters( 'woocommerce_admin_report_customers_user_query_args', array( 'fields' => array( 'user_registered' ), 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), ) ) ); $this->customers = $users_query->get_results(); foreach ( $this->customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->start_date || strtotime( $customer->user_registered ) > $this->end_date ) { unset( $this->customers[ $key ] ); } } include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv" class="export_csv" data-export="chart" data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>" data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>" > <?php esc_html_e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Output the main chart. */ public function get_main_chart() { global $wp_locale; $customer_orders = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '>', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); $guest_orders = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); $signups = $this->prepare_chart_data( $this->customers, 'user_registered', '', $this->chart_interval, $this->start_date, $this->chart_groupby ); $customer_orders = $this->prepare_chart_data( $customer_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby ); $guest_orders = $this->prepare_chart_data( $guest_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby ); $chart_data = wp_json_encode( array( 'signups' => array_values( $signups ), 'customer_orders' => array_values( $customer_orders ), 'guest_orders' => array_values( $guest_orders ), ) ); ?> <div class="chart-container"> <div class="chart-placeholder main"></div> </div> <script type="text/javascript"> var main_chart; jQuery(function(){ var chart_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) ); var drawGraph = function( highlight ) { var series = [ { label: "<?php echo esc_js( __( 'Customer orders', 'woocommerce' ) ); ?>", data: chart_data.customer_orders, color: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>', bars: { fillColor: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_html( $this->barwidth ); ?> * 0.5, align: 'center' }, shadowSize: 0, enable_tooltip: true, append_tooltip: "<?php echo esc_html( ' ' . __( 'customer orders', 'woocommerce' ) ); ?>", stack: true, }, { label: "<?php echo esc_js( __( 'Guest orders', 'woocommerce' ) ); ?>", data: chart_data.guest_orders, color: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>', bars: { fillColor: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_html( $this->barwidth ); ?> * 0.5, align: 'center' }, shadowSize: 0, enable_tooltip: true, append_tooltip: "<?php echo esc_html( ' ' . __( 'guest orders', 'woocommerce' ) ); ?>", stack: true, }, { label: "<?php echo esc_js( __( 'Signups', 'woocommerce' ) ); ?>", data: chart_data.signups, color: '<?php echo esc_html( $this->chart_colours['signups'] ); ?>', points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 4, fill: false }, shadowSize: 0, enable_tooltip: true, append_tooltip: "<?php echo esc_html( ' ' . __( 'new users', 'woocommerce' ) ); ?>", stack: false }, ]; if ( highlight !== 'undefined' && series[ highlight ] ) { highlight_series = series[ highlight ]; highlight_series.color = '#9c5d90'; if ( highlight_series.bars ) highlight_series.bars.fillColor = '#9c5d90'; if ( highlight_series.lines ) { highlight_series.lines.lineWidth = 5; } } main_chart = jQuery.plot( jQuery('.chart-placeholder.main'), series, { legend: { show: false }, grid: { color: '#aaa', borderColor: 'transparent', borderWidth: 0, hoverable: true }, xaxes: [ { color: '#aaa', position: "bottom", tickColor: 'transparent', mode: "time", timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ), tickLength: 1, minTickSize: [1, "<?php echo esc_html( $this->chart_groupby ); ?>"], tickSize: [1, "<?php echo esc_html( $this->chart_groupby ); ?>"], font: { color: "#aaa" } } ], yaxes: [ { min: 0, minTickSize: 1, tickDecimals: 0, color: '#ecf0f1', font: { color: "#aaa" } } ], } ); jQuery('.chart-placeholder').trigger( 'resize' ); } drawGraph(); jQuery('.highlight_series').on( 'mouseenter', function() { drawGraph( jQuery(this).data('series') ); } ).on( 'mouseleave', function() { drawGraph(); } ); }); </script> <?php } } includes/admin/reports/class-wc-admin-report.php 0000644 00000053622 15132754524 0016004 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * Admin Report. * * Extended by reports to show charts and stats in admin. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Admin_Report { /** * @var array List of transients name that have been updated and need persisting. */ protected static $transients_to_update = array(); /** * @var array The list of transients. */ protected static $cached_results = array(); /** * The chart interval. * * @var int */ public $chart_interval; /** * Group by SQL query. * * @var string */ public $group_by_query; /** * The bar width. * * @var int */ public $barwidth; /** * Group chart item by day or month. * * @var string */ public $chart_groupby; /** * The start date of the report. * * @var int timestamp */ public $start_date; /** * The end date of the report. * * @var int timestamp */ public $end_date; /** * Get report totals such as order totals and discount amounts. * * Data example: * * '_order_total' => array( * 'type' => 'meta', * 'function' => 'SUM', * 'name' => 'total_sales' * ) * * @param array $args * @return mixed depending on query_type */ public function get_order_report_data( $args = array() ) { global $wpdb; $default_args = array( 'data' => array(), 'where' => array(), 'where_meta' => array(), 'query_type' => 'get_row', 'group_by' => '', 'order_by' => '', 'limit' => '', 'filter_range' => false, 'nocache' => false, 'debug' => false, 'order_types' => wc_get_order_types( 'reports' ), 'order_status' => array( 'completed', 'processing', 'on-hold' ), 'parent_order_status' => false, ); $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args ); $args = wp_parse_args( $args, $default_args ); extract( $args ); if ( empty( $data ) ) { return ''; } $order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status ); $query = array(); $select = array(); foreach ( $data as $raw_key => $value ) { $key = sanitize_key( $raw_key ); $distinct = ''; if ( isset( $value['distinct'] ) ) { $distinct = 'DISTINCT'; } switch ( $value['type'] ) { case 'meta': $get_key = "meta_{$key}.meta_value"; break; case 'parent_meta': $get_key = "parent_meta_{$key}.meta_value"; break; case 'post_data': $get_key = "posts.{$key}"; break; case 'order_item_meta': $get_key = "order_item_meta_{$key}.meta_value"; break; case 'order_item': $get_key = "order_items.{$key}"; break; } if ( empty( $get_key ) ) { // Skip to the next foreach iteration else the query will be invalid. continue; } if ( $value['function'] ) { $get = "{$value['function']}({$distinct} {$get_key})"; } else { $get = "{$distinct} {$get_key}"; } $select[] = "{$get} as {$value['name']}"; } $query['select'] = 'SELECT ' . implode( ',', $select ); $query['from'] = "FROM {$wpdb->posts} AS posts"; // Joins $joins = array(); foreach ( ( $data + $where ) as $raw_key => $value ) { $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; $type = isset( $value['type'] ) ? $value['type'] : false; $key = sanitize_key( $raw_key ); switch ( $type ) { case 'meta': $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON ( posts.ID = meta_{$key}.post_id AND meta_{$key}.meta_key = '{$raw_key}' )"; break; case 'parent_meta': $joins[ "parent_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS parent_meta_{$key} ON (posts.post_parent = parent_meta_{$key}.post_id) AND (parent_meta_{$key}.meta_key = '{$raw_key}')"; break; case 'order_item_meta': $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)"; if ( ! empty( $value['order_item_type'] ) ) { $joins['order_items'] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')"; } $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " . "(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " . " AND (order_item_meta_{$key}.meta_key = '{$raw_key}')"; break; case 'order_item': $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; break; } } if ( ! empty( $where_meta ) ) { foreach ( $where_meta as $value ) { if ( ! is_array( $value ) ) { continue; } $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; $type = isset( $value['type'] ) ? $value['type'] : false; $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] ); if ( 'order_item_meta' === $type ) { $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; } else { // If we have a where clause for meta, join the postmeta table $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; } } } if ( ! empty( $parent_order_status ) ) { $joins['parent'] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID"; } $query['join'] = implode( ' ', $joins ); $query['where'] = " WHERE posts.post_type IN ( '" . implode( "','", $order_types ) . "' ) "; if ( ! empty( $order_status ) ) { $query['where'] .= " AND posts.post_status IN ( 'wc-" . implode( "','wc-", $order_status ) . "') "; } if ( ! empty( $parent_order_status ) ) { if ( ! empty( $order_status ) ) { $query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) "; } else { $query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') "; } } if ( $filter_range ) { $query['where'] .= " AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "' AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "' "; } if ( ! empty( $where_meta ) ) { $relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND'; $query['where'] .= ' AND ('; foreach ( $where_meta as $index => $value ) { if ( ! is_array( $value ) ) { continue; } $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] ); if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { if ( is_array( $value['meta_value'] ) ) { $value['meta_value'] = implode( "','", $value['meta_value'] ); } if ( ! empty( $value['meta_value'] ) ) { $where_value = "{$value['operator']} ('{$value['meta_value']}')"; } } else { $where_value = "{$value['operator']} '{$value['meta_value']}'"; } if ( ! empty( $where_value ) ) { if ( $index > 0 ) { $query['where'] .= ' ' . $relation; } if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) { if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( order_item_meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; } else { $query['where'] .= " ( order_item_meta_{$key}.meta_key = '{$value['meta_key']}'"; } $query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )"; } else { if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; } else { $query['where'] .= " ( meta_{$key}.meta_key = '{$value['meta_key']}'"; } $query['where'] .= " AND meta_{$key}.meta_value {$where_value} )"; } } } $query['where'] .= ')'; } if ( ! empty( $where ) ) { foreach ( $where as $value ) { if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { if ( is_array( $value['value'] ) ) { $value['value'] = implode( "','", $value['value'] ); } if ( ! empty( $value['value'] ) ) { $where_value = "{$value['operator']} ('{$value['value']}')"; } } else { $where_value = "{$value['operator']} '{$value['value']}'"; } if ( ! empty( $where_value ) ) { $query['where'] .= " AND {$value['key']} {$where_value}"; } } } if ( $group_by ) { $query['group_by'] = "GROUP BY {$group_by}"; } if ( $order_by ) { $query['order_by'] = "ORDER BY {$order_by}"; } if ( $limit ) { $query['limit'] = "LIMIT {$limit}"; } $query = apply_filters( 'woocommerce_reports_get_order_report_query', $query ); $query = implode( ' ', $query ); if ( $debug ) { echo '<pre>'; wc_print_r( $query ); echo '</pre>'; } if ( $debug || $nocache ) { self::enable_big_selects(); $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); } else { $query_hash = md5( $query_type . $query ); $result = $this->get_cached_query( $query_hash ); if ( $result === null ) { self::enable_big_selects(); $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); } $this->set_cached_query( $query_hash, $result ); } return $result; } /** * Init the static hooks of the class. */ protected static function add_update_transients_hook() { if ( ! has_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ) ) { add_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ); } } /** * Enables big mysql selects for reports, just once for this session. */ protected static function enable_big_selects() { static $big_selects = false; global $wpdb; if ( ! $big_selects ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $big_selects = true; } } /** * Get the cached query result or null if it's not in the cache. * * @param string $query_hash The query hash. * * @return mixed */ protected function get_cached_query( $query_hash ) { $class = strtolower( get_class( $this ) ); if ( ! isset( self::$cached_results[ $class ] ) ) { self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) ); } if ( isset( self::$cached_results[ $class ][ $query_hash ] ) ) { return self::$cached_results[ $class ][ $query_hash ]; } return null; } /** * Set the cached query result. * * @param string $query_hash The query hash. * @param mixed $data The data to cache. */ protected function set_cached_query( $query_hash, $data ) { $class = strtolower( get_class( $this ) ); if ( ! isset( self::$cached_results[ $class ] ) ) { self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) ); } self::add_update_transients_hook(); self::$transients_to_update[ $class ] = $class; self::$cached_results[ $class ][ $query_hash ] = $data; } /** * Function to update the modified transients at the end of the request. */ public static function maybe_update_transients() { foreach ( self::$transients_to_update as $key => $transient_name ) { set_transient( $transient_name, self::$cached_results[ $transient_name ], DAY_IN_SECONDS ); } // Transients have been updated reset the list. self::$transients_to_update = array(); } /** * Put data with post_date's into an array of times. * * @param array $data array of your data * @param string $date_key key for the 'date' field. e.g. 'post_date' * @param string $data_key key for the data you are charting * @param int $interval * @param string $start_date * @param string $group_by * @return array */ public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) { $prepared_data = array(); // Ensure all days (or months) have values in this range. if ( 'day' === $group_by ) { for ( $i = 0; $i <= $interval; $i ++ ) { $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000'; if ( ! isset( $prepared_data[ $time ] ) ) { $prepared_data[ $time ] = array( esc_js( $time ), 0 ); } } } else { $current_yearnum = date( 'Y', $start_date ); $current_monthnum = date( 'm', $start_date ); for ( $i = 0; $i <= $interval; $i ++ ) { $time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000'; if ( ! isset( $prepared_data[ $time ] ) ) { $prepared_data[ $time ] = array( esc_js( $time ), 0 ); } $current_monthnum ++; if ( $current_monthnum > 12 ) { $current_monthnum = 1; $current_yearnum ++; } } } foreach ( $data as $d ) { switch ( $group_by ) { case 'day': $time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000'; break; case 'month': default: $time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000'; break; } if ( ! isset( $prepared_data[ $time ] ) ) { continue; } if ( $data_key ) { $prepared_data[ $time ][1] += $d->$data_key; } else { $prepared_data[ $time ][1] ++; } } return $prepared_data; } /** * Prepares a sparkline to show sales in the last X days. * * @param int $id ID of the product to show. Blank to get all orders. * @param int $days Days of stats to get. * @param string $type Type of sparkline to get. Ignored if ID is not set. * @return string */ public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) { if ( $id ) { $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty'; $data = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), $meta_key => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), 'operator' => '>', ), array( 'key' => 'order_item_meta__product_id.meta_value', 'value' => $id, 'operator' => '=', ), ), 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', 'filter_range' => false, ) ); } else { $data = $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), 'operator' => '>', ), ), 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', 'filter_range' => false, ) ); } $total = 0; foreach ( $data as $d ) { $total += $d->sparkline_value; } if ( 'sales' === $type ) { /* translators: 1: total income 2: days */ $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); } else { /* translators: 1: total items sold 2: days */ $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); } $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) ); return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>'; } /** * Get the current range and calculate the start and end dates. * * @param string $current_range */ public function calculate_current_range( $current_range ) { switch ( $current_range ) { case 'custom': $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( $_GET['start_date'] ) ) ); if ( empty( $_GET['end_date'] ) ) { $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); } else { $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) ); } $interval = 0; $min_date = $this->start_date; while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { $interval ++; } // 3 months max for day view if ( $interval > 3 ) { $this->chart_groupby = 'month'; } else { $this->chart_groupby = 'day'; } break; case 'year': $this->start_date = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'month'; break; case 'last_month': $first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) ); $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) ); $this->chart_groupby = 'day'; break; case 'month': $this->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'day'; break; case '7day': $this->start_date = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'day'; break; } // Group by switch ( $this->chart_groupby ) { case 'day': $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; $this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) ); $this->barwidth = 60 * 60 * 24 * 1000; break; case 'month': $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)'; $this->chart_interval = 0; $min_date = strtotime( date( 'Y-m-01', $this->start_date ) ); while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { $this->chart_interval ++; } $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000; break; } } /** * Return currency tooltip JS based on WooCommerce currency position settings. * * @return string */ public function get_currency_tooltip() { switch ( get_option( 'woocommerce_currency_pos' ) ) { case 'right': $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"'; break; case 'right_space': $currency_tooltip = 'append_tooltip: " ' . get_woocommerce_currency_symbol() . '"'; break; case 'left': $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"'; break; case 'left_space': default: $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . ' "'; break; } return $currency_tooltip; } /** * Get the main chart. */ public function get_main_chart() {} /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { return array(); } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { return array(); } /** * Get an export link if needed. */ public function get_export_button() {} /** * Output the report. */ public function output_report() {} /** * Check nonce for current range. * * @since 3.0.4 * @param string $current_range Current range. */ public function check_current_range_nonce( $current_range ) { if ( 'custom' !== $current_range ) { return; } if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) { // WPCS: input var ok, CSRF ok. wp_die( /* translators: %1$s: open link, %2$s: close link */ sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '<a href="' . esc_url( wp_nonce_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'custom_range', 'wc_reports_nonce' ) ) . '">', '</a>' ), // @codingStandardsIgnoreLine. esc_attr__( 'Confirm navigation', 'woocommerce' ) ); exit; } } } includes/admin/reports/class-wc-report-most-stocked.php 0000644 00000002773 15132754524 0017331 0 ustar 00 <?php /** * WC_Report_Most_Stocked. * * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Report_Stock' ) ) { require_once dirname( __FILE__ ) . '/class-wc-report-stock.php'; } /** * WC_Report_Most_Stocked. */ class WC_Report_Most_Stocked extends WC_Report_Stock { /** * Get Products matching stock criteria. * * @param int $current_page Current page number. * @param int $per_page How many results to show per page. */ public function get_items( $current_page, $per_page ) { global $wpdb; $this->max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_most_stocked_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity > %d ", $stock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY lookup.stock_quantity DESC, id ASC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } includes/admin/reports/class-wc-report-low-in-stock.php 0000644 00000003376 15132754524 0017243 0 ustar 00 <?php /** * WC_Report_Low_In_Stock. * * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Report_Stock' ) ) { require_once dirname( __FILE__ ) . '/class-wc-report-stock.php'; } /** * Low stock report class. */ class WC_Report_Low_In_Stock extends WC_Report_Stock { /** * No items found text. */ public function no_items() { esc_html_e( 'No low in stock products found.', 'woocommerce' ); } /** * Get Products matching stock criteria. * * @param int $current_page Current page number. * @param int $per_page How many results to show per page. */ public function get_items( $current_page, $per_page ) { global $wpdb; $this->max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_low_in_stock_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity <= %d AND lookup.stock_quantity > %d ", $stock, $nostock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } includes/admin/reports/class-wc-report-taxes-by-date.php 0000644 00000022316 15132754524 0017357 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Report_Taxes_By_Date * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Taxes_By_Date extends WC_Admin_Report { /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { return array(); } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month'; ?> <a href="#" download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo date_i18n( 'Y-m-d', current_time( 'timestamp' ) ); ?>.csv" class="export_csv" data-export="table" > <?php _e( 'Export CSV', 'woocommerce' ); ?> </a> <?php } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get the main chart. */ public function get_main_chart() { $query_data = array( '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'tax_amount', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'shipping_tax_amount', ), '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping', ), 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', 'distinct' => true, ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ); // We exlude on-hold orders are they are still pending payment. $tax_rows_orders = $this->get_order_report_data( array( 'data' => $query_data, 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'refunded' ), ) ); $tax_rows_full_refunds = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'distinct' => true, 'function' => '', 'name' => 'ID', ), 'post_parent' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_parent', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'refunded' ), ) ); $tax_rows_partial_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored. ) ); $tax_rows = array(); foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0, ); } foreach ( $tax_rows_orders as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ]->total_orders += $tax_row->total_orders; $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; $tax_rows[ $key ]->total_sales += $tax_row->total_sales; $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; } foreach ( $tax_rows_partial_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; $tax_rows[ $key ]->total_sales += $tax_row->total_sales; $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; } foreach ( $tax_rows_full_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0, ); $parent_order = wc_get_order( $tax_row->post_parent ); if ( $parent_order ) { $tax_rows[ $key ]->tax_amount += $parent_order->get_cart_tax() * -1; $tax_rows[ $key ]->shipping_tax_amount += $parent_order->get_shipping_tax() * -1; $tax_rows[ $key ]->total_sales += $parent_order->get_total() * -1; $tax_rows[ $key ]->total_shipping += $parent_order->get_shipping_total() * -1; } } ?> <table class="widefat"> <thead> <tr> <th><?php _e( 'Period', 'woocommerce' ); ?></th> <th class="total_row"><?php _e( 'Number of orders', 'woocommerce' ); ?></th> <th class="total_row"><?php _e( 'Total sales', 'woocommerce' ); ?> <?php echo wc_help_tip( __( "This is the sum of the 'Order total' field within your orders.", 'woocommerce' ) ); ?></th> <th class="total_row"><?php _e( 'Total shipping', 'woocommerce' ); ?> <?php echo wc_help_tip( __( "This is the sum of the 'Shipping total' field within your orders.", 'woocommerce' ) ); ?></th> <th class="total_row"><?php _e( 'Total tax', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the total tax for the rate (shipping tax + product tax).', 'woocommerce' ) ); ?></th> <th class="total_row"><?php _e( 'Net profit', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Total sales minus shipping and tax.', 'woocommerce' ) ); ?></th> </tr> </thead> <?php if ( ! empty( $tax_rows ) ) : ?> <tbody> <?php foreach ( $tax_rows as $date => $tax_row ) { $gross = $tax_row->total_sales - $tax_row->total_shipping; $total_tax = $tax_row->tax_amount + $tax_row->shipping_tax_amount; ?> <tr> <th scope="row"> <?php echo ( 'month' === $this->chart_groupby ) ? date_i18n( 'F', strtotime( $date . '01' ) ) : date_i18n( get_option( 'date_format' ), strtotime( $date ) ); ?> </th> <td class="total_row"><?php echo $tax_row->total_orders; ?></td> <td class="total_row"><?php echo wc_price( $gross ); ?></td> <td class="total_row"><?php echo wc_price( $tax_row->total_shipping ); ?></td> <td class="total_row"><?php echo wc_price( $total_tax ); ?></td> <td class="total_row"><?php echo wc_price( $gross - $total_tax ); ?></td> </tr> <?php } ?> </tbody> <tfoot> <?php $gross = array_sum( wp_list_pluck( (array) $tax_rows, 'total_sales' ) ) - array_sum( wp_list_pluck( (array) $tax_rows, 'total_shipping' ) ); $total_tax = array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) + array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) ); ?> <tr> <th scope="row"><?php _e( 'Totals', 'woocommerce' ); ?></th> <th class="total_row"><?php echo array_sum( wp_list_pluck( (array) $tax_rows, 'total_orders' ) ); ?></th> <th class="total_row"><?php echo wc_price( $gross ); ?></th> <th class="total_row"><?php echo wc_price( array_sum( wp_list_pluck( (array) $tax_rows, 'total_shipping' ) ) ); ?></th> <th class="total_row"><?php echo wc_price( $total_tax ); ?></th> <th class="total_row"><?php echo wc_price( $gross - $total_tax ); ?></th> </tr> </tfoot> <?php else : ?> <tbody> <tr> <td><?php _e( 'No taxes found in this period', 'woocommerce' ); ?></td> </tr> </tbody> <?php endif; ?> </table> <?php } } includes/admin/reports/class-wc-report-customer-list.php 0000644 00000021062 15132754524 0017517 0 ustar 00 <?php /** * Class WC_Report_Customer_List file. * * @package WooCommerce\Reports */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * WC_Report_Customer_List. * * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Customer_List extends WP_List_Table { /** * Constructor. */ public function __construct() { parent::__construct( array( 'singular' => 'customer', 'plural' => 'customers', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No customers found.', 'woocommerce' ); } /** * Output the report. */ public function output_report() { $this->prepare_items(); echo '<div id="poststuff" class="woocommerce-reports-wide">'; if ( ! empty( $_GET['link_orders'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'link_orders' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $linked = wc_update_new_customer_past_orders( absint( $_GET['link_orders'] ) ); /* translators: single or plural number of orders */ echo '<div class="updated"><p>' . sprintf( esc_html( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) ) . '</p></div>'; } if ( ! empty( $_GET['refresh'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $user_id = absint( $_GET['refresh'] ); $user = get_user_by( 'id', $user_id ); delete_user_meta( $user_id, '_money_spent' ); delete_user_meta( $user_id, '_order_count' ); delete_user_meta( $user_id, '_last_order' ); /* translators: User display name */ echo '<div class="updated"><p>' . sprintf( esc_html__( 'Refreshed stats for %s', 'woocommerce' ), esc_html( $user->display_name ) ) . '</p></div>'; } echo '<form method="post" id="woocommerce_customers">'; $this->search_box( __( 'Search customers', 'woocommerce' ), 'customer_search' ); $this->display(); echo '</form>'; echo '</div>'; } /** * Get column value. * * @param WP_User $user WP User object. * @param string $column_name Column name. * @return string */ public function column_default( $user, $column_name ) { switch ( $column_name ) { case 'customer_name': if ( $user->last_name && $user->first_name ) { return $user->last_name . ', ' . $user->first_name; } else { return '-'; } case 'username': return $user->user_login; case 'location': $state_code = get_user_meta( $user->ID, 'billing_state', true ); $country_code = get_user_meta( $user->ID, 'billing_country', true ); $state = isset( WC()->countries->states[ $country_code ][ $state_code ] ) ? WC()->countries->states[ $country_code ][ $state_code ] : $state_code; $country = isset( WC()->countries->countries[ $country_code ] ) ? WC()->countries->countries[ $country_code ] : $country_code; $value = ''; if ( $state ) { $value .= $state . ', '; } $value .= $country; if ( $value ) { return $value; } else { return '-'; } case 'email': return '<a href="mailto:' . $user->user_email . '">' . $user->user_email . '</a>'; case 'spent': return wc_price( wc_get_customer_total_spent( $user->ID ) ); case 'orders': return wc_get_customer_order_count( $user->ID ); case 'last_order': $orders = wc_get_orders( array( 'limit' => 1, 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), 'customer' => $user->ID, ) ); if ( ! empty( $orders ) ) { $order = $orders[0]; return '<a href="' . admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) . '">' . _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() . '</a> – ' . wc_format_datetime( $order->get_date_created() ); } else { return '-'; } break; case 'wc_actions': ob_start(); ?><p> <?php do_action( 'woocommerce_admin_user_actions_start', $user ); $actions = array(); $actions['refresh'] = array( 'url' => wp_nonce_url( add_query_arg( 'refresh', $user->ID ), 'refresh' ), 'name' => __( 'Refresh stats', 'woocommerce' ), 'action' => 'refresh', ); $actions['edit'] = array( 'url' => admin_url( 'user-edit.php?user_id=' . $user->ID ), 'name' => __( 'Edit', 'woocommerce' ), 'action' => 'edit', ); $actions['view'] = array( 'url' => admin_url( 'edit.php?post_type=shop_order&_customer_user=' . $user->ID ), 'name' => __( 'View orders', 'woocommerce' ), 'action' => 'view', ); $orders = wc_get_orders( array( 'limit' => 1, 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), 'customer' => array( array( 0, $user->user_email ) ), ) ); if ( $orders ) { $actions['link'] = array( 'url' => wp_nonce_url( add_query_arg( 'link_orders', $user->ID ), 'link_orders' ), 'name' => __( 'Link previous orders', 'woocommerce' ), 'action' => 'link', ); } $actions = apply_filters( 'woocommerce_admin_user_actions', $actions, $user ); foreach ( $actions as $action ) { printf( '<a class="button tips %s" href="%s" data-tip="%s">%s</a>', esc_attr( $action['action'] ), esc_url( $action['url'] ), esc_attr( $action['name'] ), esc_attr( $action['name'] ) ); } do_action( 'woocommerce_admin_user_actions_end', $user ); ?> </p> <?php $user_actions = ob_get_contents(); ob_end_clean(); return $user_actions; } return ''; } /** * Get columns. * * @return array */ public function get_columns() { $columns = array( 'customer_name' => __( 'Name (Last, First)', 'woocommerce' ), 'username' => __( 'Username', 'woocommerce' ), 'email' => __( 'Email', 'woocommerce' ), 'location' => __( 'Location', 'woocommerce' ), 'orders' => __( 'Orders', 'woocommerce' ), 'spent' => __( 'Money spent', 'woocommerce' ), 'last_order' => __( 'Last order', 'woocommerce' ), 'wc_actions' => __( 'Actions', 'woocommerce' ), ); return $columns; } /** * Order users by name. * * @param WP_User_Query $query Query that gets passed through. * @return WP_User_Query */ public function order_by_last_name( $query ) { global $wpdb; $s = ! empty( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $query->query_from .= " LEFT JOIN {$wpdb->usermeta} as meta2 ON ({$wpdb->users}.ID = meta2.user_id) "; $query->query_where .= " AND meta2.meta_key = 'last_name' "; $query->query_orderby = ' ORDER BY meta2.meta_value, user_login ASC '; if ( $s ) { $query->query_from .= " LEFT JOIN {$wpdb->usermeta} as meta3 ON ({$wpdb->users}.ID = meta3.user_id)"; $query->query_where .= " AND ( user_login LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR user_nicename LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR meta3.meta_value LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' ) "; $query->query_orderby = ' GROUP BY ID ' . $query->query_orderby; } return $query; } /** * Prepare customer list items. */ public function prepare_items() { $current_page = absint( $this->get_pagenum() ); $per_page = 20; /** * Init column headers. */ $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); add_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** * Get users. */ $admin_users = new WP_User_Query( array( 'role' => 'administrator', 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', 'fields' => 'ID', ) ); $query = new WP_User_Query( apply_filters( 'woocommerce_admin_report_customer_list_user_query_args', array( 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), 'number' => $per_page, 'offset' => ( $current_page - 1 ) * $per_page, ) ) ); $this->items = $query->get_results(); remove_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $query->total_users, 'per_page' => $per_page, 'total_pages' => ceil( $query->total_users / $per_page ), ) ); } } includes/admin/importers/views/html-product-csv-import-form.php 0000644 00000010372 15132754524 0021033 0 ustar 00 <?php /** * Admin View: Product import form * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <form class="wc-progress-form-content woocommerce-importer" enctype="multipart/form-data" method="post"> <header> <h2><?php esc_html_e( 'Import products from a CSV file', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'This tool allows you to import (or merge) product data to your store from a CSV or TXT file.', 'woocommerce' ); ?></p> </header> <section> <table class="form-table woocommerce-importer-options"> <tbody> <tr> <th scope="row"> <label for="upload"> <?php esc_html_e( 'Choose a CSV file from your computer:', 'woocommerce' ); ?> </label> </th> <td> <?php if ( ! empty( $upload_dir['error'] ) ) { ?> <div class="inline error"> <p><?php esc_html_e( 'Before you can upload your import file, you will need to fix the following error:', 'woocommerce' ); ?></p> <p><strong><?php echo esc_html( $upload_dir['error'] ); ?></strong></p> </div> <?php } else { ?> <input type="file" id="upload" name="import" size="25" /> <input type="hidden" name="action" value="save" /> <input type="hidden" name="max_file_size" value="<?php echo esc_attr( $bytes ); ?>" /> <br> <small> <?php printf( /* translators: %s: maximum upload size */ esc_html__( 'Maximum size: %s', 'woocommerce' ), esc_html( $size ) ); ?> </small> <?php } ?> </td> </tr> <tr> <th><label for="woocommerce-importer-update-existing"><?php esc_html_e( 'Update existing products', 'woocommerce' ); ?></label><br/></th> <td> <input type="hidden" name="update_existing" value="0" /> <input type="checkbox" id="woocommerce-importer-update-existing" name="update_existing" value="1" /> <label for="woocommerce-importer-update-existing"><?php esc_html_e( 'Existing products that match by ID or SKU will be updated. Products that do not exist will be skipped.', 'woocommerce' ); ?></label> </td> </tr> <tr class="woocommerce-importer-advanced hidden"> <th> <label for="woocommerce-importer-file-url"><?php esc_html_e( 'Alternatively, enter the path to a CSV file on your server:', 'woocommerce' ); ?></label> </th> <td> <label for="woocommerce-importer-file-url" class="woocommerce-importer-file-url-field-wrapper"> <code><?php echo esc_html( ABSPATH ) . ' '; ?></code><input type="text" id="woocommerce-importer-file-url" name="file_url" /> </label> </td> </tr> <tr class="woocommerce-importer-advanced hidden"> <th><label><?php esc_html_e( 'CSV Delimiter', 'woocommerce' ); ?></label><br/></th> <td><input type="text" name="delimiter" placeholder="," size="2" /></td> </tr> <tr class="woocommerce-importer-advanced hidden"> <th><label><?php esc_html_e( 'Use previous column mapping preferences?', 'woocommerce' ); ?></label><br/></th> <td><input type="checkbox" id="woocommerce-importer-map-preferences" name="map_preferences" value="1" /></td> </tr> </tbody> </table> </section> <script type="text/javascript"> jQuery(function() { jQuery( '.woocommerce-importer-toggle-advanced-options' ).on( 'click', function() { var elements = jQuery( '.woocommerce-importer-advanced' ); if ( elements.is( '.hidden' ) ) { elements.removeClass( 'hidden' ); jQuery( this ).text( jQuery( this ).data( 'hidetext' ) ); } else { elements.addClass( 'hidden' ); jQuery( this ).text( jQuery( this ).data( 'showtext' ) ); } return false; } ); }); </script> <div class="wc-actions"> <a href="#" class="woocommerce-importer-toggle-advanced-options" data-hidetext="<?php esc_attr_e( 'Hide advanced options', 'woocommerce' ); ?>" data-showtext="<?php esc_attr_e( 'Show advanced options', 'woocommerce' ); ?>"><?php esc_html_e( 'Show advanced options', 'woocommerce' ); ?></a> <button type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button> <?php wp_nonce_field( 'woocommerce-csv-importer' ); ?> </div> </form> includes/admin/importers/views/html-csv-import-done.php 0000644 00000005642 15132754524 0017343 0 ustar 00 <?php /** * Admin View: Importer - Done! * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wc-progress-form-content woocommerce-importer"> <section class="woocommerce-importer-done"> <?php $results = array(); if ( 0 < $imported ) { $results[] = sprintf( /* translators: %d: products count */ _n( '%s product imported', '%s products imported', $imported, 'woocommerce' ), '<strong>' . number_format_i18n( $imported ) . '</strong>' ); } if ( 0 < $updated ) { $results[] = sprintf( /* translators: %d: products count */ _n( '%s product updated', '%s products updated', $updated, 'woocommerce' ), '<strong>' . number_format_i18n( $updated ) . '</strong>' ); } if ( 0 < $skipped ) { $results[] = sprintf( /* translators: %d: products count */ _n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ), '<strong>' . number_format_i18n( $skipped ) . '</strong>' ); } if ( 0 < $failed ) { $results [] = sprintf( /* translators: %d: products count */ _n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ), '<strong>' . number_format_i18n( $failed ) . '</strong>' ); } if ( 0 < $failed || 0 < $skipped ) { $results[] = '<a href="#" class="woocommerce-importer-done-view-errors">' . __( 'View import log', 'woocommerce' ) . '</a>'; } if ( ! empty( $file_name ) ) { $results[] = sprintf( /* translators: %s: File name */ __( 'File uploaded: %s', 'woocommerce' ), '<strong>' . $file_name . '</strong>' ); } /* translators: %d: import results */ echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) ); ?> </section> <section class="wc-importer-error-log" style="display:none"> <table class="widefat wc-importer-error-log-table"> <thead> <tr> <th><?php esc_html_e( 'Product', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Reason for failure', 'woocommerce' ); ?></th> </tr> </thead> <tbody> <?php if ( count( $errors ) ) { foreach ( $errors as $error ) { if ( ! is_wp_error( $error ) ) { continue; } $error_data = $error->get_error_data(); ?> <tr> <th><code><?php echo esc_html( $error_data['row'] ); ?></code></th> <td><?php echo esc_html( $error->get_error_message() ); ?></td> </tr> <?php } } ?> </tbody> </table> </section> <script type="text/javascript"> jQuery(function() { jQuery( '.woocommerce-importer-done-view-errors' ).on( 'click', function() { jQuery( '.wc-importer-error-log' ).slideToggle(); return false; } ); } ); </script> <div class="wc-actions"> <a class="button button-primary" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>"><?php esc_html_e( 'View products', 'woocommerce' ); ?></a> </div> </div> includes/admin/importers/views/html-csv-import-mapping.php 0000644 00000005755 15132754524 0020056 0 ustar 00 <?php /** * Admin View: Importer - CSV mapping * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <form class="wc-progress-form-content woocommerce-importer" method="post" action="<?php echo esc_url( $this->get_next_step_link() ); ?>"> <header> <h2><?php esc_html_e( 'Map CSV fields to products', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'Select fields from your CSV file to map against products fields, or to ignore during import.', 'woocommerce' ); ?></p> </header> <section class="wc-importer-mapping-table-wrapper"> <table class="widefat wc-importer-mapping-table"> <thead> <tr> <th><?php esc_html_e( 'Column name', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Map to field', 'woocommerce' ); ?></th> </tr> </thead> <tbody> <?php foreach ( $headers as $index => $name ) : ?> <?php $mapped_value = $mapped_items[ $index ]; ?> <tr> <td class="wc-importer-mapping-table-name"> <?php echo esc_html( $name ); ?> <?php if ( ! empty( $sample[ $index ] ) ) : ?> <span class="description"><?php esc_html_e( 'Sample:', 'woocommerce' ); ?> <code><?php echo esc_html( $sample[ $index ] ); ?></code></span> <?php endif; ?> </td> <td class="wc-importer-mapping-table-field"> <input type="hidden" name="map_from[<?php echo esc_attr( $index ); ?>]" value="<?php echo esc_attr( $name ); ?>" /> <select name="map_to[<?php echo esc_attr( $index ); ?>]"> <option value=""><?php esc_html_e( 'Do not import', 'woocommerce' ); ?></option> <option value="">--------------</option> <?php foreach ( $this->get_mapping_options( $mapped_value ) as $key => $value ) : ?> <?php if ( is_array( $value ) ) : ?> <optgroup label="<?php echo esc_attr( $value['name'] ); ?>"> <?php foreach ( $value['options'] as $sub_key => $sub_value ) : ?> <option value="<?php echo esc_attr( $sub_key ); ?>" <?php selected( $mapped_value, $sub_key ); ?>><?php echo esc_html( $sub_value ); ?></option> <?php endforeach ?> </optgroup> <?php else : ?> <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $mapped_value, $key ); ?>><?php echo esc_html( $value ); ?></option> <?php endif; ?> <?php endforeach ?> </select> </td> </tr> <?php endforeach; ?> </tbody> </table> </section> <div class="wc-actions"> <button type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Run the importer', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Run the importer', 'woocommerce' ); ?></button> <input type="hidden" name="file" value="<?php echo esc_attr( $this->file ); ?>" /> <input type="hidden" name="delimiter" value="<?php echo esc_attr( $this->delimiter ); ?>" /> <input type="hidden" name="update_existing" value="<?php echo (int) $this->update_existing; ?>" /> <?php wp_nonce_field( 'woocommerce-csv-importer' ); ?> </div> </form> includes/admin/importers/views/html-csv-import-header.php 0000644 00000000420 15132754524 0017633 0 ustar 00 <?php /** * Admin View: Header * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wrap woocommerce"> <h1><?php esc_html_e( 'Import Products', 'woocommerce' ); ?></h1> <div class="woocommerce-progress-form-wrapper"> includes/admin/importers/views/html-csv-import-steps.php 0000644 00000001146 15132754524 0017547 0 ustar 00 <?php /** * Admin View: Steps * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <ol class="wc-progress-steps"> <?php foreach ( $this->steps as $step_key => $step ) : ?> <?php $step_class = ''; if ( $step_key === $this->step ) { $step_class = 'active'; } elseif ( array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ) ) { $step_class = 'done'; } ?> <li class="<?php echo esc_attr( $step_class ); ?>"> <?php echo esc_html( $step['name'] ); ?> </li> <?php endforeach; ?> </ol> includes/admin/importers/views/html-csv-import-progress.php 0000644 00000001067 15132754524 0020257 0 ustar 00 <?php /** * Admin View: Importer - CSV import progress * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wc-progress-form-content woocommerce-importer woocommerce-importer__importing"> <header> <span class="spinner is-active"></span> <h2><?php esc_html_e( 'Importing', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'Your products are now being imported...', 'woocommerce' ); ?></p> </header> <section> <progress class="woocommerce-importer-progress" max="100" value="0"></progress> </section> </div> includes/admin/importers/views/html-csv-import-footer.php 0000644 00000000213 15132754524 0017701 0 ustar 00 <?php /** * Admin View: Header * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> </div> </div> includes/admin/importers/class-wc-tax-rate-importer.php 0000644 00000021675 15132754524 0017320 0 ustar 00 <?php /** * Tax importer class file * * @version 2.3.0 * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WP_Importer' ) ) { return; } /** * Tax Rates importer - import tax rates and local tax rates into WooCommerce. * * @package WooCommerce\Admin\Importers * @version 2.3.0 */ class WC_Tax_Rate_Importer extends WP_Importer { /** * The current file id. * * @var int */ public $id; /** * The current file url. * * @var string */ public $file_url; /** * The current import page. * * @var string */ public $import_page; /** * The current delimiter. * * @var string */ public $delimiter; /** * Constructor. */ public function __construct() { $this->import_page = 'woocommerce_tax_rate_csv'; $this->delimiter = empty( $_POST['delimiter'] ) ? ',' : (string) wc_clean( wp_unslash( $_POST['delimiter'] ) ); // WPCS: CSRF ok. } /** * Registered callback function for the WordPress Importer. * * Manages the three separate stages of the CSV import process. */ public function dispatch() { $this->header(); $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; switch ( $step ) { case 0: $this->greet(); break; case 1: check_admin_referer( 'import-upload' ); if ( $this->handle_upload() ) { if ( $this->id ) { $file = get_attached_file( $this->id ); } else { $file = ABSPATH . $this->file_url; } add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); $this->import( $file ); } break; } $this->footer(); } /** * Import is starting. */ private function import_start() { if ( function_exists( 'gc_enable' ) ) { gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound } wc_set_time_limit( 0 ); @ob_flush(); @flush(); @ini_set( 'auto_detect_line_endings', '1' ); } /** * UTF-8 encode the data if `$enc` value isn't UTF-8. * * @param mixed $data Data. * @param string $enc Encoding. * @return string */ public function format_data_from_csv( $data, $enc ) { return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data ); } /** * Import the file if it exists and is valid. * * @param mixed $file File. */ public function import( $file ) { if ( ! is_file( $file ) ) { $this->import_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); } $this->import_start(); $loop = 0; $handle = fopen( $file, 'r' ); if ( false !== $handle ) { $header = fgetcsv( $handle, 0, $this->delimiter ); if ( 10 === count( $header ) ) { $row = fgetcsv( $handle, 0, $this->delimiter ); while ( false !== $row ) { list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row; $tax_rate = array( 'tax_rate_country' => $country, 'tax_rate_state' => $state, 'tax_rate' => $rate, 'tax_rate_name' => $name, 'tax_rate_priority' => $priority, 'tax_rate_compound' => $compound ? 1 : 0, 'tax_rate_shipping' => $shipping ? 1 : 0, 'tax_rate_order' => $loop ++, 'tax_rate_class' => $class, ); $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $postcode ) ); WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $city ) ); $row = fgetcsv( $handle, 0, $this->delimiter ); } } else { $this->import_error( __( 'The CSV is invalid.', 'woocommerce' ) ); } fclose( $handle ); } // Show Result. echo '<div class="updated settings-error"><p>'; printf( /* translators: %s: tax rates count */ esc_html__( 'Import complete - imported %s tax rates.', 'woocommerce' ), '<strong>' . absint( $loop ) . '</strong>' ); echo '</p></div>'; $this->import_end(); } /** * Performs post-import cleanup of files and the cache. */ public function import_end() { echo '<p>' . esc_html__( 'All done!', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=tax' ) ) . '">' . esc_html__( 'View tax rates', 'woocommerce' ) . '</a></p>'; do_action( 'import_end' ); } /** * Handles the CSV upload and initial parsing of the file to prepare for. * displaying author import options. * * @return bool False if error uploading or invalid file, true otherwise */ public function handle_upload() { $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Tax_Rate_Importer::dispatch() if ( empty( $file_url ) ) { $file = wp_import_handle_upload(); if ( isset( $file['error'] ) ) { $this->import_error( $file['error'] ); } if ( ! wc_is_file_valid_csv( $file['file'], false ) ) { // Remove file if not valid. wp_delete_attachment( $file['id'], true ); $this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); } $this->id = absint( $file['id'] ); } elseif ( file_exists( ABSPATH . $file_url ) ) { if ( ! wc_is_file_valid_csv( ABSPATH . $file_url ) ) { $this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); } $this->file_url = esc_attr( $file_url ); } else { $this->import_error(); } return true; } /** * Output header html. */ public function header() { echo '<div class="wrap">'; echo '<h1>' . esc_html__( 'Import tax rates', 'woocommerce' ) . '</h1>'; } /** * Output footer html. */ public function footer() { echo '</div>'; } /** * Output information about the uploading process. */ public function greet() { echo '<div class="narrow">'; echo '<p>' . esc_html__( 'Hi there! Upload a CSV file containing tax rates to import the contents into your shop. Choose a .csv file to upload, then click "Upload file and import".', 'woocommerce' ) . '</p>'; /* translators: 1: Link to tax rates sample file 2: Closing link. */ echo '<p>' . sprintf( esc_html__( 'Your CSV needs to include columns in a specific order. %1$sClick here to download a sample%2$s.', 'woocommerce' ), '<a href="' . esc_url( WC()->plugin_url() ) . '/sample-data/sample_tax_rates.csv">', '</a>' ) . '</p>'; $action = 'admin.php?import=woocommerce_tax_rate_csv&step=1'; $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); $size = size_format( $bytes ); $upload_dir = wp_upload_dir(); if ( ! empty( $upload_dir['error'] ) ) : ?> <div class="error"> <p><?php esc_html_e( 'Before you can upload your import file, you will need to fix the following error:', 'woocommerce' ); ?></p> <p><strong><?php echo esc_html( $upload_dir['error'] ); ?></strong></p> </div> <?php else : ?> <form enctype="multipart/form-data" id="import-upload-form" method="post" action="<?php echo esc_attr( wp_nonce_url( $action, 'import-upload' ) ); ?>"> <table class="form-table"> <tbody> <tr> <th> <label for="upload"><?php esc_html_e( 'Choose a file from your computer:', 'woocommerce' ); ?></label> </th> <td> <input type="file" id="upload" name="import" size="25" /> <input type="hidden" name="action" value="save" /> <input type="hidden" name="max_file_size" value="<?php echo absint( $bytes ); ?>" /> <small> <?php printf( /* translators: %s: maximum upload size */ esc_html__( 'Maximum size: %s', 'woocommerce' ), esc_attr( $size ) ); ?> </small> </td> </tr> <tr> <th> <label for="file_url"><?php esc_html_e( 'OR enter path to file:', 'woocommerce' ); ?></label> </th> <td> <?php echo ' ' . esc_html( ABSPATH ) . ' '; ?><input type="text" id="file_url" name="file_url" size="25" /> </td> </tr> <tr> <th><label><?php esc_html_e( 'Delimiter', 'woocommerce' ); ?></label><br/></th> <td><input type="text" name="delimiter" placeholder="," size="2" /></td> </tr> </tbody> </table> <p class="submit"> <button type="submit" class="button" value="<?php esc_attr_e( 'Upload file and import', 'woocommerce' ); ?>"><?php esc_html_e( 'Upload file and import', 'woocommerce' ); ?></button> </p> </form> <?php endif; echo '</div>'; } /** * Show import error and quit. * * @param string $message Error message. */ private function import_error( $message = '' ) { echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'woocommerce' ) . '</strong><br />'; if ( $message ) { echo esc_html( $message ); } echo '</p>'; $this->footer(); die(); } /** * Added to http_request_timeout filter to force timeout at 60 seconds during import. * * @param int $val Value. * @return int 60 */ public function bump_request_timeout( $val ) { return 60; } } includes/admin/importers/class-wc-product-csv-importer-controller.php 0000644 00000061740 15132754524 0022222 0 ustar 00 <?php /** * Class WC_Product_CSV_Importer_Controller file. * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WP_Importer' ) ) { return; } /** * Product importer controller - handles file upload and forms in admin. * * @package WooCommerce\Admin\Importers * @version 3.1.0 */ class WC_Product_CSV_Importer_Controller { /** * The path to the current file. * * @var string */ protected $file = ''; /** * The current import step. * * @var string */ protected $step = ''; /** * Progress steps. * * @var array */ protected $steps = array(); /** * Errors. * * @var array */ protected $errors = array(); /** * The current delimiter for the file being read. * * @var string */ protected $delimiter = ','; /** * Whether to use previous mapping selections. * * @var bool */ protected $map_preferences = false; /** * Whether to skip existing products. * * @var bool */ protected $update_existing = false; /** * Get importer instance. * * @param string $file File to import. * @param array $args Importer arguments. * @return WC_Product_CSV_Importer */ public static function get_importer( $file, $args = array() ) { $importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' ); $args = apply_filters( 'woocommerce_product_csv_importer_args', $args, $importer_class ); return new $importer_class( $file, $args ); } /** * Check whether a file is a valid CSV file. * * @todo Replace this method with wc_is_file_valid_csv() function. * @param string $file File path. * @param bool $check_path Whether to also check the file is located in a valid location (Default: true). * @return bool */ public static function is_file_valid_csv( $file, $check_path = true ) { if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true ) && false !== stripos( $file, '://' ) ) { return false; } $valid_filetypes = self::get_valid_csv_filetypes(); $filetype = wp_check_filetype( $file, $valid_filetypes ); if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { return true; } return false; } /** * Get all the valid filetypes for a CSV file. * * @return array */ protected static function get_valid_csv_filetypes() { return apply_filters( 'woocommerce_csv_product_import_valid_filetypes', array( 'csv' => 'text/csv', 'txt' => 'text/plain', ) ); } /** * Constructor. */ public function __construct() { $default_steps = array( 'upload' => array( 'name' => __( 'Upload CSV file', 'woocommerce' ), 'view' => array( $this, 'upload_form' ), 'handler' => array( $this, 'upload_form_handler' ), ), 'mapping' => array( 'name' => __( 'Column mapping', 'woocommerce' ), 'view' => array( $this, 'mapping_form' ), 'handler' => '', ), 'import' => array( 'name' => __( 'Import', 'woocommerce' ), 'view' => array( $this, 'import' ), 'handler' => '', ), 'done' => array( 'name' => __( 'Done!', 'woocommerce' ), 'view' => array( $this, 'done' ), 'handler' => '', ), ); $this->steps = apply_filters( 'woocommerce_product_csv_importer_steps', $default_steps ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); $this->file = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : ''; $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ','; $this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false; // phpcs:enable // Import mappings for CSV data. include_once dirname( __FILE__ ) . '/mappings/mappings.php'; if ( $this->map_preferences ) { add_filter( 'woocommerce_csv_product_import_mapped_columns', array( $this, 'auto_map_user_preferences' ), 9999 ); } } /** * Get the URL for the next step's screen. * * @param string $step slug (default: current step). * @return string URL for next step if a next step exists. * Admin URL if it's the last step. * Empty string on failure. */ public function get_next_step_link( $step = '' ) { if ( ! $step ) { $step = $this->step; } $keys = array_keys( $this->steps ); if ( end( $keys ) === $step ) { return admin_url(); } $step_index = array_search( $step, $keys, true ); if ( false === $step_index ) { return ''; } $params = array( 'step' => $keys[ $step_index + 1 ], 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), 'delimiter' => $this->delimiter, 'update_existing' => $this->update_existing, 'map_preferences' => $this->map_preferences, '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. ); return add_query_arg( $params ); } /** * Output header view. */ protected function output_header() { include dirname( __FILE__ ) . '/views/html-csv-import-header.php'; } /** * Output steps view. */ protected function output_steps() { include dirname( __FILE__ ) . '/views/html-csv-import-steps.php'; } /** * Output footer view. */ protected function output_footer() { include dirname( __FILE__ ) . '/views/html-csv-import-footer.php'; } /** * Add error message. * * @param string $message Error message. * @param array $actions List of actions with 'url' and 'label'. */ protected function add_error( $message, $actions = array() ) { $this->errors[] = array( 'message' => $message, 'actions' => $actions, ); } /** * Add error message. */ protected function output_errors() { if ( ! $this->errors ) { return; } foreach ( $this->errors as $error ) { echo '<div class="error inline">'; echo '<p>' . esc_html( $error['message'] ) . '</p>'; if ( ! empty( $error['actions'] ) ) { echo '<p>'; foreach ( $error['actions'] as $action ) { echo '<a class="button button-primary" href="' . esc_url( $action['url'] ) . '">' . esc_html( $action['label'] ) . '</a> '; } echo '</p>'; } echo '</div>'; } } /** * Dispatch current step and show correct view. */ public function dispatch() { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { call_user_func( $this->steps[ $this->step ]['handler'], $this ); } $this->output_header(); $this->output_steps(); $this->output_errors(); call_user_func( $this->steps[ $this->step ]['view'], $this ); $this->output_footer(); } /** * Output information about the uploading process. */ protected function upload_form() { $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); $size = size_format( $bytes ); $upload_dir = wp_upload_dir(); include dirname( __FILE__ ) . '/views/html-product-csv-import-form.php'; } /** * Handle the upload form and store options. */ public function upload_form_handler() { check_admin_referer( 'woocommerce-csv-importer' ); $file = $this->handle_upload(); if ( is_wp_error( $file ) ) { $this->add_error( $file->get_error_message() ); return; } else { $this->file = $file; } wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); exit; } /** * Handles the CSV upload and initial parsing of the file to prepare for * displaying author import options. * * @return string|WP_Error */ public function handle_upload() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler() $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; if ( empty( $file_url ) ) { if ( ! isset( $_FILES['import'] ) ) { return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) { return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); } $overrides = array( 'test_form' => false, 'mimes' => self::get_valid_csv_filetypes(), ); $import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash $upload = wp_handle_upload( $import, $overrides ); if ( isset( $upload['error'] ) ) { return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); } // Construct the object array. $object = array( 'post_title' => basename( $upload['file'] ), 'post_content' => $upload['url'], 'post_mime_type' => $upload['type'], 'guid' => $upload['url'], 'context' => 'import', 'post_status' => 'private', ); // Save the data. $id = wp_insert_attachment( $object, $upload['file'] ); /* * Schedule a cleanup for one day from now in case of failed * import or missing wp_import_cleanup() call. */ wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); return $upload['file']; } elseif ( file_exists( ABSPATH . $file_url ) ) { if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) { return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); } return ABSPATH . $file_url; } // phpcs:enable return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); } /** * Mapping step. */ protected function mapping_form() { check_admin_referer( 'woocommerce-csv-importer' ); $args = array( 'lines' => 1, 'delimiter' => $this->delimiter, ); $importer = self::get_importer( $this->file, $args ); $headers = $importer->get_raw_keys(); $mapped_items = $this->auto_map_columns( $headers ); $sample = current( $importer->get_raw_data() ); if ( empty( $sample ) ) { $this->add_error( __( 'The file is empty or using a different encoding than UTF-8, please try again with a new file.', 'woocommerce' ), array( array( 'url' => admin_url( 'edit.php?post_type=product&page=product_importer' ), 'label' => __( 'Upload a new file', 'woocommerce' ), ), ) ); // Force output the errors in the same page. $this->output_errors(); return; } include_once dirname( __FILE__ ) . '/views/html-csv-import-mapping.php'; } /** * Import the file if it exists and is valid. */ public function import() { // Displaying this page triggers Ajax action to run the import with a valid nonce, // therefore this page needs to be nonce protected as well. check_admin_referer( 'woocommerce-csv-importer' ); if ( ! self::is_file_valid_csv( $this->file ) ) { $this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); $this->output_errors(); return; } if ( ! is_file( $this->file ) ) { $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); $this->output_errors(); return; } if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) { $mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) ); $mapping_to = wc_clean( wp_unslash( $_POST['map_to'] ) ); // Save mapping preferences for future imports. update_user_option( get_current_user_id(), 'woocommerce_product_import_mapping', $mapping_to ); } else { wp_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) ); exit; } wp_localize_script( 'wc-product-import', 'wc_product_import_params', array( 'import_nonce' => wp_create_nonce( 'wc-product-import' ), 'mapping' => array( 'from' => $mapping_from, 'to' => $mapping_to, ), 'file' => $this->file, 'update_existing' => $this->update_existing, 'delimiter' => $this->delimiter, ) ); wp_enqueue_script( 'wc-product-import' ); include_once dirname( __FILE__ ) . '/views/html-csv-import-progress.php'; } /** * Done step. */ protected function done() { check_admin_referer( 'woocommerce-csv-importer' ); $imported = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0; $updated = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0; $failed = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0; $skipped = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0; $file_name = isset( $_GET['file-name'] ) ? sanitize_text_field( wp_unslash( $_GET['file-name'] ) ) : ''; $errors = array_filter( (array) get_user_option( 'product_import_error_log' ) ); include_once dirname( __FILE__ ) . '/views/html-csv-import-done.php'; } /** * Columns to normalize. * * @param array $columns List of columns names and keys. * @return array */ protected function normalize_columns_names( $columns ) { $normalized = array(); foreach ( $columns as $key => $value ) { $normalized[ strtolower( $key ) ] = $value; } return $normalized; } /** * Auto map column names. * * @param array $raw_headers Raw header columns. * @param bool $num_indexes If should use numbers or raw header columns as indexes. * @return array */ protected function auto_map_columns( $raw_headers, $num_indexes = true ) { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); /* * @hooked wc_importer_generic_mappings - 10 * @hooked wc_importer_wordpress_mappings - 10 * @hooked wc_importer_default_english_mappings - 100 */ $default_columns = $this->normalize_columns_names( apply_filters( 'woocommerce_csv_product_import_mapping_default_columns', array( __( 'ID', 'woocommerce' ) => 'id', __( 'Type', 'woocommerce' ) => 'type', __( 'SKU', 'woocommerce' ) => 'sku', __( 'Name', 'woocommerce' ) => 'name', __( 'Published', 'woocommerce' ) => 'published', __( 'Is featured?', 'woocommerce' ) => 'featured', __( 'Visibility in catalog', 'woocommerce' ) => 'catalog_visibility', __( 'Short description', 'woocommerce' ) => 'short_description', __( 'Description', 'woocommerce' ) => 'description', __( 'Date sale price starts', 'woocommerce' ) => 'date_on_sale_from', __( 'Date sale price ends', 'woocommerce' ) => 'date_on_sale_to', __( 'Tax status', 'woocommerce' ) => 'tax_status', __( 'Tax class', 'woocommerce' ) => 'tax_class', __( 'In stock?', 'woocommerce' ) => 'stock_status', __( 'Stock', 'woocommerce' ) => 'stock_quantity', __( 'Backorders allowed?', 'woocommerce' ) => 'backorders', __( 'Low stock amount', 'woocommerce' ) => 'low_stock_amount', __( 'Sold individually?', 'woocommerce' ) => 'sold_individually', /* translators: %s: Weight unit */ sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight', /* translators: %s: Length unit */ sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length', /* translators: %s: Width unit */ sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width', /* translators: %s: Height unit */ sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height', __( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed', __( 'Purchase note', 'woocommerce' ) => 'purchase_note', __( 'Sale price', 'woocommerce' ) => 'sale_price', __( 'Regular price', 'woocommerce' ) => 'regular_price', __( 'Categories', 'woocommerce' ) => 'category_ids', __( 'Tags', 'woocommerce' ) => 'tag_ids', __( 'Shipping class', 'woocommerce' ) => 'shipping_class_id', __( 'Images', 'woocommerce' ) => 'images', __( 'Download limit', 'woocommerce' ) => 'download_limit', __( 'Download expiry days', 'woocommerce' ) => 'download_expiry', __( 'Parent', 'woocommerce' ) => 'parent_id', __( 'Upsells', 'woocommerce' ) => 'upsell_ids', __( 'Cross-sells', 'woocommerce' ) => 'cross_sell_ids', __( 'Grouped products', 'woocommerce' ) => 'grouped_products', __( 'External URL', 'woocommerce' ) => 'product_url', __( 'Button text', 'woocommerce' ) => 'button_text', __( 'Position', 'woocommerce' ) => 'menu_order', ), $raw_headers ) ); $special_columns = $this->get_special_columns( $this->normalize_columns_names( apply_filters( 'woocommerce_csv_product_import_mapping_special_columns', array( /* translators: %d: Attribute number */ __( 'Attribute %d name', 'woocommerce' ) => 'attributes:name', /* translators: %d: Attribute number */ __( 'Attribute %d value(s)', 'woocommerce' ) => 'attributes:value', /* translators: %d: Attribute number */ __( 'Attribute %d visible', 'woocommerce' ) => 'attributes:visible', /* translators: %d: Attribute number */ __( 'Attribute %d global', 'woocommerce' ) => 'attributes:taxonomy', /* translators: %d: Attribute number */ __( 'Attribute %d default', 'woocommerce' ) => 'attributes:default', /* translators: %d: Download number */ __( 'Download %d ID', 'woocommerce' ) => 'downloads:id', /* translators: %d: Download number */ __( 'Download %d name', 'woocommerce' ) => 'downloads:name', /* translators: %d: Download number */ __( 'Download %d URL', 'woocommerce' ) => 'downloads:url', /* translators: %d: Meta number */ __( 'Meta: %s', 'woocommerce' ) => 'meta:', ), $raw_headers ) ) ); $headers = array(); foreach ( $raw_headers as $key => $field ) { $normalized_field = strtolower( $field ); $index = $num_indexes ? $key : $field; $headers[ $index ] = $normalized_field; if ( isset( $default_columns[ $normalized_field ] ) ) { $headers[ $index ] = $default_columns[ $normalized_field ]; } else { foreach ( $special_columns as $regex => $special_key ) { // Don't use the normalized field in the regex since meta might be case-sensitive. if ( preg_match( $regex, $field, $matches ) ) { $headers[ $index ] = $special_key . $matches[1]; break; } } } } return apply_filters( 'woocommerce_csv_product_import_mapped_columns', $headers, $raw_headers ); } /** * Map columns using the user's lastest import mappings. * * @param array $headers Header columns. * @return array */ public function auto_map_user_preferences( $headers ) { $mapping_preferences = get_user_option( 'woocommerce_product_import_mapping' ); if ( ! empty( $mapping_preferences ) && is_array( $mapping_preferences ) ) { return $mapping_preferences; } return $headers; } /** * Sanitize special column name regex. * * @param string $value Raw special column name. * @return string */ protected function sanitize_special_column_name_regex( $value ) { return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i'; } /** * Get special columns. * * @param array $columns Raw special columns. * @return array */ protected function get_special_columns( $columns ) { $formatted = array(); foreach ( $columns as $key => $value ) { $regex = $this->sanitize_special_column_name_regex( $key ); $formatted[ $regex ] = $value; } return $formatted; } /** * Get mapping options. * * @param string $item Item name. * @return array */ protected function get_mapping_options( $item = '' ) { // Get index for special column names. $index = $item; if ( preg_match( '/\d+/', $item, $matches ) ) { $index = $matches[0]; } // Properly format for meta field. $meta = str_replace( 'meta:', '', $item ); // Available options. $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $options = array( 'id' => __( 'ID', 'woocommerce' ), 'type' => __( 'Type', 'woocommerce' ), 'sku' => __( 'SKU', 'woocommerce' ), 'name' => __( 'Name', 'woocommerce' ), 'published' => __( 'Published', 'woocommerce' ), 'featured' => __( 'Is featured?', 'woocommerce' ), 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), 'short_description' => __( 'Short description', 'woocommerce' ), 'description' => __( 'Description', 'woocommerce' ), 'price' => array( 'name' => __( 'Price', 'woocommerce' ), 'options' => array( 'regular_price' => __( 'Regular price', 'woocommerce' ), 'sale_price' => __( 'Sale price', 'woocommerce' ), 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), ), ), 'tax_status' => __( 'Tax status', 'woocommerce' ), 'tax_class' => __( 'Tax class', 'woocommerce' ), 'stock_status' => __( 'In stock?', 'woocommerce' ), 'stock_quantity' => _x( 'Stock', 'Quantity in stock', 'woocommerce' ), 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), /* translators: %s: weight unit */ 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ), 'dimensions' => array( 'name' => __( 'Dimensions', 'woocommerce' ), 'options' => array( /* translators: %s: dimension unit */ 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ), /* translators: %s: dimension unit */ 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ), /* translators: %s: dimension unit */ 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ), ), ), 'category_ids' => __( 'Categories', 'woocommerce' ), 'tag_ids' => __( 'Tags (comma separated)', 'woocommerce' ), 'tag_ids_spaces' => __( 'Tags (space separated)', 'woocommerce' ), 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), 'images' => __( 'Images', 'woocommerce' ), 'parent_id' => __( 'Parent', 'woocommerce' ), 'upsell_ids' => __( 'Upsells', 'woocommerce' ), 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), 'grouped_products' => __( 'Grouped products', 'woocommerce' ), 'external' => array( 'name' => __( 'External product', 'woocommerce' ), 'options' => array( 'product_url' => __( 'External URL', 'woocommerce' ), 'button_text' => __( 'Button text', 'woocommerce' ), ), ), 'downloads' => array( 'name' => __( 'Downloads', 'woocommerce' ), 'options' => array( 'downloads:id' . $index => __( 'Download ID', 'woocommerce' ), 'downloads:name' . $index => __( 'Download name', 'woocommerce' ), 'downloads:url' . $index => __( 'Download URL', 'woocommerce' ), 'download_limit' => __( 'Download limit', 'woocommerce' ), 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), ), ), 'attributes' => array( 'name' => __( 'Attributes', 'woocommerce' ), 'options' => array( 'attributes:name' . $index => __( 'Attribute name', 'woocommerce' ), 'attributes:value' . $index => __( 'Attribute value(s)', 'woocommerce' ), 'attributes:taxonomy' . $index => __( 'Is a global attribute?', 'woocommerce' ), 'attributes:visible' . $index => __( 'Attribute visibility', 'woocommerce' ), 'attributes:default' . $index => __( 'Default attribute', 'woocommerce' ), ), ), 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), 'purchase_note' => __( 'Purchase note', 'woocommerce' ), 'meta:' . $meta => __( 'Import as meta data', 'woocommerce' ), 'menu_order' => __( 'Position', 'woocommerce' ), ); return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item ); } } includes/admin/importers/mappings/mappings.php 0000644 00000000530 15132754524 0015641 0 ustar 00 <?php /** * Load up extra automatic mappings for the CSV importer. * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } require dirname( __FILE__ ) . '/default.php'; require dirname( __FILE__ ) . '/generic.php'; require dirname( __FILE__ ) . '/shopify.php'; require dirname( __FILE__ ) . '/wordpress.php'; includes/admin/importers/mappings/default.php 0000644 00000010420 15132754524 0015446 0 ustar 00 <?php /** * Default mappings * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Importer current locale. * * @since 3.1.0 * @return string */ function wc_importer_current_locale() { $locale = get_locale(); if ( function_exists( 'get_user_locale' ) ) { $locale = get_user_locale(); } return $locale; } /** * Add English mapping placeholders when not using English as current language. * * @since 3.1.0 * @param array $mappings Importer columns mappings. * @return array */ function wc_importer_default_english_mappings( $mappings ) { if ( 'en_US' === wc_importer_current_locale() ) { return $mappings; } $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $new_mappings = array( 'ID' => 'id', 'Type' => 'type', 'SKU' => 'sku', 'Name' => 'name', 'Published' => 'published', 'Is featured?' => 'featured', 'Visibility in catalog' => 'catalog_visibility', 'Short description' => 'short_description', 'Description' => 'description', 'Date sale price starts' => 'date_on_sale_from', 'Date sale price ends' => 'date_on_sale_to', 'Tax status' => 'tax_status', 'Tax class' => 'tax_class', 'In stock?' => 'stock_status', 'Stock' => 'stock_quantity', 'Backorders allowed?' => 'backorders', 'Low stock amount' => 'low_stock_amount', 'Sold individually?' => 'sold_individually', sprintf( 'Weight (%s)', $weight_unit ) => 'weight', sprintf( 'Length (%s)', $dimension_unit ) => 'length', sprintf( 'Width (%s)', $dimension_unit ) => 'width', sprintf( 'Height (%s)', $dimension_unit ) => 'height', 'Allow customer reviews?' => 'reviews_allowed', 'Purchase note' => 'purchase_note', 'Sale price' => 'sale_price', 'Regular price' => 'regular_price', 'Categories' => 'category_ids', 'Tags' => 'tag_ids', 'Shipping class' => 'shipping_class_id', 'Images' => 'images', 'Download limit' => 'download_limit', 'Download expiry days' => 'download_expiry', 'Parent' => 'parent_id', 'Upsells' => 'upsell_ids', 'Cross-sells' => 'cross_sell_ids', 'Grouped products' => 'grouped_products', 'External URL' => 'product_url', 'Button text' => 'button_text', 'Position' => 'menu_order', ); return array_merge( $mappings, $new_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 ); /** * Add English special mapping placeholders when not using English as current language. * * @since 3.1.0 * @param array $mappings Importer columns mappings. * @return array */ function wc_importer_default_special_english_mappings( $mappings ) { if ( 'en_US' === wc_importer_current_locale() ) { return $mappings; } $new_mappings = array( 'Attribute %d name' => 'attributes:name', 'Attribute %d value(s)' => 'attributes:value', 'Attribute %d visible' => 'attributes:visible', 'Attribute %d global' => 'attributes:taxonomy', 'Attribute %d default' => 'attributes:default', 'Download %d ID' => 'downloads:id', 'Download %d name' => 'downloads:name', 'Download %d URL' => 'downloads:url', 'Meta: %s' => 'meta:', ); return array_merge( $mappings, $new_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 ); includes/admin/importers/mappings/generic.php 0000644 00000001461 15132754524 0015443 0 ustar 00 <?php /** * Generic mappings * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Add generic mappings. * * @since 3.1.0 * @param array $mappings Importer columns mappings. * @return array */ function wc_importer_generic_mappings( $mappings ) { $generic_mappings = array( __( 'Title', 'woocommerce' ) => 'name', __( 'Product Title', 'woocommerce' ) => 'name', __( 'Price', 'woocommerce' ) => 'regular_price', __( 'Parent SKU', 'woocommerce' ) => 'parent_id', __( 'Quantity', 'woocommerce' ) => 'stock_quantity', __( 'Menu order', 'woocommerce' ) => 'menu_order', ); return array_merge( $mappings, $generic_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_generic_mappings' ); includes/admin/importers/mappings/shopify.php 0000644 00000005526 15132754524 0015516 0 ustar 00 <?php /** * Shopify mappings * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Add Shopify mappings. * * @since 3.7.0 * @param array $mappings Importer columns mappings. * @param array $raw_headers Raw headers from CSV being imported. * @return array */ function wc_importer_shopify_mappings( $mappings, $raw_headers ) { // Only map if this is looks like a Shopify export. if ( 0 !== count( array_diff( array( 'Title', 'Body (HTML)', 'Type', 'Variant SKU' ), $raw_headers ) ) ) { return $mappings; } $shopify_mappings = array( 'Variant SKU' => 'sku', 'Title' => 'name', 'Body (HTML)' => 'description', 'Quantity' => 'stock_quantity', 'Variant Inventory Qty' => 'stock_quantity', 'Image Src' => 'images', 'Variant Image' => 'images', 'Variant SKU' => 'sku', 'Variant Price' => 'sale_price', 'Variant Compare At Price' => 'regular_price', 'Type' => 'category_ids', 'Tags' => 'tag_ids_spaces', 'Variant Grams' => 'weight', 'Variant Requires Shipping' => 'meta:shopify_requires_shipping', 'Variant Taxable' => 'tax_status', ); return array_merge( $mappings, $shopify_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_shopify_mappings', 10, 2 ); /** * Add special wildcard Shopify mappings. * * @since 3.7.0 * @param array $mappings Importer columns mappings. * @param array $raw_headers Raw headers from CSV being imported. * @return array */ function wc_importer_shopify_special_mappings( $mappings, $raw_headers ) { // Only map if this is looks like a Shopify export. if ( 0 !== count( array_diff( array( 'Title', 'Body (HTML)', 'Type', 'Variant SKU' ), $raw_headers ) ) ) { return $mappings; } $shopify_mappings = array( 'Option%d Name' => 'attributes:name', 'Option%d Value' => 'attributes:value', ); return array_merge( $mappings, $shopify_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_shopify_special_mappings', 10, 2 ); /** * Expand special Shopify columns to WC format. * * @since 3.7.0 * @param array $data Array of data. * @return array Expanded data. */ function wc_importer_shopify_expand_data( $data ) { if ( isset( $data['meta:shopify_requires_shipping'] ) ) { $requires_shipping = wc_string_to_bool( $data['meta:shopify_requires_shipping'] ); if ( ! $requires_shipping ) { if ( isset( $data['type'] ) ) { $data['type'][] = 'virtual'; } else { $data['type'] = array( 'virtual' ); } } unset( $data['meta:shopify_requires_shipping'] ); } return $data; } add_filter( 'woocommerce_product_importer_pre_expand_data', 'wc_importer_shopify_expand_data' ); includes/admin/importers/mappings/wordpress.php 0000644 00000001224 15132754524 0016054 0 ustar 00 <?php /** * WordPress mappings * * @package WooCommerce\Admin\Importers */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Add mappings for WordPress tables. * * @since 3.1.0 * @param array $mappings Importer columns mappings. * @return array */ function wc_importer_wordpress_mappings( $mappings ) { $wp_mappings = array( 'post_id' => 'id', 'post_title' => 'name', 'post_content' => 'description', 'post_excerpt' => 'short_description', 'post_parent' => 'parent_id', ); return array_merge( $mappings, $wp_mappings ); } add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_wordpress_mappings' ); includes/admin/class-wc-admin-taxonomies.php 0000644 00000042524 15132754524 0015160 0 ustar 00 <?php /** * Handles taxonomies in admin * * @class WC_Admin_Taxonomies * @version 2.3.10 * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use Automattic\WooCommerce\Internal\AssignDefaultCategory; /** * WC_Admin_Taxonomies class. */ class WC_Admin_Taxonomies { /** * Class instance. * * @var WC_Admin_Taxonomies instance */ protected static $instance = false; /** * Default category ID. * * @var int */ private $default_cat_id = 0; /** * Get class instance */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. */ public function __construct() { // Default category ID. $this->default_cat_id = get_option( 'default_product_cat', 0 ); // Category/term ordering. add_action( 'create_term', array( $this, 'create_term' ), 5, 3 ); add_action( 'delete_product_cat', function() { wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); } ); // Add form. add_action( 'product_cat_add_form_fields', array( $this, 'add_category_fields' ) ); add_action( 'product_cat_edit_form_fields', array( $this, 'edit_category_fields' ), 10 ); add_action( 'created_term', array( $this, 'save_category_fields' ), 10, 3 ); add_action( 'edit_term', array( $this, 'save_category_fields' ), 10, 3 ); // Add columns. add_filter( 'manage_edit-product_cat_columns', array( $this, 'product_cat_columns' ) ); add_filter( 'manage_product_cat_custom_column', array( $this, 'product_cat_column' ), 10, 3 ); // Add row actions. add_filter( 'product_cat_row_actions', array( $this, 'product_cat_row_actions' ), 10, 2 ); add_filter( 'admin_init', array( $this, 'handle_product_cat_row_actions' ) ); // Taxonomy page descriptions. add_action( 'product_cat_pre_add_form', array( $this, 'product_cat_description' ) ); add_action( 'after-product_cat-table', array( $this, 'product_cat_notes' ) ); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $attribute ) { add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) ); } } // Maintain hierarchy of terms. add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) ); // Admin footer scripts for this product categories admin screen. add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) ); } /** * Order term when created (put in position 0). * * @param mixed $term_id Term ID. * @param mixed $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) { if ( 'product_cat' !== $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) { return; } $meta_name = taxonomy_is_product_attribute( $taxonomy ) ? 'order_' . esc_attr( $taxonomy ) : 'order'; update_term_meta( $term_id, $meta_name, 0 ); } /** * When a term is deleted, delete its meta. * * @deprecated 3.6.0 No longer needed. * @param mixed $term_id Term ID. */ public function delete_term( $term_id ) { wc_deprecated_function( 'delete_term', '3.6' ); } /** * Category thumbnail fields. */ public function add_category_fields() { ?> <div class="form-field term-display-type-wrap"> <label for="display_type"><?php esc_html_e( 'Display type', 'woocommerce' ); ?></label> <select id="display_type" name="display_type" class="postform"> <option value=""><?php esc_html_e( 'Default', 'woocommerce' ); ?></option> <option value="products"><?php esc_html_e( 'Products', 'woocommerce' ); ?></option> <option value="subcategories"><?php esc_html_e( 'Subcategories', 'woocommerce' ); ?></option> <option value="both"><?php esc_html_e( 'Both', 'woocommerce' ); ?></option> </select> </div> <div class="form-field term-thumbnail-wrap"> <label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label> <div id="product_cat_thumbnail" style="float: left; margin-right: 10px;"><img src="<?php echo esc_url( wc_placeholder_img_src() ); ?>" width="60px" height="60px" /></div> <div style="line-height: 60px;"> <input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" /> <button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button> <button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button> </div> <script type="text/javascript"> // Only show the "remove image" button when needed if ( ! jQuery( '#product_cat_thumbnail_id' ).val() ) { jQuery( '.remove_image_button' ).hide(); } // Uploading files var file_frame; jQuery( document ).on( 'click', '.upload_image_button', function( event ) { event.preventDefault(); // If the media frame already exists, reopen it. if ( file_frame ) { file_frame.open(); return; } // Create the media frame. file_frame = wp.media.frames.downloadable_file = wp.media({ title: '<?php esc_html_e( 'Choose an image', 'woocommerce' ); ?>', button: { text: '<?php esc_html_e( 'Use image', 'woocommerce' ); ?>' }, multiple: false }); // When an image is selected, run a callback. file_frame.on( 'select', function() { var attachment = file_frame.state().get( 'selection' ).first().toJSON(); var attachment_thumbnail = attachment.sizes.thumbnail || attachment.sizes.full; jQuery( '#product_cat_thumbnail_id' ).val( attachment.id ); jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', attachment_thumbnail.url ); jQuery( '.remove_image_button' ).show(); }); // Finally, open the modal. file_frame.open(); }); jQuery( document ).on( 'click', '.remove_image_button', function() { jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' ); jQuery( '#product_cat_thumbnail_id' ).val( '' ); jQuery( '.remove_image_button' ).hide(); return false; }); jQuery( document ).ajaxComplete( function( event, request, options ) { if ( request && 4 === request.readyState && 200 === request.status && options.data && 0 <= options.data.indexOf( 'action=add-tag' ) ) { var res = wpAjax.parseAjaxResponse( request.responseXML, 'ajax-response' ); if ( ! res || res.errors ) { return; } // Clear Thumbnail fields on submit jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' ); jQuery( '#product_cat_thumbnail_id' ).val( '' ); jQuery( '.remove_image_button' ).hide(); // Clear Display type field on submit jQuery( '#display_type' ).val( '' ); return; } } ); </script> <div class="clear"></div> </div> <?php } /** * Edit category thumbnail field. * * @param mixed $term Term (category) being edited. */ public function edit_category_fields( $term ) { $display_type = get_term_meta( $term->term_id, 'display_type', true ); $thumbnail_id = absint( get_term_meta( $term->term_id, 'thumbnail_id', true ) ); if ( $thumbnail_id ) { $image = wp_get_attachment_thumb_url( $thumbnail_id ); } else { $image = wc_placeholder_img_src(); } ?> <tr class="form-field term-display-type-wrap"> <th scope="row" valign="top"><label><?php esc_html_e( 'Display type', 'woocommerce' ); ?></label></th> <td> <select id="display_type" name="display_type" class="postform"> <option value="" <?php selected( '', $display_type ); ?>><?php esc_html_e( 'Default', 'woocommerce' ); ?></option> <option value="products" <?php selected( 'products', $display_type ); ?>><?php esc_html_e( 'Products', 'woocommerce' ); ?></option> <option value="subcategories" <?php selected( 'subcategories', $display_type ); ?>><?php esc_html_e( 'Subcategories', 'woocommerce' ); ?></option> <option value="both" <?php selected( 'both', $display_type ); ?>><?php esc_html_e( 'Both', 'woocommerce' ); ?></option> </select> </td> </tr> <tr class="form-field term-thumbnail-wrap"> <th scope="row" valign="top"><label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label></th> <td> <div id="product_cat_thumbnail" style="float: left; margin-right: 10px;"><img src="<?php echo esc_url( $image ); ?>" width="60px" height="60px" /></div> <div style="line-height: 60px;"> <input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" value="<?php echo esc_attr( $thumbnail_id ); ?>" /> <button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button> <button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button> </div> <script type="text/javascript"> // Only show the "remove image" button when needed if ( '0' === jQuery( '#product_cat_thumbnail_id' ).val() ) { jQuery( '.remove_image_button' ).hide(); } // Uploading files var file_frame; jQuery( document ).on( 'click', '.upload_image_button', function( event ) { event.preventDefault(); // If the media frame already exists, reopen it. if ( file_frame ) { file_frame.open(); return; } // Create the media frame. file_frame = wp.media.frames.downloadable_file = wp.media({ title: '<?php esc_html_e( 'Choose an image', 'woocommerce' ); ?>', button: { text: '<?php esc_html_e( 'Use image', 'woocommerce' ); ?>' }, multiple: false }); // When an image is selected, run a callback. file_frame.on( 'select', function() { var attachment = file_frame.state().get( 'selection' ).first().toJSON(); var attachment_thumbnail = attachment.sizes.thumbnail || attachment.sizes.full; jQuery( '#product_cat_thumbnail_id' ).val( attachment.id ); jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', attachment_thumbnail.url ); jQuery( '.remove_image_button' ).show(); }); // Finally, open the modal. file_frame.open(); }); jQuery( document ).on( 'click', '.remove_image_button', function() { jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' ); jQuery( '#product_cat_thumbnail_id' ).val( '' ); jQuery( '.remove_image_button' ).hide(); return false; }); </script> <div class="clear"></div> </td> </tr> <?php } /** * Save category fields * * @param mixed $term_id Term ID being saved. * @param mixed $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ public function save_category_fields( $term_id, $tt_id = '', $taxonomy = '' ) { if ( isset( $_POST['display_type'] ) && 'product_cat' === $taxonomy ) { // WPCS: CSRF ok, input var ok. update_term_meta( $term_id, 'display_type', esc_attr( $_POST['display_type'] ) ); // WPCS: CSRF ok, sanitization ok, input var ok. } if ( isset( $_POST['product_cat_thumbnail_id'] ) && 'product_cat' === $taxonomy ) { // WPCS: CSRF ok, input var ok. update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // WPCS: CSRF ok, input var ok. } } /** * Description for product_cat page to aid users. */ public function product_cat_description() { echo wp_kses( wpautop( __( 'Product categories for your store can be managed here. To change the order of categories on the front-end you can drag and drop to sort them. To see more categories listed click the "screen options" link at the top-right of this page.', 'woocommerce' ) ), array( 'p' => array() ) ); } /** * Add some notes to describe the behavior of the default category. */ public function product_cat_notes() { $category_id = get_option( 'default_product_cat', 0 ); $category = get_term( $category_id, 'product_cat' ); $category_name = ( ! $category || is_wp_error( $category ) ) ? _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) : $category->name; ?> <div class="form-wrap edit-term-notes"> <p> <strong><?php esc_html_e( 'Note:', 'woocommerce' ); ?></strong><br> <?php printf( /* translators: %s: default category */ esc_html__( 'Deleting a category does not delete the products in that category. Instead, products that were only assigned to the deleted category are set to the category %s.', 'woocommerce' ), '<strong>' . esc_html( $category_name ) . '</strong>' ); ?> </p> </div> <?php } /** * Description for shipping class page to aid users. */ public function product_attribute_description() { echo wp_kses( wpautop( __( 'Attribute terms can be assigned to products and variations.<br/><br/><b>Note</b>: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ), array( 'p' => array() ) ); } /** * Thumbnail column added to category admin. * * @param mixed $columns Columns array. * @return array */ public function product_cat_columns( $columns ) { $new_columns = array(); if ( isset( $columns['cb'] ) ) { $new_columns['cb'] = $columns['cb']; unset( $columns['cb'] ); } $new_columns['thumb'] = __( 'Image', 'woocommerce' ); $columns = array_merge( $new_columns, $columns ); $columns['handle'] = ''; return $columns; } /** * Adjust row actions. * * @param array $actions Array of actions. * @param object $term Term object. * @return array */ public function product_cat_row_actions( $actions, $term ) { $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) { $actions['make_default'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', wp_nonce_url( 'edit-tags.php?action=make_default&taxonomy=product_cat&post_type=product&tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ), /* translators: %s: taxonomy term name */ esc_attr( sprintf( __( 'Make “%s” the default category', 'woocommerce' ), $term->name ) ), __( 'Make default', 'woocommerce' ) ); } return $actions; } /** * Handle custom row actions. */ public function handle_product_cat_row_actions() { if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok. $make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok. if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok. update_option( 'default_product_cat', $make_default_id ); } } } /** * Thumbnail column value added to category admin. * * @param string $columns Column HTML output. * @param string $column Column name. * @param int $id Product ID. * * @return string */ public function product_cat_column( $columns, $column, $id ) { if ( 'thumb' === $column ) { // Prepend tooltip for default category. $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); if ( $default_category_id === $id ) { $columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) ); } $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); if ( $thumbnail_id ) { $image = wp_get_attachment_thumb_url( $thumbnail_id ); } else { $image = wc_placeholder_img_src(); } // Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 . $image = str_replace( ' ', '%20', $image ); $columns .= '<img src="' . esc_url( $image ) . '" alt="' . esc_attr__( 'Thumbnail', 'woocommerce' ) . '" class="wp-post-image" height="48" width="48" />'; } if ( 'handle' === $column ) { $columns .= '<input type="hidden" name="term_id" value="' . esc_attr( $id ) . '" />'; } return $columns; } /** * Maintain term hierarchy when editing a product. * * @param array $args Term checklist args. * @return array */ public function disable_checked_ontop( $args ) { if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) { $args['checked_ontop'] = false; } return $args; } /** * Admin footer scripts for the product categories admin screen * * @return void */ public function scripts_at_product_cat_screen_footer() { if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok. return; } // Ensure the tooltip is displayed when the image column is disabled on product categories. wc_enqueue_js( "(function( $ ) { 'use strict'; var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' ); product_cat.find( 'th' ).empty(); product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) ); })( jQuery );" ); } } $wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance(); includes/admin/class-wc-admin-notices.php 0000644 00000045670 15132754524 0014443 0 ustar 00 <?php /** * Display notices in admin * * @package WooCommerce\Admin * @version 3.4.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Admin_Notices Class. */ class WC_Admin_Notices { /** * Stores notices. * * @var array */ private static $notices = array(); /** * Array of notices - name => callback. * * @var array */ private static $core_notices = array( 'update' => 'update_notice', 'template_files' => 'template_file_check_notice', 'legacy_shipping' => 'legacy_shipping_notice', 'no_shipping_methods' => 'no_shipping_methods_notice', 'regenerating_thumbnails' => 'regenerating_thumbnails_notice', 'regenerating_lookup_table' => 'regenerating_lookup_table_notice', 'no_secure_connection' => 'secure_connection_notice', WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice', 'maxmind_license_key' => 'maxmind_missing_license_key_notice', 'redirect_download_method' => 'redirect_download_method_notice', 'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice', 'base_tables_missing' => 'base_tables_missing_notice', ); /** * Constructor. */ public static function init() { self::$notices = get_option( 'woocommerce_admin_notices', array() ); add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) ); add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) ); add_action( 'wp_loaded', array( __CLASS__, 'hide_notices' ) ); // @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation. // That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want // to avoid. if ( ! WC_Install::is_new_install() || ! wc_is_running_from_async_action_scheduler() ) { add_action( 'shutdown', array( __CLASS__, 'store_notices' ) ); } if ( current_user_can( 'manage_woocommerce' ) ) { add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) ); } } /** * Parses query to create nonces when available. * * @deprecated 5.4.0 * @param object $response The WP_REST_Response we're working with. * @return object $response The prepared WP_REST_Response object. */ public static function prepare_note_with_nonce( $response ) { wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '5.4.0' ); return $response; } /** * Store notices to DB */ public static function store_notices() { update_option( 'woocommerce_admin_notices', self::get_notices() ); } /** * Get notices * * @return array */ public static function get_notices() { return self::$notices; } /** * Remove all notices. */ public static function remove_all_notices() { self::$notices = array(); } /** * Reset notices for themes when switched or a new version of WC is installed. */ public static function reset_admin_notices() { if ( ! self::is_ssl() ) { self::add_notice( 'no_secure_connection' ); } if ( ! self::is_uploads_directory_protected() ) { self::add_notice( 'uploads_directory_is_unprotected' ); } self::add_notice( 'template_files' ); self::add_min_version_notice(); self::add_maxmind_missing_license_key_notice(); } /** * Show a notice. * * @param string $name Notice name. * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. */ public static function add_notice( $name, $force_save = false ) { self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) ); if ( $force_save ) { // Adding early save to prevent more race conditions with notices. self::store_notices(); } } /** * Remove a notice from being displayed. * * @param string $name Notice name. * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. */ public static function remove_notice( $name, $force_save = false ) { self::$notices = array_diff( self::get_notices(), array( $name ) ); delete_option( 'woocommerce_admin_notice_' . $name ); if ( $force_save ) { // Adding early save to prevent more race conditions with notices. self::store_notices(); } } /** * See if a notice is being shown. * * @param string $name Notice name. * * @return boolean */ public static function has_notice( $name ) { return in_array( $name, self::get_notices(), true ); } /** * Hide a notice if the GET variable is set. */ public static function hide_notices() { if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { // WPCS: input var ok, CSRF ok. if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wc_notice_nonce'] ) ), 'woocommerce_hide_notices_nonce' ) ) { // WPCS: input var ok, CSRF ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); } $hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok. self::remove_notice( $hide_notice ); update_user_meta( get_current_user_id(), 'dismissed_' . $hide_notice . '_notice', true ); do_action( 'woocommerce_hide_' . $hide_notice . '_notice' ); } } /** * Add notices + styles if needed. */ public static function add_notices() { $notices = self::get_notices(); if ( empty( $notices ) ) { return; } $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $show_on_screens = array( 'dashboard', 'plugins', ); // Notices should only show on WooCommerce screens, the main dashboard, and on the plugins screen. if ( ! in_array( $screen_id, wc_get_screen_ids(), true ) && ! in_array( $screen_id, $show_on_screens, true ) ) { return; } wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), Constants::get_constant( 'WC_VERSION' ) ); // Add RTL support. wp_style_add_data( 'woocommerce-activation', 'rtl', 'replace' ); foreach ( $notices as $notice ) { if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) ); } else { add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) ); } } } /** * Add a custom notice. * * @param string $name Notice name. * @param string $notice_html Notice HTML. */ public static function add_custom_notice( $name, $notice_html ) { self::add_notice( $name ); update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); } /** * Output any stored custom notices. */ public static function output_custom_notices() { $notices = self::get_notices(); if ( ! empty( $notices ) ) { foreach ( $notices as $notice ) { if ( empty( self::$core_notices[ $notice ] ) ) { $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); if ( $notice_html ) { include dirname( __FILE__ ) . '/views/html-notice-custom.php'; } } } } } /** * If we need to update the database, include a message with the DB update button. */ public static function update_notice() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; if ( WC()->is_wc_admin_active() && in_array( $screen_id, wc_get_screen_ids(), true ) ) { return; } if ( WC_Install::needs_db_update() ) { $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok, CSRF ok. include dirname( __FILE__ ) . '/views/html-notice-updating.php'; } else { include dirname( __FILE__ ) . '/views/html-notice-update.php'; } } else { include dirname( __FILE__ ) . '/views/html-notice-updated.php'; } } /** * If we have just installed, show a message with the install pages button. * * @deprecated 4.6.0 */ public static function install_notice() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Show a notice highlighting bad template files. */ public static function template_file_check_notice() { $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' ); $outdated = false; foreach ( $core_templates as $file ) { $theme_file = false; if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . $file; } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { $theme_file = get_template_directory() . '/' . $file; } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; } if ( false !== $theme_file ) { $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); $theme_version = WC_Admin_Status::get_file_version( $theme_file ); if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) { $outdated = true; break; } } } if ( $outdated ) { include dirname( __FILE__ ) . '/views/html-notice-template-check.php'; } else { self::remove_notice( 'template_files' ); } } /** * Show a notice asking users to convert to shipping zones. * * @todo remove in 4.0.0 */ public static function legacy_shipping_notice() { $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); $enabled = false; foreach ( $maybe_load_legacy_methods as $method ) { $options = get_option( 'woocommerce_' . $method . '_settings' ); if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { $enabled = true; } } if ( $enabled ) { include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php'; } else { self::remove_notice( 'template_files' ); } } /** * No shipping methods. */ public static function no_shipping_methods_notice() { if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { // WPCS: input var ok, CSRF ok. $product_count = wp_count_posts( 'product' ); $method_count = wc_get_shipping_method_count(); if ( $product_count->publish > 0 && 0 === $method_count ) { include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php'; } if ( $method_count > 0 ) { self::remove_notice( 'no_shipping_methods' ); } } } /** * Notice shown when regenerating thumbnails background process is running. */ public static function regenerating_thumbnails_notice() { include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php'; } /** * Notice about secure connection. */ public static function secure_connection_notice() { if ( self::is_ssl() || get_user_meta( get_current_user_id(), 'dismissed_no_secure_connection_notice', true ) ) { return; } include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php'; } /** * Notice shown when regenerating thumbnails background process is running. * * @since 3.6.0 */ public static function regenerating_lookup_table_notice() { // See if this is still relevent. if ( ! wc_update_product_lookup_tables_is_running() ) { self::remove_notice( 'regenerating_lookup_table' ); return; } include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php'; } /** * Add notice about minimum PHP and WordPress requirement. * * @since 3.6.5 */ public static function add_min_version_notice() { if ( version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ) || version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ) ) { self::add_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); } } /** * Notice about WordPress and PHP minimum requirements. * * @since 3.6.5 * @return void */ public static function wp_php_min_requirements_notice() { if ( apply_filters( 'woocommerce_hide_php_wp_nag', get_user_meta( get_current_user_id(), 'dismissed_' . WC_PHP_MIN_REQUIREMENTS_NOTICE . '_notice', true ) ) ) { self::remove_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); return; } $old_php = version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ); $old_wp = version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ); // Both PHP and WordPress up to date version => no notice. if ( ! $old_php && ! $old_wp ) { return; } if ( $old_php && $old_wp ) { $msg = sprintf( /* translators: 1: Minimum PHP version 2: Minimum WordPress version */ __( 'Update required: WooCommerce will soon require PHP version %1$s and WordPress version %2$s or newer.', 'woocommerce' ), WC_NOTICE_MIN_PHP_VERSION, WC_NOTICE_MIN_WP_VERSION ); } elseif ( $old_php ) { $msg = sprintf( /* translators: %s: Minimum PHP version */ __( 'Update required: WooCommerce will soon require PHP version %s or newer.', 'woocommerce' ), WC_NOTICE_MIN_PHP_VERSION ); } elseif ( $old_wp ) { $msg = sprintf( /* translators: %s: Minimum WordPress version */ __( 'Update required: WooCommerce will soon require WordPress version %s or newer.', 'woocommerce' ), WC_NOTICE_MIN_WP_VERSION ); } include dirname( __FILE__ ) . '/views/html-notice-wp-php-minimum-requirements.php'; } /** * Add MaxMind missing license key notice. * * @since 3.9.0 */ public static function add_maxmind_missing_license_key_notice() { $default_address = get_option( 'woocommerce_default_customer_address' ); if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) { return; } $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' ); if ( empty( $integration_options['license_key'] ) ) { self::add_notice( 'maxmind_license_key' ); } } /** * Add notice about Redirect-only download method, nudging user to switch to a different method instead. */ public static function add_redirect_download_method_notice() { if ( 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { self::add_notice( 'redirect_download_method' ); } else { self::remove_notice( 'redirect_download_method' ); } } /** * Display MaxMind missing license key notice. * * @since 3.9.0 */ public static function maxmind_missing_license_key_notice() { $user_dismissed_notice = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true ); $filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ); if ( $user_dismissed_notice || $filter_dismissed_notice ) { self::remove_notice( 'maxmind_license_key' ); return; } include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php'; } /** * Notice about Redirect-Only download method. * * @since 4.0 */ public static function redirect_download_method_notice() { if ( apply_filters( 'woocommerce_hide_redirect_method_nag', get_user_meta( get_current_user_id(), 'dismissed_redirect_download_method_notice', true ) ) ) { self::remove_notice( 'redirect_download_method' ); return; } include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php'; } /** * Notice about uploads directory begin unprotected. * * @since 4.2.0 */ public static function uploads_directory_is_unprotected_notice() { if ( get_user_meta( get_current_user_id(), 'dismissed_uploads_directory_is_unprotected_notice', true ) || self::is_uploads_directory_protected() ) { self::remove_notice( 'uploads_directory_is_unprotected' ); return; } include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php'; } /** * Notice about base tables missing. */ public static function base_tables_missing_notice() { $notice_dismissed = apply_filters( 'woocommerce_hide_base_tables_missing_nag', get_user_meta( get_current_user_id(), 'dismissed_base_tables_missing_notice', true ) ); if ( $notice_dismissed ) { self::remove_notice( 'base_tables_missing' ); } include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php'; } /** * Determine if the store is running SSL. * * @return bool Flag SSL enabled. * @since 3.5.1 */ protected static function is_ssl() { $shop_page = wc_get_page_permalink( 'shop' ); return ( is_ssl() && 'https' === substr( $shop_page, 0, 5 ) ); } /** * Wrapper for is_plugin_active. * * @param string $plugin Plugin to check. * @return boolean */ protected static function is_plugin_active( $plugin ) { if ( ! function_exists( 'is_plugin_active' ) ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active( $plugin ); } /** * Simplify Commerce is no longer in core. * * @deprecated 3.6.0 No longer shown. */ public static function simplify_commerce_notice() { wc_deprecated_function( 'WC_Admin_Notices::simplify_commerce_notice', '3.6.0' ); } /** * Show the Theme Check notice. * * @deprecated 3.3.0 No longer shown. */ public static function theme_check_notice() { wc_deprecated_function( 'WC_Admin_Notices::theme_check_notice', '3.3.0' ); } /** * Check if uploads directory is protected. * * @since 4.2.0 * @return bool */ protected static function is_uploads_directory_protected() { $cache_key = '_woocommerce_upload_directory_status'; $status = get_transient( $cache_key ); // Check for cache. if ( false !== $status ) { return 'protected' === $status; } // Get only data from the uploads directory. $uploads = wp_get_upload_dir(); // Check for the "uploads/woocommerce_uploads" directory. $response = wp_safe_remote_get( esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ), array( 'redirection' => 0, ) ); $response_code = intval( wp_remote_retrieve_response_code( $response ) ); $response_content = wp_remote_retrieve_body( $response ); // Check if returns 200 with empty content in case can open an index.html file, // and check for non-200 codes in case the directory is protected. $is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code ); set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', 1 * DAY_IN_SECONDS ); return $is_protected; } } WC_Admin_Notices::init(); includes/admin/class-wc-admin-dashboard.php 0000644 00000051505 15132754524 0014720 0 ustar 00 <?php /** * Admin Dashboard * * @package WooCommerce\Admin * @version 2.1.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) : /** * WC_Admin_Dashboard Class. */ class WC_Admin_Dashboard { /** * Hook in tabs. */ public function __construct() { // Only hook in admin parts if the user has admin access. if ( $this->should_display_widget() ) { // If on network admin, only load the widget that works in that context and skip the rest. if ( is_multisite() && is_network_admin() ) { add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) ); } else { add_action( 'wp_dashboard_setup', array( $this, 'init' ) ); } } } /** * Init dashboard widgets. */ public function init() { // Reviews Widget. if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) { wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce Recent Reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) ); } wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce Status', 'woocommerce' ), array( $this, 'status_widget' ) ); // Network Order Widget. if ( is_multisite() && is_main_site() ) { $this->register_network_order_widget(); } } /** * Register the network order dashboard widget. */ public function register_network_order_widget() { wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) ); } /** * Check to see if we should display the widget. * * @return bool */ private function should_display_widget() { if ( ! WC()->is_wc_admin_active() ) { return false; } $has_permission = current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' ); $task_completed_or_hidden = 'yes' === get_option( 'woocommerce_task_list_complete' ) || 'yes' === get_option( 'woocommerce_task_list_hidden' ); return $task_completed_or_hidden && $has_permission; } /** * Get top seller from DB. * * @return object */ private function get_top_seller() { global $wpdb; $query = array(); $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id FROM {$wpdb->posts} as posts"; $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id "; $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id "; $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id "; $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) "; $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; $query['where'] .= "AND order_item_meta.meta_key = '_qty' "; $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' "; $query['where'] .= "AND posts.post_date >= '" . gmdate( 'Y-m-01', current_time( 'timestamp' ) ) . "' "; $query['where'] .= "AND posts.post_date <= '" . gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' "; $query['groupby'] = 'GROUP BY product_id'; $query['orderby'] = 'ORDER BY qty DESC'; $query['limits'] = 'LIMIT 1'; return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Get sales report data. * * @return object */ private function get_sales_report_data() { include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php'; $sales_by_date = new WC_Report_Sales_By_Date(); $sales_by_date->start_date = strtotime( gmdate( 'Y-m-01', current_time( 'timestamp' ) ) ); $sales_by_date->end_date = strtotime( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) ); $sales_by_date->chart_groupby = 'day'; $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; return $sales_by_date->get_report_data(); } /** * Show status widget. */ public function status_widget() { $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_enqueue_script( 'wc-status-widget', WC()->plugin_url() . '/assets/js/admin/wc-status-widget' . $suffix . '.js', array( 'jquery' ), $version, true ); include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ); $reports = new WC_Admin_Report(); $net_sales_link = 'admin.php?page=wc-reports&tab=orders&range=month'; $top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids='; $report_data = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data(); if ( ! $is_wc_admin_disabled ) { $net_sales_link = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period'; $top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products='; } echo '<ul class="wc_status_list">'; if ( current_user_can( 'view_woocommerce_reports' ) ) { if ( $report_data ) { ?> <li class="sales-this-month"> <a href="<?php echo esc_url( admin_url( $net_sales_link ) ); ?>"> <?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, '' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> <?php printf( /* translators: %s: net sales */ esc_html__( '%s net sales this month', 'woocommerce' ), '<strong>' . wc_price( $report_data->net_sales ) . '</strong>' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <?php } $top_seller = $this->get_top_seller(); if ( $top_seller && $top_seller->qty ) { ?> <li class="best-seller-this-month"> <a href="<?php echo esc_url( admin_url( $top_seller_link . $top_seller->product_id ) ); ?>"> <?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, $top_seller->product_id, 'count' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> <?php printf( /* translators: 1: top seller product title 2: top seller quantity */ esc_html__( '%1$s top seller this month (sold %2$d)', 'woocommerce' ), '<strong>' . get_the_title( $top_seller->product_id ) . '</strong>', $top_seller->qty ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <?php } } $this->status_widget_order_rows(); $this->status_widget_stock_rows( $is_wc_admin_disabled ); do_action( 'woocommerce_after_dashboard_status_widget', $reports ); echo '</ul>'; } /** * Show order data is status widget. */ private function status_widget_order_rows() { if ( ! current_user_can( 'edit_shop_orders' ) ) { return; } $on_hold_count = 0; $processing_count = 0; foreach ( wc_get_order_types( 'order-count' ) as $type ) { $counts = (array) wp_count_posts( $type ); $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0; $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0; } ?> <li class="processing-orders"> <a href="<?php echo esc_url( admin_url( 'edit.php?post_status=wc-processing&post_type=shop_order' ) ); ?>"> <?php printf( /* translators: %s: order count */ _n( '<strong>%s order</strong> awaiting processing', '<strong>%s orders</strong> awaiting processing', $processing_count, 'woocommerce' ), $processing_count ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <li class="on-hold-orders"> <a href="<?php echo esc_url( admin_url( 'edit.php?post_status=wc-on-hold&post_type=shop_order' ) ); ?>"> <?php printf( /* translators: %s: order count */ _n( '<strong>%s order</strong> on-hold', '<strong>%s orders</strong> on-hold', $on_hold_count, 'woocommerce' ), $on_hold_count ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <?php } /** * Show stock data is status widget. * * @param bool $is_wc_admin_disabled if woocommerce admin is disabled. */ private function status_widget_stock_rows( $is_wc_admin_disabled ) { global $wpdb; // Requires lookup table added in 3.6. if ( version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) ) { return; } $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); $transient_name = 'wc_low_stock_count'; $lowinstock_count = get_transient( $transient_name ); if ( false === $lowinstock_count ) { /** * Status widget low in stock count pre query. * * @since 4.3.0 * @param null|string $low_in_stock_count Low in stock count, by default null. * @param int $stock Low stock amount. * @param int $nostock No stock amount */ $lowinstock_count = apply_filters( 'woocommerce_status_widget_low_in_stock_count_pre_query', null, $stock, $nostock ); if ( is_null( $lowinstock_count ) ) { $lowinstock_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} AS lookup INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID WHERE stock_quantity <= %d AND stock_quantity > %d AND posts.post_status = 'publish'", $stock, $nostock ) ); } set_transient( $transient_name, (int) $lowinstock_count, DAY_IN_SECONDS * 30 ); } $transient_name = 'wc_outofstock_count'; $outofstock_count = get_transient( $transient_name ); $lowstock_link = 'admin.php?page=wc-reports&tab=stock&report=low_in_stock'; $outofstock_link = 'admin.php?page=wc-reports&tab=stock&report=out_of_stock'; if ( false === $is_wc_admin_disabled ) { $lowstock_link = 'admin.php?page=wc-admin&type=lowstock&path=%2Fanalytics%2Fstock'; $outofstock_link = 'admin.php?page=wc-admin&type=outofstock&path=%2Fanalytics%2Fstock'; } if ( false === $outofstock_count ) { /** * Status widget out of stock count pre query. * * @since 4.3.0 * @param null|string $outofstock_count Out of stock count, by default null. * @param int $nostock No stock amount */ $outofstock_count = apply_filters( 'woocommerce_status_widget_out_of_stock_count_pre_query', null, $nostock ); if ( is_null( $outofstock_count ) ) { $outofstock_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} AS lookup INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID WHERE stock_quantity <= %d AND posts.post_status = 'publish'", $nostock ) ); } set_transient( $transient_name, (int) $outofstock_count, DAY_IN_SECONDS * 30 ); } ?> <li class="low-in-stock"> <a href="<?php echo esc_url( admin_url( $lowstock_link ) ); ?>"> <?php printf( /* translators: %s: order count */ _n( '<strong>%s product</strong> low in stock', '<strong>%s products</strong> low in stock', $lowinstock_count, 'woocommerce' ), $lowinstock_count ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <li class="out-of-stock"> <a href="<?php echo esc_url( admin_url( $outofstock_link ) ); ?>"> <?php printf( /* translators: %s: order count */ _n( '<strong>%s product</strong> out of stock', '<strong>%s products</strong> out of stock', $outofstock_count, 'woocommerce' ), $outofstock_count ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> </a> </li> <?php } /** * Recent reviews widget. */ public function recent_reviews() { global $wpdb; $query_from = apply_filters( 'woocommerce_report_recent_reviews_query_from', "FROM {$wpdb->comments} comments LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID) WHERE comments.comment_approved = '1' AND comments.comment_type = 'review' AND posts.post_password = '' AND posts.post_type = 'product' AND comments.comment_parent = 0 ORDER BY comments.comment_date_gmt DESC LIMIT 5" ); $comments = $wpdb->get_results( "SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_author_email, comments.comment_ID, comments.comment_content {$query_from};" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); if ( $comments ) { echo '<ul>'; foreach ( $comments as $comment ) { echo '<li>'; echo get_avatar( $comment->comment_author_email, '32' ); $rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ); /* translators: %s: rating */ echo '<div class="star-rating"><span style="width:' . esc_attr( $rating * 20 ) . '%">' . sprintf( esc_html__( '%s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '</span></div>'; /* translators: %s: review author */ echo '<h4 class="meta"><a href="' . esc_url( get_permalink( $comment->ID ) ) . '#comment-' . esc_attr( absint( $comment->comment_ID ) ) . '">' . esc_html( apply_filters( 'woocommerce_admin_dashboard_recent_reviews', $comment->post_title, $comment ) ) . '</a> ' . sprintf( esc_html__( 'reviewed by %s', 'woocommerce' ), esc_html( $comment->comment_author ) ) . '</h4>'; echo '<blockquote>' . wp_kses_data( $comment->comment_content ) . '</blockquote></li>'; } echo '</ul>'; } else { echo '<p>' . esc_html__( 'There are no product reviews yet.', 'woocommerce' ) . '</p>'; } } /** * Network orders widget. */ public function network_orders() { $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_enqueue_style( 'wc-network-orders', WC()->plugin_url() . '/assets/css/network-order-widget.css', array(), $version ); wp_enqueue_script( 'wc-network-orders', WC()->plugin_url() . '/assets/js/admin/network-orders' . $suffix . '.js', array( 'jquery', 'underscore' ), $version, true ); $user = wp_get_current_user(); $blogs = get_blogs_of_user( $user->ID ); $blog_ids = wp_list_pluck( $blogs, 'userblog_id' ); wp_localize_script( 'wc-network-orders', 'woocommerce_network_orders', array( 'nonce' => wp_create_nonce( 'wp_rest' ), 'sites' => array_values( $blog_ids ), 'order_endpoint' => get_rest_url( null, 'wc/v3/orders/network' ), ) ); ?> <div class="post-type-shop_order"> <div id="woocommerce-network-order-table-loading" class="woocommerce-network-order-table-loading is-active"> <p> <span class="spinner is-active"></span> <?php esc_html_e( 'Loading network orders', 'woocommerce' ); ?> </p> </div> <table id="woocommerce-network-order-table" class="woocommerce-network-order-table"> <thead> <tr> <td><?php esc_html_e( 'Order', 'woocommerce' ); ?></td> <td><?php esc_html_e( 'Status', 'woocommerce' ); ?></td> <td><?php esc_html_e( 'Total', 'woocommerce' ); ?></td> </tr> </thead> <tbody id="network-orders-tbody"> </tbody> </table> <div id="woocommerce-network-orders-no-orders" class="woocommerce-network-orders-no-orders"> <p> <?php esc_html_e( 'No orders found', 'woocommerce' ); ?> </p> </div> <?php // @codingStandardsIgnoreStart ?> <script type="text/template" id="network-orders-row-template"> <tr> <td> <a href="<%- edit_url %>" class="order-view"><strong>#<%- number %> <%- customer %></strong></a> <br> <em> <%- blog.blogname %> </em> </td> <td> <mark class="order-status status-<%- status %>"><span><%- status_name %></span></mark> </td> <td> <%= formatted_total %> </td> </tr> </script> <?php // @codingStandardsIgnoreEnd ?> </div> <?php } /** * Gets the sales performance data from the new WooAdmin store. * * @return stdClass|WP_Error|WP_REST_Response */ private function get_wc_admin_performance_data() { $request = new \WP_REST_Request( 'GET', '/wc-analytics/reports/performance-indicators' ); $start_date = gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp' ) ); $end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) ); $request->set_query_params( array( 'before' => $end_date, 'after' => $start_date, 'stats' => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold', ) ); $response = rest_do_request( $request ); if ( is_wp_error( $response ) ) { return $response; } if ( 200 !== $response->get_status() ) { return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) ); } $report_keys = array( 'net_revenue' => 'net_sales', ); $performance_data = new stdClass(); foreach ( $response->get_data() as $indicator ) { if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) { $key = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart']; $performance_data->$key = $indicator['value']; } } return $performance_data; } /** * Overwrites the original sparkline to use the new reports data if WooAdmin is enabled. * Prepares a sparkline to show sales in the last X days. * * @param WC_Admin_Report $reports old class for getting reports. * @param bool $is_wc_admin_disabled If WC Admin is disabled or not. * @param int $id ID of the product to show. Blank to get all orders. * @param string $type Type of sparkline to get. Ignored if ID is not set. * @return string */ private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) { $days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ); if ( $is_wc_admin_disabled ) { return $reports->sales_sparkline( $id, $days, $type ); } $sales_endpoint = '/wc-analytics/reports/revenue/stats'; $start_date = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) ); $end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) ); $meta_key = 'net_revenue'; $params = array( 'order' => 'asc', 'interval' => 'day', 'per_page' => 100, 'before' => $end_date, 'after' => $start_date, ); if ( $id ) { $sales_endpoint = '/wc-analytics/reports/products/stats'; $meta_key = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold'; $params['products'] = $id; } $request = new \WP_REST_Request( 'GET', $sales_endpoint ); $params['fields'] = array( $meta_key ); $request->set_query_params( $params ); $response = rest_do_request( $request ); if ( is_wp_error( $response ) ) { return $response; } $resp_data = $response->get_data(); $data = $resp_data['intervals']; $sparkline_data = array(); $total = 0; foreach ( $data as $d ) { $total += $d['subtotals']->$meta_key; array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) ); } if ( 'sales' === $type ) { /* translators: 1: total income 2: days */ $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); } else { /* translators: 1: total items sold 2: days */ $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); } return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>'; } } endif; return new WC_Admin_Dashboard(); includes/admin/class-wc-admin-dashboard-setup.php 0000644 00000013111 15132754524 0016045 0 ustar 00 <?php /** * Admin Dashboard - Setup * * @package WooCommerce\Admin * @version 2.1.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) : /** * WC_Admin_Dashboard_Setup Class. */ class WC_Admin_Dashboard_Setup { /** * List of tasks. * * @var array */ private $tasks = array( 'store_details' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&path=%2Fsetup-wizard', ), 'products' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&task=products', ), 'woocommerce-payments' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&path=%2Fpayments%2Fconnect', ), 'payments' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&task=payments', ), 'tax' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&task=tax', ), 'shipping' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&task=shipping', ), 'appearance' => array( 'completed' => false, 'button_link' => 'admin.php?page=wc-admin&task=appearance', ), ); /** * # of completed tasks. * * @var int */ private $completed_tasks_count = 0; /** * WC_Admin_Dashboard_Setup constructor. */ public function __construct() { if ( $this->should_display_widget() ) { $this->populate_general_tasks(); $this->populate_payment_tasks(); $this->completed_tasks_count = $this->get_completed_tasks_count(); add_meta_box( 'wc_admin_dashboard_setup', __( 'WooCommerce Setup', 'woocommerce' ), array( $this, 'render' ), 'dashboard', 'normal', 'high' ); } } /** * Render meta box output. */ public function render() { $version = Constants::get_constant( 'WC_VERSION' ); wp_enqueue_style( 'wc-dashboard-setup', WC()->plugin_url() . '/assets/css/dashboard-setup.css', array(), $version ); $task = $this->get_next_task(); if ( ! $task ) { return; } $button_link = $task['button_link']; $completed_tasks_count = $this->completed_tasks_count; $tasks_count = count( $this->tasks ); // Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2). $progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100; $circle_r = 6.5; $circle_dashoffset = ( ( 100 - $progress_percentage ) / 100 ) * ( pi() * ( $circle_r * 2 ) ); include __DIR__ . '/views/html-admin-dashboard-setup.php'; } /** * Populate tasks from the database. */ private function populate_general_tasks() { $tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() ); foreach ( $tasks as $task ) { if ( isset( $this->tasks[ $task ] ) ) { $this->tasks[ $task ]['completed'] = true; $this->tasks[ $task ]['button_link'] = wc_admin_url( $this->tasks[ $task ]['button_link'] ); } } } /** * Getter for $tasks * * @return array */ public function get_tasks() { return $this->tasks; } /** * Return # of completed tasks */ public function get_completed_tasks_count() { $completed_tasks = array_filter( $this->tasks, function( $task ) { return $task['completed']; } ); return count( $completed_tasks ); } /** * Get the next task. * * @return array|null */ private function get_next_task() { foreach ( $this->get_tasks() as $task ) { if ( false === $task['completed'] ) { return $task; } } return null; } /** * Check to see if we should display the widget * * @return bool */ private function should_display_widget() { return WC()->is_wc_admin_active() && 'yes' !== get_option( 'woocommerce_task_list_complete' ) && 'yes' !== get_option( 'woocommerce_task_list_hidden' ); } /** * Populate payment tasks's visibility and completion */ private function populate_payment_tasks() { $is_woo_payment_installed = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' ); $country = explode( ':', get_option( 'woocommerce_default_country', 'US:CA' ) )[0]; // woocommerce-payments requires its plugin activated and country must be US. if ( ! $is_woo_payment_installed || 'US' !== $country ) { unset( $this->tasks['woocommerce-payments'] ); } // payments can't be used when woocommerce-payments exists and country is US. if ( $is_woo_payment_installed && 'US' === $country ) { unset( $this->tasks['payments'] ); } if ( isset( $this->tasks['payments'] ) ) { $gateways = WC()->payment_gateways->get_available_payment_gateways(); $enabled_gateways = array_filter( $gateways, function ( $gateway ) { return 'yes' === $gateway->enabled; } ); $this->tasks['payments']['completed'] = ! empty( $enabled_gateways ); } if ( isset( $this->tasks['woocommerce-payments'] ) ) { $wc_pay_is_connected = false; if ( class_exists( '\WC_Payments' ) ) { $wc_payments_gateway = \WC_Payments::get_gateway(); $wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' ) ? $wc_payments_gateway->is_connected() : false; } $this->tasks['woocommerce-payments']['completed'] = $wc_pay_is_connected; } } } endif; return new WC_Admin_Dashboard_Setup(); includes/admin/class-wc-admin-meta-boxes.php 0000644 00000022655 15132754524 0015041 0 ustar 00 <?php /** * WooCommerce Meta Boxes * * Sets up the write panels used by products and orders (custom post types). * * @package WooCommerce\Admin\Meta Boxes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Admin_Meta_Boxes. */ class WC_Admin_Meta_Boxes { /** * Is meta boxes saved once? * * @var boolean */ private static $saved_meta_boxes = false; /** * Meta box error messages. * * @var array */ public static $meta_box_errors = array(); /** * Constructor. */ public function __construct() { add_action( 'add_meta_boxes', array( $this, 'remove_meta_boxes' ), 10 ); add_action( 'add_meta_boxes', array( $this, 'rename_meta_boxes' ), 20 ); add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 30 ); add_action( 'save_post', array( $this, 'save_meta_boxes' ), 1, 2 ); /** * Save Order Meta Boxes. * * In order: * Save the order items. * Save the order totals. * Save the order downloads. * Save order data - also updates status and sends out admin emails if needed. Last to show latest data. * Save actions - sends out other emails. Last to show latest data. */ add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 ); // Save Product Meta Boxes. add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Data::save', 10, 2 ); add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Images::save', 20, 2 ); // Save Coupon Meta Boxes. add_action( 'woocommerce_process_shop_coupon_meta', 'WC_Meta_Box_Coupon_Data::save', 10, 2 ); // Save Rating Meta Boxes. add_filter( 'wp_update_comment_data', 'WC_Meta_Box_Product_Reviews::save', 1 ); // Error handling (for showing errors from meta boxes on next page load). add_action( 'admin_notices', array( $this, 'output_errors' ) ); add_action( 'shutdown', array( $this, 'save_errors' ) ); add_filter( 'theme_product_templates', array( $this, 'remove_block_templates' ), 10, 1 ); } /** * Add an error message. * * @param string $text Error to add. */ public static function add_error( $text ) { self::$meta_box_errors[] = $text; } /** * Save errors to an option. */ public function save_errors() { update_option( 'woocommerce_meta_box_errors', self::$meta_box_errors ); } /** * Show any stored error messages. */ public function output_errors() { $errors = array_filter( (array) get_option( 'woocommerce_meta_box_errors' ) ); if ( ! empty( $errors ) ) { echo '<div id="woocommerce_errors" class="error notice is-dismissible">'; foreach ( $errors as $error ) { echo '<p>' . wp_kses_post( $error ) . '</p>'; } echo '</div>'; // Clear. delete_option( 'woocommerce_meta_box_errors' ); } } /** * Add WC Meta boxes. */ public function add_meta_boxes() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; // Products. add_meta_box( 'postexcerpt', __( 'Product short description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' ); add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); // Orders. foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { $order_type_object = get_post_type_object( $type ); /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Data::output', $type, 'normal', 'high' ); add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $type, 'normal', 'high' ); /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Notes::output', $type, 'side', 'default' ); add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $type, 'normal', 'default' ); /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Actions::output', $type, 'side', 'high' ); } // Coupons. add_meta_box( 'woocommerce-coupon-data', __( 'Coupon data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' ); // Comment rating. if ( 'comment' === $screen_id && isset( $_GET['c'] ) && metadata_exists( 'comment', wc_clean( wp_unslash( $_GET['c'] ) ), 'rating' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Product_Reviews::output', 'comment', 'normal', 'high' ); } } /** * Remove bloat. */ public function remove_meta_boxes() { remove_meta_box( 'postexcerpt', 'product', 'normal' ); remove_meta_box( 'product_shipping_classdiv', 'product', 'side' ); remove_meta_box( 'commentsdiv', 'product', 'normal' ); remove_meta_box( 'commentstatusdiv', 'product', 'side' ); remove_meta_box( 'commentstatusdiv', 'product', 'normal' ); remove_meta_box( 'woothemes-settings', 'shop_coupon', 'normal' ); remove_meta_box( 'commentstatusdiv', 'shop_coupon', 'normal' ); remove_meta_box( 'slugdiv', 'shop_coupon', 'normal' ); foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { remove_meta_box( 'commentsdiv', $type, 'normal' ); remove_meta_box( 'woothemes-settings', $type, 'normal' ); remove_meta_box( 'commentstatusdiv', $type, 'normal' ); remove_meta_box( 'slugdiv', $type, 'normal' ); remove_meta_box( 'submitdiv', $type, 'side' ); } } /** * Rename core meta boxes. */ public function rename_meta_boxes() { global $post; // Comments/Reviews. if ( isset( $post ) && ( 'publish' === $post->post_status || 'private' === $post->post_status ) && post_type_supports( 'product', 'comments' ) ) { remove_meta_box( 'commentsdiv', 'product', 'normal' ); add_meta_box( 'commentsdiv', __( 'Reviews', 'woocommerce' ), 'post_comment_meta_box', 'product', 'normal' ); } } /** * Check if we're saving, the trigger an action based on the post type. * * @param int $post_id Post ID. * @param object $post Post object. */ public function save_meta_boxes( $post_id, $post ) { $post_id = absint( $post_id ); // $post_id and $post are required if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) { return; } // Dont' save meta boxes for revisions or autosaves. if ( Constants::is_true( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) { return; } // Check the nonce. if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return; } // Check the post being saved == the $post_id to prevent triggering this call for other save_post events. if ( empty( $_POST['post_ID'] ) || absint( $_POST['post_ID'] ) !== $post_id ) { return; } // Check user has permission to edit. if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } // We need this save event to run once to avoid potential endless loops. This would have been perfect: // remove_action( current_filter(), __METHOD__ ); // But cannot be used due to https://github.com/woocommerce/woocommerce/issues/6485 // When that is patched in core we can use the above. self::$saved_meta_boxes = true; // Check the post type. if ( in_array( $post->post_type, wc_get_order_types( 'order-meta-boxes' ), true ) ) { do_action( 'woocommerce_process_shop_order_meta', $post_id, $post ); } elseif ( in_array( $post->post_type, array( 'product', 'shop_coupon' ), true ) ) { do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post ); } } /** * Remove block-based templates from the list of available templates for products. * * @param string[] $templates Array of template header names keyed by the template file name. * * @return string[] Templates array excluding block-based templates. */ public function remove_block_templates( $templates ) { if ( count( $templates ) === 0 || ! function_exists( 'gutenberg_get_block_template' ) ) { return $templates; } $theme = wp_get_theme()->get_stylesheet(); $filtered_templates = array(); foreach ( $templates as $template_key => $template_name ) { $gutenberg_template = gutenberg_get_block_template( $theme . '//' . $template_key ); if ( ! $gutenberg_template ) { $filtered_templates[ $template_key ] = $template_name; } } return $filtered_templates; } } new WC_Admin_Meta_Boxes(); includes/admin/plugin-updates/class-wc-plugins-screen-updates.php 0000644 00000013046 15132754524 0021243 0 ustar 00 <?php /** * Manages WooCommerce plugin updating on the Plugins screen. * * @package WooCommerce\Admin * @version 3.2.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Plugin_Updates' ) ) { include_once dirname( __FILE__ ) . '/class-wc-plugin-updates.php'; } /** * Class WC_Plugins_Screen_Updates */ class WC_Plugins_Screen_Updates extends WC_Plugin_Updates { /** * The upgrade notice shown inline. * * @var string */ protected $upgrade_notice = ''; /** * Constructor. */ public function __construct() { add_action( 'in_plugin_update_message-woocommerce/woocommerce.php', array( $this, 'in_plugin_update_message' ), 10, 2 ); } /** * Show plugin changes on the plugins screen. Code adapted from W3 Total Cache. * * @param array $args Unused parameter. * @param stdClass $response Plugin update response. */ public function in_plugin_update_message( $args, $response ) { $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ); if ( ! is_string( $version_type ) ) { $version_type = 'none'; } $this->new_version = $response->new_version; $this->upgrade_notice = $this->get_upgrade_notice( $response->new_version ); $this->major_untested_plugins = $this->get_untested_plugins( $response->new_version, $version_type ); $current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) ); $new_version_parts = explode( '.', $this->new_version ); // If user has already moved to the minor version, we don't need to flag up anything. if ( version_compare( $current_version_parts[0] . '.' . $current_version_parts[1], $new_version_parts[0] . '.' . $new_version_parts[1], '=' ) ) { return; } if ( ! empty( $this->major_untested_plugins ) ) { $this->upgrade_notice .= $this->get_extensions_inline_warning_major(); } if ( ! empty( $this->major_untested_plugins ) ) { $this->upgrade_notice .= $this->get_extensions_modal_warning(); add_action( 'admin_print_footer_scripts', array( $this, 'plugin_screen_modal_js' ) ); } echo apply_filters( 'woocommerce_in_plugin_update_message', $this->upgrade_notice ? '</p>' . wp_kses_post( $this->upgrade_notice ) . '<p class="dummy">' : '' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } /** * Get the upgrade notice from WordPress.org. * * @param string $version WooCommerce new version. * @return string */ protected function get_upgrade_notice( $version ) { $transient_name = 'wc_upgrade_notice_' . $version; $upgrade_notice = get_transient( $transient_name ); if ( false === $upgrade_notice ) { $response = wp_safe_remote_get( 'https://plugins.svn.wordpress.org/woocommerce/trunk/readme.txt' ); if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) ) { $upgrade_notice = $this->parse_update_notice( $response['body'], $version ); set_transient( $transient_name, $upgrade_notice, DAY_IN_SECONDS ); } } return $upgrade_notice; } /** * Parse update notice from readme file. * * @param string $content WooCommerce readme file content. * @param string $new_version WooCommerce new version. * @return string */ private function parse_update_notice( $content, $new_version ) { $version_parts = explode( '.', $new_version ); $check_for_notices = array( $version_parts[0] . '.0', // Major. $version_parts[0] . '.0.0', // Major. $version_parts[0] . '.' . $version_parts[1], // Minor. $version_parts[0] . '.' . $version_parts[1] . '.' . $version_parts[2], // Patch. ); $notice_regexp = '~==\s*Upgrade Notice\s*==\s*=\s*(.*)\s*=(.*)(=\s*' . preg_quote( $new_version ) . '\s*=|$)~Uis'; $upgrade_notice = ''; foreach ( $check_for_notices as $check_version ) { if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $check_version, '>' ) ) { continue; } $matches = null; if ( preg_match( $notice_regexp, $content, $matches ) ) { $notices = (array) preg_split( '~[\r\n]+~', trim( $matches[2] ) ); if ( version_compare( trim( $matches[1] ), $check_version, '=' ) ) { $upgrade_notice .= '<p class="wc_plugin_upgrade_notice">'; foreach ( $notices as $index => $line ) { $upgrade_notice .= preg_replace( '~\[([^\]]*)\]\(([^\)]*)\)~', '<a href="${2}">${1}</a>', $line ); } $upgrade_notice .= '</p>'; } break; } } return wp_kses_post( $upgrade_notice ); } /** * JS for the modal window on the plugins screen. */ public function plugin_screen_modal_js() { ?> <script> ( function( $ ) { var $update_box = $( '#woocommerce-update' ); var $update_link = $update_box.find('a.update-link').first(); var update_url = $update_link.attr( 'href' ); // Set up thickbox. $update_link.removeClass( 'update-link' ); $update_link.addClass( 'wc-thickbox' ); $update_link.attr( 'href', '#TB_inline?height=600&width=550&inlineId=wc_untested_extensions_modal' ); // Trigger the update if the user accepts the modal's warning. $( '#wc_untested_extensions_modal .accept' ).on( 'click', function( evt ) { evt.preventDefault(); tb_remove(); $update_link.removeClass( 'wc-thickbox open-plugin-details-modal' ); $update_link.addClass( 'update-link' ); $update_link.attr( 'href', update_url ); $update_link.trigger( 'click' ); }); $( '#wc_untested_extensions_modal .cancel' ).on( 'click', function( evt ) { evt.preventDefault(); tb_remove(); }); })( jQuery ); </script> <?php $this->generic_modal_js(); } } new WC_Plugins_Screen_Updates(); includes/admin/plugin-updates/class-wc-updates-screen-updates.php 0000644 00000006070 15132754524 0021226 0 ustar 00 <?php /** * Manages WooCommerce plugin updating on the Updates screen. * * @package WooCommerce\Admin * @version 3.2.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Plugin_Updates' ) ) { include_once dirname( __FILE__ ) . '/class-wc-plugin-updates.php'; } /** * Class WC_Updates_Screen_Updates */ class WC_Updates_Screen_Updates extends WC_Plugin_Updates { /** * Constructor. */ public function __construct() { add_action( 'admin_print_footer_scripts', array( $this, 'update_screen_modal' ) ); } /** * Show a warning message on the upgrades screen if the user tries to upgrade and has untested plugins. */ public function update_screen_modal() { $updateable_plugins = get_plugin_updates(); if ( empty( $updateable_plugins['woocommerce/woocommerce.php'] ) || empty( $updateable_plugins['woocommerce/woocommerce.php']->update ) || empty( $updateable_plugins['woocommerce/woocommerce.php']->update->new_version ) ) { return; } $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ); if ( ! is_string( $version_type ) ) { $version_type = 'none'; } $this->new_version = wc_clean( $updateable_plugins['woocommerce/woocommerce.php']->update->new_version ); $this->major_untested_plugins = $this->get_untested_plugins( $this->new_version, $version_type ); if ( ! empty( $this->major_untested_plugins ) ) { echo $this->get_extensions_modal_warning(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped $this->update_screen_modal_js(); } } /** * JS for the modal window on the updates screen. */ protected function update_screen_modal_js() { ?> <script> ( function( $ ) { var modal_dismissed = false; // Show the modal if the WC upgrade checkbox is checked. var show_modal_if_checked = function() { if ( modal_dismissed ) { return; } var $checkbox = $( 'input[value="woocommerce/woocommerce.php"]' ); if ( $checkbox.prop( 'checked' ) ) { $( '#wc-upgrade-warning' ).trigger( 'click' ); } } $( '#plugins-select-all, input[value="woocommerce/woocommerce.php"]' ).on( 'change', function() { show_modal_if_checked(); } ); // Add a hidden thickbox link to use for bringing up the modal. $('body').append( '<a href="#TB_inline?height=600&width=550&inlineId=wc_untested_extensions_modal" class="wc-thickbox" id="wc-upgrade-warning" style="display:none"></a>' ); // Don't show the modal again once it's been accepted. $( '#wc_untested_extensions_modal .accept' ).on( 'click', function( evt ) { evt.preventDefault(); modal_dismissed = true; tb_remove(); }); // Uncheck the WC update checkbox if the modal is canceled. $( '#wc_untested_extensions_modal .cancel' ).on( 'click', function( evt ) { evt.preventDefault(); $( 'input[value="woocommerce/woocommerce.php"]' ).prop( 'checked', false ); tb_remove(); }); })( jQuery ); </script> <?php $this->generic_modal_js(); } } new WC_Updates_Screen_Updates(); includes/admin/plugin-updates/views/html-notice-untested-extensions-modal.php 0000644 00000003565 15132754524 0023635 0 ustar 00 <?php /** * Admin View: Notice - Untested extensions. * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } $untested_plugins_msg = sprintf( /* translators: %s: version number */ __( 'The following active plugin(s) have not declared compatibility with WooCommerce %s yet and should be updated and examined further before you proceed:', 'woocommerce' ), $new_version ); ?> <div id="wc_untested_extensions_modal"> <div class="wc_untested_extensions_modal--content"> <h1><?php esc_html_e( "Are you sure you're ready?", 'woocommerce' ); ?></h1> <div class="wc_plugin_upgrade_notice extensions_warning"> <p><?php echo esc_html( $untested_plugins_msg ); ?></p> <div class="plugin-details-table-container"> <table class="plugin-details-table" cellspacing="0"> <thead> <tr> <th><?php esc_html_e( 'Plugin', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Tested up to WooCommerce version', 'woocommerce' ); ?></th> </tr> </thead> <tbody> <?php foreach ( $plugins as $plugin ) : ?> <tr> <td><?php echo esc_html( $plugin['Name'] ); ?></td> <td><?php echo esc_html( $plugin['WC tested up to'] ); ?></td> </tr> <?php endforeach ?> </tbody> </table> </div> <p><?php esc_html_e( 'We strongly recommend creating a backup of your site before updating.', 'woocommerce' ); ?> <a href="https://woocommerce.com/2017/05/create-use-backups-woocommerce/" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p> <?php if ( current_user_can( 'update_plugins' ) ) : ?> <div class="actions"> <a href="#" class="button button-secondary cancel"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a> <a class="button button-primary accept" href="#"><?php esc_html_e( 'Update now', 'woocommerce' ); ?></a> </div> <?php endif ?> </div> </div> </div> includes/admin/plugin-updates/views/html-notice-untested-extensions-inline.php 0000644 00000001410 15132754524 0024002 0 ustar 00 <?php /** * Admin View: Notice - Untested extensions. * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wc_plugin_upgrade_notice extensions_warning <?php echo esc_attr( $upgrade_type ); ?>"> <p><?php echo wp_kses_post( $message ); ?></p> <table class="plugin-details-table" cellspacing="0"> <thead> <tr> <th><?php esc_html_e( 'Plugin', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Tested up to WooCommerce version', 'woocommerce' ); ?></th> </tr> </thead> <tbody> <?php foreach ( $plugins as $plugin ) : ?> <tr> <td><?php echo esc_html( $plugin['Name'] ); ?></td> <td><?php echo esc_html( $plugin['WC tested up to'] ); ?></td> </tr> <?php endforeach ?> </tbody> </table> </div> includes/admin/plugin-updates/class-wc-plugin-updates.php 0000644 00000015367 15132754524 0017613 0 ustar 00 <?php /** * Class for displaying plugin warning notifications and determining 3rd party plugin compatibility. * * @package WooCommerce\Admin * @version 3.2.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Admin_Plugin_Updates Class. */ class WC_Plugin_Updates { /** * This is the header used by extensions to show requirements. * * @var string */ const VERSION_REQUIRED_HEADER = 'WC requires at least'; /** * This is the header used by extensions to show testing. * * @var string */ const VERSION_TESTED_HEADER = 'WC tested up to'; /** * The version for the update to WooCommerce. * * @var string */ protected $new_version = ''; /** * Array of plugins lacking testing with the major version. * * @var array */ protected $major_untested_plugins = array(); /** * Common JS for initializing and managing thickbox-based modals. */ protected function generic_modal_js() { ?> <script> ( function( $ ) { // Initialize thickbox. tb_init( '.wc-thickbox' ); var old_tb_position = false; // Make the WC thickboxes look good when opened. $( '.wc-thickbox' ).on( 'click', function( evt ) { var $overlay = $( '#TB_overlay' ); if ( ! $overlay.length ) { $( 'body' ).append( '<div id="TB_overlay"></div><div id="TB_window" class="wc_untested_extensions_modal_container"></div>' ); } else { $( '#TB_window' ).removeClass( 'thickbox-loading' ).addClass( 'wc_untested_extensions_modal_container' ); } // WP overrides the tb_position function. We need to use a different tb_position function than that one. // This is based on the original tb_position. if ( ! old_tb_position ) { old_tb_position = tb_position; } tb_position = function() { $( '#TB_window' ).css( { marginLeft: '-' + parseInt( ( TB_WIDTH / 2 ), 10 ) + 'px', width: TB_WIDTH + 'px' } ); $( '#TB_window' ).css( { marginTop: '-' + parseInt( ( TB_HEIGHT / 2 ), 10 ) + 'px' } ); }; }); // Reset tb_position to WP default when modal is closed. $( 'body' ).on( 'thickbox:removed', function() { if ( old_tb_position ) { tb_position = old_tb_position; } }); })( jQuery ); </script> <?php } /* |-------------------------------------------------------------------------- | Message Helpers |-------------------------------------------------------------------------- | | Methods for getting messages. */ /** * Get the inline warning notice for major version updates. * * @return string */ protected function get_extensions_inline_warning_major() { $upgrade_type = 'major'; $plugins = $this->major_untested_plugins; $version_parts = explode( '.', $this->new_version ); $new_version = $version_parts[0] . '.0'; if ( empty( $plugins ) ) { return; } /* translators: %s: version number */ $message = sprintf( __( "<strong>Heads up!</strong> The versions of the following plugins you're running haven't been tested with WooCommerce %s. Please update them or confirm compatibility before updating WooCommerce, or you may experience issues:", 'woocommerce' ), $new_version ); ob_start(); include __DIR__ . '/views/html-notice-untested-extensions-inline.php'; return ob_get_clean(); } /** * Get the warning notice for the modal window. * * @return string */ protected function get_extensions_modal_warning() { $version_parts = explode( '.', $this->new_version ); $new_version = $version_parts[0] . '.0'; $plugins = $this->major_untested_plugins; ob_start(); include __DIR__ . '/views/html-notice-untested-extensions-modal.php'; return ob_get_clean(); } /* |-------------------------------------------------------------------------- | Data Helpers |-------------------------------------------------------------------------- | | Methods for getting & manipulating data. */ /** * Get installed plugins that have a tested version lower than the input version. * * In case of testing major version compatibility and if current WC version is >= major version part * of the $new_version, no plugins are returned, even if they don't explicitly declare compatibility * with the $new_version. * * @param string $new_version WooCommerce version to test against. * @param string $release 'major', 'minor', or 'none'. * @return array of plugin info arrays */ public function get_untested_plugins( $new_version, $release ) { // Since 5.0 all versions are backwards compatible. if ( 'none' === $release ) { return array(); } $extensions = array_merge( $this->get_plugins_with_header( self::VERSION_TESTED_HEADER ), $this->get_plugins_for_woocommerce() ); $untested = array(); $new_version_parts = explode( '.', $new_version ); $version = $new_version_parts[0]; if ( 'minor' === $release ) { $version .= '.' . $new_version_parts[1]; } foreach ( $extensions as $file => $plugin ) { if ( ! empty( $plugin[ self::VERSION_TESTED_HEADER ] ) ) { $plugin_version_parts = explode( '.', $plugin[ self::VERSION_TESTED_HEADER ] ); if ( ! is_numeric( $plugin_version_parts[0] ) || ( 'minor' === $release && ! isset( $plugin_version_parts[1] ) ) || ( 'minor' === $release && ! is_numeric( $plugin_version_parts[1] ) ) ) { continue; } $plugin_version = $plugin_version_parts[0]; if ( 'minor' === $release ) { $plugin_version .= '.' . $plugin_version_parts[1]; } if ( version_compare( $plugin_version, $version, '<' ) ) { $untested[ $file ] = $plugin; } } else { $plugin[ self::VERSION_TESTED_HEADER ] = __( 'unknown', 'woocommerce' ); $untested[ $file ] = $plugin; } } return $untested; } /** * Get plugins that have a valid value for a specific header. * * @param string $header Plugin header to search for. * @return array Array of plugins that contain the searched header. */ protected function get_plugins_with_header( $header ) { $plugins = get_plugins(); $matches = array(); foreach ( $plugins as $file => $plugin ) { if ( ! empty( $plugin[ $header ] ) ) { $matches[ $file ] = $plugin; } } return apply_filters( 'woocommerce_get_plugins_with_header', $matches, $header, $plugins ); } /** * Get plugins which "maybe" are for WooCommerce. * * @return array of plugin info arrays */ protected function get_plugins_for_woocommerce() { $plugins = get_plugins(); $matches = array(); foreach ( $plugins as $file => $plugin ) { if ( 'WooCommerce' !== $plugin['Name'] && ( stristr( $plugin['Name'], 'woocommerce' ) || stristr( $plugin['Description'], 'woocommerce' ) ) ) { $matches[ $file ] = $plugin; } } return apply_filters( 'woocommerce_get_plugins_for_woocommerce', $matches, $plugins ); } } includes/admin/class-wc-admin-post-types.php 0000644 00000107313 15132754524 0015117 0 ustar 00 <?php /** * Post Types Admin * * @package WooCommerce\Admin * @version 3.3.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_Post_Types', false ) ) { new WC_Admin_Post_Types(); return; } /** * WC_Admin_Post_Types Class. * * Handles the edit posts views and some functionality on the edit post screen for WC post types. */ class WC_Admin_Post_Types { /** * Constructor. */ public function __construct() { include_once __DIR__ . '/class-wc-admin-meta-boxes.php'; if ( ! function_exists( 'duplicate_post_plugin_activation' ) ) { include_once __DIR__ . '/class-wc-admin-duplicate-product.php'; } // Load correct list table classes for current screen. add_action( 'current_screen', array( $this, 'setup_screen' ) ); add_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); // Admin notices. add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) ); add_filter( 'bulk_post_updated_messages', array( $this, 'bulk_post_updated_messages' ), 10, 2 ); // Disable Auto Save. add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) ); // Extra post data and screen elements. add_action( 'edit_form_top', array( $this, 'edit_form_top' ) ); add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 1, 2 ); add_action( 'edit_form_after_title', array( $this, 'edit_form_after_title' ) ); add_filter( 'default_hidden_meta_boxes', array( $this, 'hidden_meta_boxes' ), 10, 2 ); add_action( 'post_submitbox_misc_actions', array( $this, 'product_data_visibility' ) ); // Uploads. add_filter( 'upload_dir', array( $this, 'upload_dir' ) ); add_filter( 'wp_unique_filename', array( $this, 'update_filename' ), 10, 3 ); add_action( 'media_upload_downloadable_product', array( $this, 'media_upload_downloadable_product' ) ); // Hide template for CPT archive. add_filter( 'theme_page_templates', array( $this, 'hide_cpt_archive_templates' ), 10, 3 ); add_action( 'edit_form_top', array( $this, 'show_cpt_archive_notice' ) ); // Add a post display state for special WC pages. add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 ); // Bulk / quick edit. add_action( 'bulk_edit_custom_box', array( $this, 'bulk_edit' ), 10, 2 ); add_action( 'quick_edit_custom_box', array( $this, 'quick_edit' ), 10, 2 ); add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); add_action( 'woocommerce_product_bulk_and_quick_edit', array( $this, 'bulk_and_quick_edit_save_post' ), 10, 2 ); } /** * Looks at the current screen and loads the correct list table handler. * * @since 3.3.0 */ public function setup_screen() { global $wc_list_table; $request_data = $this->request_data(); $screen_id = false; if ( function_exists( 'get_current_screen' ) ) { $screen = get_current_screen(); $screen_id = isset( $screen, $screen->id ) ? $screen->id : ''; } if ( ! empty( $request_data['screen'] ) ) { $screen_id = wc_clean( wp_unslash( $request_data['screen'] ) ); } switch ( $screen_id ) { case 'edit-shop_order': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-orders.php'; $wc_list_table = new WC_Admin_List_Table_Orders(); break; case 'edit-shop_coupon': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-coupons.php'; $wc_list_table = new WC_Admin_List_Table_Coupons(); break; case 'edit-product': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-products.php'; $wc_list_table = new WC_Admin_List_Table_Products(); break; } // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times. remove_action( 'current_screen', array( $this, 'setup_screen' ) ); remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); } /** * Change messages when a post type is updated. * * @param array $messages Array of messages. * @return array */ public function post_updated_messages( $messages ) { global $post; $messages['product'] = array( 0 => '', // Unused. Messages start at index 1. /* translators: %s: Product view URL. */ 1 => sprintf( __( 'Product updated. <a href="%s">View Product</a>', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Product updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), /* translators: %s: product url */ 6 => sprintf( __( 'Product published. <a href="%s">View Product</a>', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), 7 => __( 'Product saved.', 'woocommerce' ), /* translators: %s: product url */ 8 => sprintf( __( 'Product submitted. <a target="_blank" href="%s">Preview product</a>', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), 9 => sprintf( /* translators: 1: date 2: product url */ __( 'Product scheduled for: %1$s. <a target="_blank" href="%2$s">Preview product</a>', 'woocommerce' ), '<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>', esc_url( get_permalink( $post->ID ) ) ), /* translators: %s: product url */ 10 => sprintf( __( 'Product draft updated. <a target="_blank" href="%s">Preview product</a>', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), ); $messages['shop_order'] = array( 0 => '', // Unused. Messages start at index 1. 1 => __( 'Order updated.', 'woocommerce' ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Order updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), 6 => __( 'Order updated.', 'woocommerce' ), 7 => __( 'Order saved.', 'woocommerce' ), 8 => __( 'Order submitted.', 'woocommerce' ), 9 => sprintf( /* translators: %s: date */ __( 'Order scheduled for: %s.', 'woocommerce' ), '<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>' ), 10 => __( 'Order draft updated.', 'woocommerce' ), 11 => __( 'Order updated and sent.', 'woocommerce' ), ); $messages['shop_coupon'] = array( 0 => '', // Unused. Messages start at index 1. 1 => __( 'Coupon updated.', 'woocommerce' ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Coupon updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), 6 => __( 'Coupon updated.', 'woocommerce' ), 7 => __( 'Coupon saved.', 'woocommerce' ), 8 => __( 'Coupon submitted.', 'woocommerce' ), 9 => sprintf( /* translators: %s: date */ __( 'Coupon scheduled for: %s.', 'woocommerce' ), '<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>' ), 10 => __( 'Coupon draft updated.', 'woocommerce' ), ); return $messages; } /** * Specify custom bulk actions messages for different post types. * * @param array $bulk_messages Array of messages. * @param array $bulk_counts Array of how many objects were updated. * @return array */ public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) { $bulk_messages['product'] = array( /* translators: %s: product count */ 'updated' => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: product count */ 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: product count */ 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: product count */ 'trashed' => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: product count */ 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); $bulk_messages['shop_order'] = array( /* translators: %s: order count */ 'updated' => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: order count */ 'locked' => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: order count */ 'deleted' => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: order count */ 'trashed' => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: order count */ 'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); $bulk_messages['shop_coupon'] = array( /* translators: %s: coupon count */ 'updated' => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: coupon count */ 'locked' => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: coupon count */ 'deleted' => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: coupon count */ 'trashed' => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: coupon count */ 'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); return $bulk_messages; } /** * Custom bulk edit - form. * * @param string $column_name Column being shown. * @param string $post_type Post type being shown. */ public function bulk_edit( $column_name, $post_type ) { if ( 'price' !== $column_name || 'product' !== $post_type ) { return; } $shipping_class = get_terms( 'product_shipping_class', array( 'hide_empty' => false, ) ); include WC()->plugin_path() . '/includes/admin/views/html-bulk-edit-product.php'; } /** * Custom quick edit - form. * * @param string $column_name Column being shown. * @param string $post_type Post type being shown. */ public function quick_edit( $column_name, $post_type ) { if ( 'price' !== $column_name || 'product' !== $post_type ) { return; } $shipping_class = get_terms( 'product_shipping_class', array( 'hide_empty' => false, ) ); include WC()->plugin_path() . '/includes/admin/views/html-quick-edit-product.php'; } /** * Offers a way to hook into save post without causing an infinite loop * when quick/bulk saving product info. * * @since 3.0.0 * @param int $post_id Post ID being saved. * @param object $post Post object being saved. */ public function bulk_and_quick_edit_hook( $post_id, $post ) { remove_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ) ); do_action( 'woocommerce_product_bulk_and_quick_edit', $post_id, $post ); add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); } /** * Quick and bulk edit saving. * * @param int $post_id Post ID being saved. * @param object $post Post object being saved. * @return int */ public function bulk_and_quick_edit_save_post( $post_id, $post ) { $request_data = $this->request_data(); // If this is an autosave, our form has not been submitted, so we don't want to do anything. if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) { return $post_id; } // Don't save revisions and autosaves. if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'product' !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) { return $post_id; } // Check nonce. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) { return $post_id; } // Get the product and save. $product = wc_get_product( $post ); if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok. $this->quick_edit_save( $post_id, $product ); } else { $this->bulk_edit_save( $post_id, $product ); } return $post_id; } /** * Quick edit. * * @param int $post_id Post ID being saved. * @param WC_Product $product Product object. */ private function quick_edit_save( $post_id, $product ) { $request_data = $this->request_data(); $data_store = $product->get_data_store(); $old_regular_price = $product->get_regular_price(); $old_sale_price = $product->get_sale_price(); $input_to_props = array( '_weight' => 'weight', '_length' => 'length', '_width' => 'width', '_height' => 'height', '_visibility' => 'catalog_visibility', '_tax_class' => 'tax_class', '_tax_status' => 'tax_status', ); foreach ( $input_to_props as $input_var => $prop ) { if ( isset( $request_data[ $input_var ] ) ) { $product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) ); } } if ( isset( $request_data['_sku'] ) ) { $sku = $product->get_sku(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_sku = (string) wc_clean( $request_data['_sku'] ); if ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $post_id, $new_sku ); if ( $unique_sku ) { $product->set_sku( wc_clean( wp_unslash( $new_sku ) ) ); } } else { $product->set_sku( '' ); } } } if ( ! empty( $request_data['_shipping_class'] ) ) { if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { $product->set_shipping_class_id( 0 ); } else { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } } $product->set_featured( isset( $request_data['_featured'] ) ); if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) { if ( isset( $request_data['_regular_price'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] ); $product->set_regular_price( $new_regular_price ); } else { $new_regular_price = null; } if ( isset( $request_data['_sale_price'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] ); $product->set_sale_price( $new_sale_price ); } else { $new_sale_price = null; } // Handle price - remove dates and set to lowest. $price_changed = false; if ( ! is_null( $new_regular_price ) && $new_regular_price !== $old_regular_price ) { $price_changed = true; } elseif ( ! is_null( $new_sale_price ) && $new_sale_price !== $old_sale_price ) { $price_changed = true; } if ( $price_changed ) { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); } } // Handle Stock Data. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no'; if ( ! empty( $request_data['_stock_status'] ) ) { $stock_status = wc_clean( $request_data['_stock_status'] ); } else { $stock_status = $product->is_type( 'variable' ) ? null : 'instock'; } // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $product->set_manage_stock( $manage_stock ); if ( 'external' !== $product->get_type() ) { $product->set_backorders( $backorders ); } if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : ''; $product->set_stock_quantity( $stock_amount ); } $product = $this->maybe_update_stock_status( $product, $stock_status ); $product->save(); do_action( 'woocommerce_product_quick_edit_save', $product ); } /** * Bulk edit. * * @param int $post_id Post ID being saved. * @param WC_Product $product Product object. */ public function bulk_edit_save( $post_id, $product ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash $request_data = $this->request_data(); $data_store = $product->get_data_store(); if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) { $product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) ); } if ( ! empty( $request_data['change_dimensions'] ) ) { if ( isset( $request_data['_length'] ) ) { $product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) ); } if ( isset( $request_data['_width'] ) ) { $product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) ); } if ( isset( $request_data['_height'] ) ) { $product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) ); } } if ( ! empty( $request_data['_tax_status'] ) ) { $product->set_tax_status( wc_clean( $request_data['_tax_status'] ) ); } if ( ! empty( $request_data['_tax_class'] ) ) { $tax_class = wc_clean( wp_unslash( $request_data['_tax_class'] ) ); if ( 'standard' === $tax_class ) { $tax_class = ''; } $product->set_tax_class( $tax_class ); } if ( ! empty( $request_data['_shipping_class'] ) ) { if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { $product->set_shipping_class_id( 0 ); } else { $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } } if ( ! empty( $request_data['_visibility'] ) ) { $product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) ); } if ( ! empty( $request_data['_featured'] ) ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $product->set_featured( wp_unslash( $request_data['_featured'] ) ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } if ( ! empty( $request_data['_sold_individually'] ) ) { if ( 'yes' === $request_data['_sold_individually'] ) { $product->set_sold_individually( 'yes' ); } else { $product->set_sold_individually( '' ); } } // Handle price - remove dates and set to lowest. $change_price_product_types = apply_filters( 'woocommerce_bulk_edit_save_price_product_types', array( 'simple', 'external' ) ); $can_product_type_change_price = false; foreach ( $change_price_product_types as $product_type ) { if ( $product->is_type( $product_type ) ) { $can_product_type_change_price = true; break; } } if ( $can_product_type_change_price ) { $regular_price_changed = $this->set_new_price( $product, 'regular' ); $sale_price_changed = $this->set_new_price( $product, 'sale' ); if ( $regular_price_changed || $sale_price_changed ) { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); if ( $product->get_regular_price() < $product->get_sale_price() ) { $product->set_sale_price( '' ); } } } // Handle Stock Data. $was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; $backorders = $product->get_backorders(); $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders; if ( ! empty( $request_data['_manage_stock'] ) ) { $manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; } else { $manage_stock = $was_managing_stock; } $stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity(); $product->set_manage_stock( $manage_stock ); if ( 'external' !== $product->get_type() ) { $product->set_backorders( $backorders ); } if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $change_stock = absint( $request_data['change_stock'] ); switch ( $change_stock ) { case 2: wc_update_product_stock( $product, $stock_amount, 'increase', true ); break; case 3: wc_update_product_stock( $product, $stock_amount, 'decrease', true ); break; default: wc_update_product_stock( $product, $stock_amount, 'set', true ); break; } } else { // Reset values if WooCommerce Setting - Manage Stock status is disabled. $product->set_stock_quantity( '' ); $product->set_manage_stock( 'no' ); } $stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] ); $product = $this->maybe_update_stock_status( $product, $stock_status ); $product->save(); do_action( 'woocommerce_product_bulk_edit_save', $product ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Disable the auto-save functionality for Orders. */ public function disable_autosave() { global $post; if ( $post && in_array( get_post_type( $post->ID ), wc_get_order_types( 'order-meta-boxes' ), true ) ) { wp_dequeue_script( 'autosave' ); } } /** * Output extra data on post forms. * * @param WP_Post $post Current post object. */ public function edit_form_top( $post ) { echo '<input type="hidden" id="original_post_title" name="original_post_title" value="' . esc_attr( $post->post_title ) . '" />'; } /** * Change title boxes in admin. * * @param string $text Text to shown. * @param WP_Post $post Current post object. * @return string */ public function enter_title_here( $text, $post ) { switch ( $post->post_type ) { case 'product': $text = esc_html__( 'Product name', 'woocommerce' ); break; case 'shop_coupon': $text = esc_html__( 'Coupon code', 'woocommerce' ); break; } return $text; } /** * Print coupon description textarea field. * * @param WP_Post $post Current post object. */ public function edit_form_after_title( $post ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped if ( 'shop_coupon' === $post->post_type ) { ?> <textarea id="woocommerce-coupon-description" name="excerpt" cols="5" rows="2" placeholder="<?php esc_attr_e( 'Description (optional)', 'woocommerce' ); ?>"><?php echo $post->post_excerpt; ?></textarea> <?php } // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Hidden default Meta-Boxes. * * @param array $hidden Hidden boxes. * @param object $screen Current screen. * @return array */ public function hidden_meta_boxes( $hidden, $screen ) { if ( 'product' === $screen->post_type && 'post' === $screen->base ) { $hidden = array_merge( $hidden, array( 'postcustom' ) ); } return $hidden; } /** * Output product visibility options. */ public function product_data_visibility() { global $post, $thepostid, $product_object; if ( 'product' !== $post->post_type ) { return; } $thepostid = $post->ID; $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); $current_visibility = $product_object->get_catalog_visibility(); $current_featured = wc_bool_to_string( $product_object->get_featured() ); $visibility_options = wc_get_product_visibility_options(); ?> <div class="misc-pub-section" id="catalog-visibility"> <?php esc_html_e( 'Catalog visibility:', 'woocommerce' ); ?> <strong id="catalog-visibility-display"> <?php echo isset( $visibility_options[ $current_visibility ] ) ? esc_html( $visibility_options[ $current_visibility ] ) : esc_html( $current_visibility ); if ( 'yes' === $current_featured ) { echo ', ' . esc_html__( 'Featured', 'woocommerce' ); } ?> </strong> <a href="#catalog-visibility" class="edit-catalog-visibility hide-if-no-js"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> <div id="catalog-visibility-select" class="hide-if-js"> <input type="hidden" name="current_visibility" id="current_visibility" value="<?php echo esc_attr( $current_visibility ); ?>" /> <input type="hidden" name="current_featured" id="current_featured" value="<?php echo esc_attr( $current_featured ); ?>" /> <?php echo '<p>' . esc_html__( 'This setting determines which shop pages products will be listed on.', 'woocommerce' ) . '</p>'; foreach ( $visibility_options as $name => $label ) { echo '<input type="radio" name="_visibility" id="_visibility_' . esc_attr( $name ) . '" value="' . esc_attr( $name ) . '" ' . checked( $current_visibility, $name, false ) . ' data-label="' . esc_attr( $label ) . '" /> <label for="_visibility_' . esc_attr( $name ) . '" class="selectit">' . esc_html( $label ) . '</label><br />'; } echo '<br /><input type="checkbox" name="_featured" id="_featured" ' . checked( $current_featured, 'yes', false ) . ' /> <label for="_featured">' . esc_html__( 'This is a featured product', 'woocommerce' ) . '</label><br />'; ?> <p> <a href="#catalog-visibility" class="save-post-visibility hide-if-no-js button"><?php esc_html_e( 'OK', 'woocommerce' ); ?></a> <a href="#catalog-visibility" class="cancel-post-visibility hide-if-no-js"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a> </p> </div> </div> <?php } /** * Change upload dir for downloadable files. * * @param array $pathdata Array of paths. * @return array */ public function upload_dir( $pathdata ) { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( isset( $_POST['type'] ) && 'downloadable_product' === $_POST['type'] ) { if ( empty( $pathdata['subdir'] ) ) { $pathdata['path'] = $pathdata['path'] . '/woocommerce_uploads'; $pathdata['url'] = $pathdata['url'] . '/woocommerce_uploads'; $pathdata['subdir'] = '/woocommerce_uploads'; } else { $new_subdir = '/woocommerce_uploads' . $pathdata['subdir']; $pathdata['path'] = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['path'] ); $pathdata['url'] = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['url'] ); $pathdata['subdir'] = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['subdir'] ); } } return $pathdata; // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Change filename for WooCommerce uploads and prepend unique chars for security. * * @param string $full_filename Original filename. * @param string $ext Extension of file. * @param string $dir Directory path. * * @return string New filename with unique hash. * @since 4.0 */ public function update_filename( $full_filename, $ext, $dir ) { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['type'] ) || ! 'downloadable_product' === $_POST['type'] ) { return $full_filename; } if ( ! strpos( $dir, 'woocommerce_uploads' ) ) { return $full_filename; } if ( 'no' === get_option( 'woocommerce_downloads_add_hash_to_filename' ) ) { return $full_filename; } return $this->unique_filename( $full_filename, $ext ); // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Change filename to append random text. * * @param string $full_filename Original filename with extension. * @param string $ext Extension. * * @return string Modified filename. */ public function unique_filename( $full_filename, $ext ) { $ideal_random_char_length = 6; // Not going with a larger length because then downloaded filename will not be pretty. $max_filename_length = 255; // Max file name length for most file systems. $length_to_prepend = min( $ideal_random_char_length, $max_filename_length - strlen( $full_filename ) - 1 ); if ( 1 > $length_to_prepend ) { return $full_filename; } $suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) ); $filename = $full_filename; if ( strlen( $ext ) > 0 ) { $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ); } $full_filename = str_replace( $filename, "$filename-$suffix", $full_filename ); return $full_filename; } /** * Run a filter when uploading a downloadable product. */ public function woocommerce_media_upload_downloadable_product() { do_action( 'media_upload_file' ); } /** * Grant downloadable file access to any newly added files on any existing. * orders for this product that have previously been granted downloadable file access. * * @param int $product_id product identifier. * @param int $variation_id optional product variation identifier. * @param array $downloadable_files newly set files. * @deprecated 3.3.0 and moved to post-data class. */ public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) { wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' ); WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ); } /** * When editing the shop page, we should hide templates. * * @param array $page_templates Templates array. * @param string $theme Classname. * @param WP_Post $post The current post object. * @return array */ public function hide_cpt_archive_templates( $page_templates, $theme, $post ) { $shop_page_id = wc_get_page_id( 'shop' ); if ( $post && absint( $post->ID ) === $shop_page_id ) { $page_templates = array(); } return $page_templates; } /** * Show a notice above the CPT archive. * * @param WP_Post $post The current post object. */ public function show_cpt_archive_notice( $post ) { $shop_page_id = wc_get_page_id( 'shop' ); if ( $post && absint( $post->ID ) === $shop_page_id ) { echo '<div class="notice notice-info">'; /* translators: %s: URL to read more about the shop page. */ echo '<p>' . sprintf( wp_kses_post( __( 'This is the WooCommerce shop page. The shop page is a special archive that lists your products. <a href="%s">You can read more about this here</a>.', 'woocommerce' ) ), 'https://docs.woocommerce.com/document/woocommerce-pages/#section-4' ) . '</p>'; echo '</div>'; } } /** * Add a post display state for special WC pages in the page list table. * * @param array $post_states An array of post display states. * @param WP_Post $post The current post object. */ public function add_display_post_states( $post_states, $post ) { if ( wc_get_page_id( 'shop' ) === $post->ID ) { $post_states['wc_page_for_shop'] = __( 'Shop Page', 'woocommerce' ); } if ( wc_get_page_id( 'cart' ) === $post->ID ) { $post_states['wc_page_for_cart'] = __( 'Cart Page', 'woocommerce' ); } if ( wc_get_page_id( 'checkout' ) === $post->ID ) { $post_states['wc_page_for_checkout'] = __( 'Checkout Page', 'woocommerce' ); } if ( wc_get_page_id( 'myaccount' ) === $post->ID ) { $post_states['wc_page_for_myaccount'] = __( 'My Account Page', 'woocommerce' ); } if ( wc_get_page_id( 'terms' ) === $post->ID ) { $post_states['wc_page_for_terms'] = __( 'Terms and Conditions Page', 'woocommerce' ); } return $post_states; } /** * Apply product type constraints to stock status. * * @param WC_Product $product The product whose stock status will be adjusted. * @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request. * @return WC_Product The supplied product, or the synced product if it was a variable product. */ private function maybe_update_stock_status( $product, $stock_status ) { if ( $product->is_type( 'external' ) ) { // External products are always in stock. $product->set_stock_status( 'instock' ); } elseif ( isset( $stock_status ) ) { if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) { // Stock status is determined by children. foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! $product->get_manage_stock() ) { $child->set_stock_status( $stock_status ); $child->save(); } } $product = WC_Product_Variable::sync( $product, false ); } else { $product->set_stock_status( $stock_status ); } } return $product; } /** * Set the new regular or sale price if requested. * * @param WC_Product $product The product to set the new price for. * @param string $price_type 'regular' or 'sale'. * @return bool true if a new price has been set, false otherwise. */ private function set_new_price( $product, $price_type ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended $request_data = $this->request_data(); if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) { return false; } $old_price = $product->{"get_{$price_type}_price"}(); $price_changed = false; $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); $raw_price = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) ); $is_percentage = (bool) strstr( $raw_price, '%' ); $price = wc_format_decimal( $raw_price ); switch ( $change_price ) { case 1: $new_price = $price; break; case 2: if ( $is_percentage ) { $percent = $price / 100; $new_price = $old_price + ( $old_price * $percent ); } else { $new_price = $old_price + $price; } break; case 3: if ( $is_percentage ) { $percent = $price / 100; $new_price = max( 0, $old_price - ( $old_price * $percent ) ); } else { $new_price = max( 0, $old_price - $price ); } break; case 4: if ( 'sale' !== $price_type ) { break; } $regular_price = $product->get_regular_price(); if ( $is_percentage ) { $percent = $price / 100; $new_price = max( 0, $regular_price - ( NumberUtil::round( $regular_price * $percent, wc_get_price_decimals() ) ) ); } else { $new_price = max( 0, $regular_price - $price ); } break; default: break; } if ( isset( $new_price ) && $new_price !== $old_price ) { $price_changed = true; $new_price = NumberUtil::round( $new_price, wc_get_price_decimals() ); $product->{"set_{$price_type}_price"}( $new_price ); } return $price_changed; // phpcs:disable WordPress.Security.NonceVerification.Recommended } /** * Get the current request data ($_REQUEST superglobal). * This method is added to ease unit testing. * * @return array The $_REQUEST superglobal. */ protected function request_data() { return $_REQUEST; } } new WC_Admin_Post_Types(); includes/admin/wc-admin-functions.php 0000644 00000040540 15132754524 0013673 0 ustar 00 <?php /** * WooCommerce Admin Functions * * @package WooCommerce\Admin\Functions * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Get all WooCommerce screen ids. * * @return array */ function wc_get_screen_ids() { $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); $screen_ids = array( 'toplevel_page_' . $wc_screen_id, $wc_screen_id . '_page_wc-reports', $wc_screen_id . '_page_wc-shipping', $wc_screen_id . '_page_wc-settings', $wc_screen_id . '_page_wc-status', $wc_screen_id . '_page_wc-addons', 'toplevel_page_wc-reports', 'product_page_product_attributes', 'product_page_product_exporter', 'product_page_product_importer', 'edit-product', 'product', 'edit-shop_coupon', 'shop_coupon', 'edit-product_cat', 'edit-product_tag', 'profile', 'user-edit', ); foreach ( wc_get_order_types() as $type ) { $screen_ids[] = $type; $screen_ids[] = 'edit-' . $type; } $attributes = wc_get_attribute_taxonomies(); if ( $attributes ) { foreach ( $attributes as $attribute ) { $screen_ids[] = 'edit-' . wc_attribute_taxonomy_name( $attribute->attribute_name ); } } return apply_filters( 'woocommerce_screen_ids', $screen_ids ); } /** * Create a page and store the ID in an option. * * @param mixed $slug Slug for the new page. * @param string $option Option name to store the page's ID. * @param string $page_title (default: '') Title for the new page. * @param string $page_content (default: '') Content for the new page. * @param int $post_parent (default: 0) Parent for the new page. * @param string $post_status (default: publish) The post status of the new page. * @return int page ID. */ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0, $post_status = 'publish' ) { global $wpdb; $option_value = get_option( $option ); if ( $option_value > 0 ) { $page_object = get_post( $option_value ); if ( $page_object && 'page' === $page_object->post_type && ! in_array( $page_object->post_status, array( 'pending', 'trash', 'future', 'auto-draft' ), true ) ) { // Valid page is already in place. return $page_object->ID; } } if ( strlen( $page_content ) > 0 ) { // Search for an existing page with the specified page content (typically a shortcode). $shortcode = str_replace( array( '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ), '', $page_content ); $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_content LIKE %s LIMIT 1;", "%{$shortcode}%" ) ); } else { // Search for an existing page with the specified page slug. $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_name = %s LIMIT 1;", $slug ) ); } $valid_page_found = apply_filters( 'woocommerce_create_page_id', $valid_page_found, $slug, $page_content ); if ( $valid_page_found ) { if ( $option ) { update_option( $option, $valid_page_found ); } return $valid_page_found; } // Search for a matching valid trashed page. if ( strlen( $page_content ) > 0 ) { // Search for an existing page with the specified page content (typically a shortcode). $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) ); } else { // Search for an existing page with the specified page slug. $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;", $slug ) ); } if ( $trashed_page_found ) { $page_id = $trashed_page_found; $page_data = array( 'ID' => $page_id, 'post_status' => $post_status, ); wp_update_post( $page_data ); } else { $page_data = array( 'post_status' => $post_status, 'post_type' => 'page', 'post_author' => 1, 'post_name' => $slug, 'post_title' => $page_title, 'post_content' => $page_content, 'post_parent' => $post_parent, 'comment_status' => 'closed', ); $page_id = wp_insert_post( $page_data ); do_action( 'woocommerce_page_created', $page_id, $page_data ); } if ( $option ) { update_option( $option, $page_id ); } return $page_id; } /** * Output admin fields. * * Loops through the woocommerce options array and outputs each field. * * @param array $options Opens array to output. */ function woocommerce_admin_fields( $options ) { if ( ! class_exists( 'WC_Admin_Settings', false ) ) { include dirname( __FILE__ ) . '/class-wc-admin-settings.php'; } WC_Admin_Settings::output_fields( $options ); } /** * Update all settings which are passed. * * @param array $options Option fields to save. * @param array $data Passed data. */ function woocommerce_update_options( $options, $data = null ) { if ( ! class_exists( 'WC_Admin_Settings', false ) ) { include dirname( __FILE__ ) . '/class-wc-admin-settings.php'; } WC_Admin_Settings::save_fields( $options, $data ); } /** * Get a setting from the settings API. * * @param mixed $option_name Option name to save. * @param mixed $default Default value to save. * @return string */ function woocommerce_settings_get_option( $option_name, $default = '' ) { if ( ! class_exists( 'WC_Admin_Settings', false ) ) { include dirname( __FILE__ ) . '/class-wc-admin-settings.php'; } return WC_Admin_Settings::get_option( $option_name, $default ); } /** * Sees if line item stock has already reduced stock, and whether those values need adjusting e.g. after changing item qty. * * @since 3.6.0 * @param WC_Order_Item $item Item object. * @param integer $item_quantity Optional quantity to check against. Read from object if not passed. * @return boolean|array|WP_Error Array of changes or error object when stock is updated (@see wc_update_product_stock). False if nothing changes. */ function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) { if ( 'line_item' !== $item->get_type() ) { return false; } /** * Prevent adjust line item product stock. * * @since 3.7.1 * @param bool $prevent If should prevent. * @param WC_Order_Item $item Item object. * @param int $item_quantity Optional quantity to check against. */ if ( apply_filters( 'woocommerce_prevent_adjust_line_item_product_stock', false, $item, $item_quantity ) ) { return false; } $product = $item->get_product(); if ( ! $product || ! $product->managing_stock() ) { return false; } $item_quantity = wc_stock_amount( $item_quantity >= 0 ? $item_quantity : $item->get_quantity() ); $already_reduced_stock = wc_stock_amount( $item->get_meta( '_reduced_stock', true ) ); $restock_refunded_items = wc_stock_amount( $item->get_meta( '_restock_refunded_items', true ) ); $order = $item->get_order(); $refunded_item_quantity = $order->get_qty_refunded_for_item( $item->get_id() ); $diff = $item_quantity - $restock_refunded_items - $already_reduced_stock; /* * 0 as $item_quantity usually indicates we're deleting the order item. * Let's restore back the reduced count. */ if ( 0 === $item_quantity ) { $diff = $already_reduced_stock * -1; } if ( $diff < 0 ) { $new_stock = wc_update_product_stock( $product, $diff * -1, 'increase' ); } elseif ( $diff > 0 ) { $new_stock = wc_update_product_stock( $product, $diff, 'decrease' ); } else { return false; } if ( is_wp_error( $new_stock ) ) { return $new_stock; } $item->update_meta_data( '_reduced_stock', $item_quantity - $restock_refunded_items ); $item->save(); if ( $item_quantity > 0 ) { // If stock was reduced, then we need to mark this on parent order object as well so that cancel logic works properly. $order_data_store = WC_Data_Store::load( 'order' ); if ( $item->get_order_id() && ! $order_data_store->get_stock_reduced( $item->get_order_id() ) ) { $order_data_store->set_stock_reduced( $item->get_order_id(), true ); } } return array( 'from' => $new_stock + $diff, 'to' => $new_stock, ); } /** * Save order items. Uses the CRUD. * * @since 2.2 * @param int $order_id Order ID. * @param array $items Order items to save. */ function wc_save_order_items( $order_id, $items ) { // Allow other plugins to check change in order items before they are saved. do_action( 'woocommerce_before_save_order_items', $order_id, $items ); $qty_change_order_notes = array(); $order = wc_get_order( $order_id ); // Line items and fees. if ( isset( $items['order_item_id'] ) ) { $data_keys = array( 'line_tax' => array(), 'line_subtotal_tax' => array(), 'order_item_name' => null, 'order_item_qty' => null, 'order_item_tax_class' => null, 'line_total' => null, 'line_subtotal' => null, ); foreach ( $items['order_item_id'] as $item_id ) { $item = WC_Order_Factory::get_order_item( absint( $item_id ) ); if ( ! $item ) { continue; } $item_data = array(); foreach ( $data_keys as $key => $default ) { $item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_check_invalid_utf8( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default; } if ( '0' === $item_data['order_item_qty'] ) { $changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 ); if ( $changed_stock && ! is_wp_error( $changed_stock ) ) { $qty_change_order_notes[] = $item->get_name() . ' – ' . $changed_stock['from'] . '→' . $changed_stock['to']; } $item->delete(); continue; } $item->set_props( array( 'name' => $item_data['order_item_name'], 'quantity' => $item_data['order_item_qty'], 'tax_class' => $item_data['order_item_tax_class'], 'total' => $item_data['line_total'], 'subtotal' => $item_data['line_subtotal'], 'taxes' => array( 'total' => $item_data['line_tax'], 'subtotal' => $item_data['line_subtotal_tax'], ), ) ); if ( 'fee' === $item->get_type() ) { $item->set_amount( $item_data['line_total'] ); } if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) { foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) { $meta_key = substr( wp_unslash( $meta_key ), 0, 255 ); $meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : ''; if ( '' === $meta_key && '' === $meta_value ) { if ( ! strstr( $meta_id, 'new-' ) ) { $item->delete_meta_data_by_mid( $meta_id ); } } elseif ( strstr( $meta_id, 'new-' ) ) { $item->add_meta_data( $meta_key, $meta_value, false ); } else { $item->update_meta_data( $meta_key, $meta_value, $meta_id ); } } } // Allow other plugins to change item object before it is saved. do_action( 'woocommerce_before_save_order_item', $item ); $item->save(); if ( in_array( $order->get_status(), array( 'processing', 'completed', 'on-hold' ) ) ) { $changed_stock = wc_maybe_adjust_line_item_product_stock( $item ); if ( $changed_stock && ! is_wp_error( $changed_stock ) ) { $qty_change_order_notes[] = $item->get_name() . ' (' . $changed_stock['from'] . '→' . $changed_stock['to'] . ')'; } } } } // Shipping Rows. if ( isset( $items['shipping_method_id'] ) ) { $data_keys = array( 'shipping_method' => null, 'shipping_method_title' => null, 'shipping_cost' => 0, 'shipping_taxes' => array(), ); foreach ( $items['shipping_method_id'] as $item_id ) { $item = WC_Order_Factory::get_order_item( absint( $item_id ) ); if ( ! $item ) { continue; } $item_data = array(); foreach ( $data_keys as $key => $default ) { $item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_clean( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default; } $item->set_props( array( 'method_id' => $item_data['shipping_method'], 'method_title' => $item_data['shipping_method_title'], 'total' => $item_data['shipping_cost'], 'taxes' => array( 'total' => $item_data['shipping_taxes'], ), ) ); if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) { foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) { $meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : ''; if ( '' === $meta_key && '' === $meta_value ) { if ( ! strstr( $meta_id, 'new-' ) ) { $item->delete_meta_data_by_mid( $meta_id ); } } elseif ( strstr( $meta_id, 'new-' ) ) { $item->add_meta_data( $meta_key, $meta_value, false ); } else { $item->update_meta_data( $meta_key, $meta_value, $meta_id ); } } } $item->save(); } } $order = wc_get_order( $order_id ); if ( ! empty( $qty_change_order_notes ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Adjusted stock: %s', 'woocommerce' ), implode( ', ', $qty_change_order_notes ) ), false, true ); } $order->update_taxes(); $order->calculate_totals( false ); // Inform other plugins that the items have been saved. do_action( 'woocommerce_saved_order_items', $order_id, $items ); } /** * Get HTML for some action buttons. Used in list tables. * * @since 3.3.0 * @param array $actions Actions to output. * @return string */ function wc_render_action_buttons( $actions ) { $actions_html = ''; foreach ( $actions as $action ) { if ( isset( $action['group'] ) ) { $actions_html .= '<div class="wc-action-button-group"><label>' . $action['group'] . '</label> <span class="wc-action-button-group__items">' . wc_render_action_buttons( $action['actions'] ) . '</span></div>'; } elseif ( isset( $action['action'], $action['url'], $action['name'] ) ) { $actions_html .= sprintf( '<a class="button wc-action-button wc-action-button-%1$s %1$s" href="%2$s" aria-label="%3$s" title="%3$s">%4$s</a>', esc_attr( $action['action'] ), esc_url( $action['url'] ), esc_attr( isset( $action['title'] ) ? $action['title'] : $action['name'] ), esc_html( $action['name'] ) ); } } return $actions_html; } /** * Shows a notice if variations are missing prices. * * @since 3.6.0 * @param WC_Product $product_object Product object. */ function wc_render_invalid_variation_notice( $product_object ) { global $wpdb; // Give ability for extensions to hide this notice. if ( ! apply_filters( 'woocommerce_show_invalid_variations_notice', true, $product_object ) ) { return; } $variation_ids = $product_object ? $product_object->get_children() : array(); if ( empty( $variation_ids ) ) { return; } $variation_count = count( $variation_ids ); // Check if a variation exists without pricing data. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $invalid_variation_count = $wpdb->get_var( " SELECT count(post_id) FROM {$wpdb->postmeta} WHERE post_id in (" . implode( ',', array_map( 'absint', $variation_ids ) ) . ") AND meta_key='_price' AND meta_value >= 0 AND meta_value != '' " ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared if ( 0 < ( $variation_count - $invalid_variation_count ) ) { ?> <div id="message" class="inline notice woocommerce-message woocommerce-notice-invalid-variation"> <p> <?php echo wp_kses_post( sprintf( /* Translators: %d variation count. */ _n( '%d variation does not have a price.', '%d variations do not have prices.', ( $variation_count - $invalid_variation_count ), 'woocommerce' ), ( $variation_count - $invalid_variation_count ) ) . ' ' . __( 'Variations (and their attributes) that do not have prices will not be shown in your store.', 'woocommerce' ) ); ?> </p> </div> <?php } } /** * Get current admin page URL. * * Returns an empty string if it cannot generate a URL. * * @internal * @since 4.4.0 * @return string */ function wc_get_current_admin_url() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $uri = preg_replace( '|^.*/wp-admin/|i', '', $uri ); if ( ! $uri ) { return ''; } return remove_query_arg( array( '_wpnonce', '_wc_notice_nonce', 'wc_db_update', 'wc_db_update_nonce', 'wc-hide-notice' ), admin_url( $uri ) ); } includes/admin/class-wc-admin-settings.php 0000644 00000103074 15132754524 0014630 0 ustar 00 <?php /** * WooCommerce Admin Settings Class * * @package WooCommerce\Admin * @version 3.4.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Admin_Settings', false ) ) : /** * WC_Admin_Settings Class. */ class WC_Admin_Settings { /** * Setting pages. * * @var array */ private static $settings = array(); /** * Error messages. * * @var array */ private static $errors = array(); /** * Update messages. * * @var array */ private static $messages = array(); /** * Include the settings page classes. */ public static function get_settings_pages() { if ( empty( self::$settings ) ) { $settings = array(); include_once dirname( __FILE__ ) . '/settings/class-wc-settings-page.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-general.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-products.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-tax.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-shipping.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-payment-gateways.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-accounts.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-emails.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-integrations.php'; $settings[] = include __DIR__ . '/settings/class-wc-settings-advanced.php'; self::$settings = apply_filters( 'woocommerce_get_settings_pages', $settings ); } return self::$settings; } /** * Save the settings. */ public static function save() { global $current_tab; check_admin_referer( 'woocommerce-settings' ); // Trigger actions. do_action( 'woocommerce_settings_save_' . $current_tab ); do_action( 'woocommerce_update_options_' . $current_tab ); do_action( 'woocommerce_update_options' ); self::add_message( __( 'Your settings have been saved.', 'woocommerce' ) ); self::check_download_folder_protection(); // Clear any unwanted data and flush rules. update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' ); WC()->query->init_query_vars(); WC()->query->add_endpoints(); do_action( 'woocommerce_settings_saved' ); } /** * Add a message. * * @param string $text Message. */ public static function add_message( $text ) { self::$messages[] = $text; } /** * Add an error. * * @param string $text Message. */ public static function add_error( $text ) { self::$errors[] = $text; } /** * Output messages + errors. */ public static function show_messages() { if ( count( self::$errors ) > 0 ) { foreach ( self::$errors as $error ) { echo '<div id="message" class="error inline"><p><strong>' . esc_html( $error ) . '</strong></p></div>'; } } elseif ( count( self::$messages ) > 0 ) { foreach ( self::$messages as $message ) { echo '<div id="message" class="updated inline"><p><strong>' . esc_html( $message ) . '</strong></p></div>'; } } } /** * Settings page. * * Handles the display of the main woocommerce settings page in admin. */ public static function output() { global $current_section, $current_tab; $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; do_action( 'woocommerce_settings_start' ); wp_enqueue_script( 'woocommerce_settings', WC()->plugin_url() . '/assets/js/admin/settings' . $suffix . '.js', array( 'jquery', 'wp-util', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'iris', 'selectWoo' ), WC()->version, true ); wp_localize_script( 'woocommerce_settings', 'woocommerce_settings_params', array( 'i18n_nav_warning' => __( 'The changes you made will be lost if you navigate away from this page.', 'woocommerce' ), 'i18n_moved_up' => __( 'Item moved up', 'woocommerce' ), 'i18n_moved_down' => __( 'Item moved down', 'woocommerce' ), 'i18n_no_specific_countries_selected' => __( 'Selecting no country / region to sell to prevents from completing the checkout. Continue anyway?', 'woocommerce' ), ) ); // Get tabs for the settings page. $tabs = apply_filters( 'woocommerce_settings_tabs_array', array() ); include dirname( __FILE__ ) . '/views/html-admin-settings.php'; } /** * Get a setting from the settings API. * * @param string $option_name Option name. * @param mixed $default Default value. * @return mixed */ public static function get_option( $option_name, $default = '' ) { if ( ! $option_name ) { return $default; } // Array value. if ( strstr( $option_name, '[' ) ) { parse_str( $option_name, $option_array ); // Option name is first key. $option_name = current( array_keys( $option_array ) ); // Get value. $option_values = get_option( $option_name, '' ); $key = key( $option_array[ $option_name ] ); if ( isset( $option_values[ $key ] ) ) { $option_value = $option_values[ $key ]; } else { $option_value = null; } } else { // Single value. $option_value = get_option( $option_name, null ); } if ( is_array( $option_value ) ) { $option_value = wp_unslash( $option_value ); } elseif ( ! is_null( $option_value ) ) { $option_value = stripslashes( $option_value ); } return ( null === $option_value ) ? $default : $option_value; } /** * Output admin fields. * * Loops through the woocommerce options array and outputs each field. * * @param array[] $options Opens array to output. */ public static function output_fields( $options ) { foreach ( $options as $value ) { if ( ! isset( $value['type'] ) ) { continue; } if ( ! isset( $value['id'] ) ) { $value['id'] = ''; } if ( ! isset( $value['title'] ) ) { $value['title'] = isset( $value['name'] ) ? $value['name'] : ''; } if ( ! isset( $value['class'] ) ) { $value['class'] = ''; } if ( ! isset( $value['css'] ) ) { $value['css'] = ''; } if ( ! isset( $value['default'] ) ) { $value['default'] = ''; } if ( ! isset( $value['desc'] ) ) { $value['desc'] = ''; } if ( ! isset( $value['desc_tip'] ) ) { $value['desc_tip'] = false; } if ( ! isset( $value['placeholder'] ) ) { $value['placeholder'] = ''; } if ( ! isset( $value['suffix'] ) ) { $value['suffix'] = ''; } if ( ! isset( $value['value'] ) ) { $value['value'] = self::get_option( $value['id'], $value['default'] ); } // Custom attribute handling. $custom_attributes = array(); if ( ! empty( $value['custom_attributes'] ) && is_array( $value['custom_attributes'] ) ) { foreach ( $value['custom_attributes'] as $attribute => $attribute_value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; } } // Description handling. $field_description = self::get_field_description( $value ); $description = $field_description['description']; $tooltip_html = $field_description['tooltip_html']; // Switch based on type. switch ( $value['type'] ) { // Section Titles. case 'title': if ( ! empty( $value['title'] ) ) { echo '<h2>' . esc_html( $value['title'] ) . '</h2>'; } if ( ! empty( $value['desc'] ) ) { echo '<div id="' . esc_attr( sanitize_title( $value['id'] ) ) . '-description">'; echo wp_kses_post( wpautop( wptexturize( $value['desc'] ) ) ); echo '</div>'; } echo '<table class="form-table">' . "\n\n"; if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) ); } break; // Section Ends. case 'sectionend': if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_end' ); } echo '</table>'; if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_after' ); } break; // Standard text inputs and subtypes like 'number'. case 'text': case 'password': case 'datetime': case 'datetime-local': case 'date': case 'month': case 'time': case 'week': case 'number': case 'email': case 'url': case 'tel': $option_value = $value['value']; ?><tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>"> <input name="<?php echo esc_attr( $value['id'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" type="<?php echo esc_attr( $value['type'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" value="<?php echo esc_attr( $option_value ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> /><?php echo esc_html( $value['suffix'] ); ?> <?php echo $description; // WPCS: XSS ok. ?> </td> </tr> <?php break; // Color picker. case 'color': $option_value = $value['value']; ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">‎ <span class="colorpickpreview" style="background: <?php echo esc_attr( $option_value ); ?>"> </span> <input name="<?php echo esc_attr( $value['id'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" type="text" dir="ltr" style="<?php echo esc_attr( $value['css'] ); ?>" value="<?php echo esc_attr( $option_value ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>colorpick" placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> />‎ <?php echo $description; // WPCS: XSS ok. ?> <div id="colorPickerDiv_<?php echo esc_attr( $value['id'] ); ?>" class="colorpickdiv" style="z-index: 100;background:#eee;border:1px solid #ccc;position:absolute;display:none;"></div> </td> </tr> <?php break; // Textarea. case 'textarea': $option_value = $value['value']; ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>"> <?php echo $description; // WPCS: XSS ok. ?> <textarea name="<?php echo esc_attr( $value['id'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> ><?php echo esc_textarea( $option_value ); // WPCS: XSS ok. ?></textarea> </td> </tr> <?php break; // Select boxes. case 'select': case 'multiselect': $option_value = $value['value']; ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>"> <select name="<?php echo esc_attr( $value['id'] ); ?><?php echo ( 'multiselect' === $value['type'] ) ? '[]' : ''; ?>" id="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> <?php echo 'multiselect' === $value['type'] ? 'multiple="multiple"' : ''; ?> > <?php foreach ( $value['options'] as $key => $val ) { ?> <option value="<?php echo esc_attr( $key ); ?>" <?php if ( is_array( $option_value ) ) { selected( in_array( (string) $key, $option_value, true ), true ); } else { selected( $option_value, (string) $key ); } ?> ><?php echo esc_html( $val ); ?></option> <?php } ?> </select> <?php echo $description; // WPCS: XSS ok. ?> </td> </tr> <?php break; // Radio inputs. case 'radio': $option_value = $value['value']; ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>"> <fieldset> <?php echo $description; // WPCS: XSS ok. ?> <ul> <?php foreach ( $value['options'] as $key => $val ) { ?> <li> <label><input name="<?php echo esc_attr( $value['id'] ); ?>" value="<?php echo esc_attr( $key ); ?>" type="radio" style="<?php echo esc_attr( $value['css'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> <?php checked( $key, $option_value ); ?> /> <?php echo esc_html( $val ); ?></label> </li> <?php } ?> </ul> </fieldset> </td> </tr> <?php break; // Checkbox input. case 'checkbox': $option_value = $value['value']; $visibility_class = array(); if ( ! isset( $value['hide_if_checked'] ) ) { $value['hide_if_checked'] = false; } if ( ! isset( $value['show_if_checked'] ) ) { $value['show_if_checked'] = false; } if ( 'yes' === $value['hide_if_checked'] || 'yes' === $value['show_if_checked'] ) { $visibility_class[] = 'hidden_option'; } if ( 'option' === $value['hide_if_checked'] ) { $visibility_class[] = 'hide_options_if_checked'; } if ( 'option' === $value['show_if_checked'] ) { $visibility_class[] = 'show_options_if_checked'; } if ( ! isset( $value['checkboxgroup'] ) || 'start' === $value['checkboxgroup'] ) { ?> <tr valign="top" class="<?php echo esc_attr( implode( ' ', $visibility_class ) ); ?>"> <th scope="row" class="titledesc"><?php echo esc_html( $value['title'] ); ?></th> <td class="forminp forminp-checkbox"> <fieldset> <?php } else { ?> <fieldset class="<?php echo esc_attr( implode( ' ', $visibility_class ) ); ?>"> <?php } if ( ! empty( $value['title'] ) ) { ?> <legend class="screen-reader-text"><span><?php echo esc_html( $value['title'] ); ?></span></legend> <?php } ?> <label for="<?php echo esc_attr( $value['id'] ); ?>"> <input name="<?php echo esc_attr( $value['id'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" type="checkbox" class="<?php echo esc_attr( isset( $value['class'] ) ? $value['class'] : '' ); ?>" value="1" <?php checked( $option_value, 'yes' ); ?> <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> /> <?php echo $description; // WPCS: XSS ok. ?> </label> <?php echo $tooltip_html; // WPCS: XSS ok. ?> <?php if ( ! isset( $value['checkboxgroup'] ) || 'end' === $value['checkboxgroup'] ) { ?> </fieldset> </td> </tr> <?php } else { ?> </fieldset> <?php } break; // Image width settings. @todo deprecate and remove in 4.0. No longer needed by core. case 'image_width': $image_size = str_replace( '_image_size', '', $value['id'] ); $size = wc_get_image_size( $image_size ); $width = isset( $size['width'] ) ? $size['width'] : $value['default']['width']; $height = isset( $size['height'] ) ? $size['height'] : $value['default']['height']; $crop = isset( $size['crop'] ) ? $size['crop'] : $value['default']['crop']; $disabled_attr = ''; $disabled_message = ''; if ( has_filter( 'woocommerce_get_image_size_' . $image_size ) ) { $disabled_attr = 'disabled="disabled"'; $disabled_message = '<p><small>' . esc_html__( 'The settings of this image size have been disabled because its values are being overwritten by a filter.', 'woocommerce' ) . '</small></p>'; } ?> <tr valign="top"> <th scope="row" class="titledesc"> <label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html . $disabled_message; // WPCS: XSS ok. ?></label> </th> <td class="forminp image_width_settings"> <input name="<?php echo esc_attr( $value['id'] ); ?>[width]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-width" type="text" size="3" value="<?php echo esc_attr( $width ); ?>" /> × <input name="<?php echo esc_attr( $value['id'] ); ?>[height]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-height" type="text" size="3" value="<?php echo esc_attr( $height ); ?>" />px <label><input name="<?php echo esc_attr( $value['id'] ); ?>[crop]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-crop" type="checkbox" value="1" <?php checked( 1, $crop ); ?> /> <?php esc_html_e( 'Hard crop?', 'woocommerce' ); ?></label> </td> </tr> <?php break; // Single page selects. case 'single_select_page': $args = array( 'name' => $value['id'], 'id' => $value['id'], 'sort_column' => 'menu_order', 'sort_order' => 'ASC', 'show_option_none' => ' ', 'class' => $value['class'], 'echo' => false, 'selected' => absint( $value['value'] ), 'post_status' => 'publish,private,draft', ); if ( isset( $value['args'] ) ) { $args = wp_parse_args( $value['args'], $args ); } ?> <tr valign="top" class="single_select_page"> <th scope="row" class="titledesc"> <label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <?php echo str_replace( ' id=', " data-placeholder='" . esc_attr__( 'Select a page…', 'woocommerce' ) . "' style='" . $value['css'] . "' class='" . $value['class'] . "' id=", wp_dropdown_pages( $args ) ); // WPCS: XSS ok. ?> <?php echo $description; // WPCS: XSS ok. ?> </td> </tr> <?php break; case 'single_select_page_with_search': $option_value = $value['value']; $page = get_post( $option_value ); if ( ! is_null( $page ) ) { $page = get_post( $option_value ); $option_display_name = sprintf( /* translators: 1: page name 2: page ID */ __( '%1$s (ID: %2$s)', 'woocommerce' ), $page->post_title, $option_value ); } ?> <tr valign="top" class="single_select_page"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></label> </th> <td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>"> <select name="<?php echo esc_attr( $value['id'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" <?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> data-placeholder="<?php esc_attr_e( 'Search for a page…', 'woocommerce' ); ?>" data-allow_clear="true" data-exclude="<?php echo wc_esc_json( wp_json_encode( $value['args']['exclude'] ) ); ?>" > <option value=""></option> <?php if ( ! is_null( $page ) ) { ?> <option value="<?php echo esc_attr( $option_value ); ?>" selected="selected"> <?php echo wp_strip_all_tags( $option_display_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </option> <?php } ?> </select> <?php echo $description; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php break; // Single country selects. case 'single_select_country': $country_setting = (string) $value['value']; if ( strstr( $country_setting, ':' ) ) { $country_setting = explode( ':', $country_setting ); $country = current( $country_setting ); $state = end( $country_setting ); } else { $country = $country_setting; $state = '*'; } ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp"><select name="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" data-placeholder="<?php esc_attr_e( 'Choose a country / region…', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="wc-enhanced-select"> <?php WC()->countries->country_dropdown_options( $country, $state ); ?> </select> <?php echo $description; // WPCS: XSS ok. ?> </td> </tr> <?php break; // Country multiselects. case 'multi_select_countries': $selections = (array) $value['value']; if ( ! empty( $value['options'] ) ) { $countries = $value['options']; } else { $countries = WC()->countries->countries; } asort( $countries ); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <select multiple="multiple" name="<?php echo esc_attr( $value['id'] ); ?>[]" style="width:350px" data-placeholder="<?php esc_attr_e( 'Choose countries / regions…', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="wc-enhanced-select"> <?php if ( ! empty( $countries ) ) { foreach ( $countries as $key => $val ) { echo '<option value="' . esc_attr( $key ) . '"' . wc_selected( $key, $selections ) . '>' . esc_html( $val ) . '</option>'; // WPCS: XSS ok. } } ?> </select> <?php echo ( $description ) ? $description : ''; // WPCS: XSS ok. ?> <br /><a class="select_all button" href="#"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a> <a class="select_none button" href="#"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></a> </td> </tr> <?php break; // Days/months/years selector. case 'relative_date_selector': $periods = array( 'days' => __( 'Day(s)', 'woocommerce' ), 'weeks' => __( 'Week(s)', 'woocommerce' ), 'months' => __( 'Month(s)', 'woocommerce' ), 'years' => __( 'Year(s)', 'woocommerce' ), ); $option_value = wc_parse_relative_date_option( $value['value'] ); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label> </th> <td class="forminp"> <input name="<?php echo esc_attr( $value['id'] ); ?>[number]" id="<?php echo esc_attr( $value['id'] ); ?>" type="number" style="width: 80px;" value="<?php echo esc_attr( $option_value['number'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>" placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>" step="1" min="1" <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> /> <select name="<?php echo esc_attr( $value['id'] ); ?>[unit]" style="width: auto;"> <?php foreach ( $periods as $value => $label ) { echo '<option value="' . esc_attr( $value ) . '"' . selected( $option_value['unit'], $value, false ) . '>' . esc_html( $label ) . '</option>'; } ?> </select> <?php echo ( $description ) ? $description : ''; // WPCS: XSS ok. ?> </td> </tr> <?php break; // Default: run an action. default: do_action( 'woocommerce_admin_field_' . $value['type'], $value ); break; } } } /** * Helper function to get the formatted description and tip HTML for a * given form field. Plugins can call this when implementing their own custom * settings types. * * @param array $value The form field value array. * @return array The description and tip as a 2 element array. */ public static function get_field_description( $value ) { $description = ''; $tooltip_html = ''; if ( true === $value['desc_tip'] ) { $tooltip_html = $value['desc']; } elseif ( ! empty( $value['desc_tip'] ) ) { $description = $value['desc']; $tooltip_html = $value['desc_tip']; } elseif ( ! empty( $value['desc'] ) ) { $description = $value['desc']; } if ( $description && in_array( $value['type'], array( 'textarea', 'radio' ), true ) ) { $description = '<p style="margin-top:0">' . wp_kses_post( $description ) . '</p>'; } elseif ( $description && in_array( $value['type'], array( 'checkbox' ), true ) ) { $description = wp_kses_post( $description ); } elseif ( $description ) { $description = '<p class="description">' . wp_kses_post( $description ) . '</p>'; } if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) { $tooltip_html = '<p class="description">' . $tooltip_html . '</p>'; } elseif ( $tooltip_html ) { $tooltip_html = wc_help_tip( $tooltip_html ); } return array( 'description' => $description, 'tooltip_html' => $tooltip_html, ); } /** * Save admin fields. * * Loops through the woocommerce options array and outputs each field. * * @param array $options Options array to output. * @param array $data Optional. Data to use for saving. Defaults to $_POST. * @return bool */ public static function save_fields( $options, $data = null ) { if ( is_null( $data ) ) { $data = $_POST; // WPCS: input var okay, CSRF ok. } if ( empty( $data ) ) { return false; } // Options to update will be stored here and saved later. $update_options = array(); $autoload_options = array(); // Loop options and get values to save. foreach ( $options as $option ) { if ( ! isset( $option['id'] ) || ! isset( $option['type'] ) || ( isset( $option['is_option'] ) && false === $option['is_option'] ) ) { continue; } // Get posted value. if ( strstr( $option['id'], '[' ) ) { parse_str( $option['id'], $option_name_array ); $option_name = current( array_keys( $option_name_array ) ); $setting_name = key( $option_name_array[ $option_name ] ); $raw_value = isset( $data[ $option_name ][ $setting_name ] ) ? wp_unslash( $data[ $option_name ][ $setting_name ] ) : null; } else { $option_name = $option['id']; $setting_name = ''; $raw_value = isset( $data[ $option['id'] ] ) ? wp_unslash( $data[ $option['id'] ] ) : null; } // Format the value based on option type. switch ( $option['type'] ) { case 'checkbox': $value = '1' === $raw_value || 'yes' === $raw_value ? 'yes' : 'no'; break; case 'textarea': $value = wp_kses_post( trim( $raw_value ) ); break; case 'multiselect': case 'multi_select_countries': $value = array_filter( array_map( 'wc_clean', (array) $raw_value ) ); break; case 'image_width': $value = array(); if ( isset( $raw_value['width'] ) ) { $value['width'] = wc_clean( $raw_value['width'] ); $value['height'] = wc_clean( $raw_value['height'] ); $value['crop'] = isset( $raw_value['crop'] ) ? 1 : 0; } else { $value['width'] = $option['default']['width']; $value['height'] = $option['default']['height']; $value['crop'] = $option['default']['crop']; } break; case 'select': $allowed_values = empty( $option['options'] ) ? array() : array_map( 'strval', array_keys( $option['options'] ) ); if ( empty( $option['default'] ) && empty( $allowed_values ) ) { $value = null; break; } $default = ( empty( $option['default'] ) ? $allowed_values[0] : $option['default'] ); $value = in_array( $raw_value, $allowed_values, true ) ? $raw_value : $default; break; case 'relative_date_selector': $value = wc_parse_relative_date_option( $raw_value ); break; default: $value = wc_clean( $raw_value ); break; } /** * Fire an action when a certain 'type' of field is being saved. * * @deprecated 2.4.0 - doesn't allow manipulation of values! */ if ( has_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ) ) ) { wc_deprecated_function( 'The woocommerce_update_option_X action', '2.4.0', 'woocommerce_admin_settings_sanitize_option filter' ); do_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ), $option ); continue; } /** * Sanitize the value of an option. * * @since 2.4.0 */ $value = apply_filters( 'woocommerce_admin_settings_sanitize_option', $value, $option, $raw_value ); /** * Sanitize the value of an option by option name. * * @since 2.4.0 */ $value = apply_filters( "woocommerce_admin_settings_sanitize_option_$option_name", $value, $option, $raw_value ); if ( is_null( $value ) ) { continue; } // Check if option is an array and handle that differently to single values. if ( $option_name && $setting_name ) { if ( ! isset( $update_options[ $option_name ] ) ) { $update_options[ $option_name ] = get_option( $option_name, array() ); } if ( ! is_array( $update_options[ $option_name ] ) ) { $update_options[ $option_name ] = array(); } $update_options[ $option_name ][ $setting_name ] = $value; } else { $update_options[ $option_name ] = $value; } $autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true; /** * Fire an action before saved. * * @deprecated 2.4.0 - doesn't allow manipulation of values! */ do_action( 'woocommerce_update_option', $option ); } // Save all options in our array. foreach ( $update_options as $name => $value ) { update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' ); } return true; } /** * Checks which method we're using to serve downloads. * * If using force or x-sendfile, this ensures the .htaccess is in place. */ public static function check_download_folder_protection() { $upload_dir = wp_get_upload_dir(); $downloads_path = $upload_dir['basedir'] . '/woocommerce_uploads'; $download_method = get_option( 'woocommerce_file_download_method' ); $file_path = $downloads_path . '/.htaccess'; $file_content = 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all'; $create = false; if ( wp_mkdir_p( $downloads_path ) && ! file_exists( $file_path ) ) { $create = true; } else { $current_content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( $current_content !== $file_content ) { unlink( $file_path ); $create = true; } } if ( $create ) { $file_handle = @fopen( $file_path, 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen if ( $file_handle ) { fwrite( $file_handle, $file_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose } } } } endif; includes/admin/class-wc-admin-api-keys-table-list.php 0000644 00000016110 15132754524 0016542 0 ustar 00 <?php /** * WooCommerce API Keys Table List * * @package WooCommerce\Admin * @version 2.4.0 */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * API Keys table list class. */ class WC_Admin_API_Keys_Table_List extends WP_List_Table { /** * Initialize the API key table list. */ public function __construct() { parent::__construct( array( 'singular' => 'key', 'plural' => 'keys', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No keys found.', 'woocommerce' ); } /** * Get list columns. * * @return array */ public function get_columns() { return array( 'cb' => '<input type="checkbox" />', 'title' => __( 'Description', 'woocommerce' ), 'truncated_key' => __( 'Consumer key ending in', 'woocommerce' ), 'user' => __( 'User', 'woocommerce' ), 'permissions' => __( 'Permissions', 'woocommerce' ), 'last_access' => __( 'Last access', 'woocommerce' ), ); } /** * Column cb. * * @param array $key Key data. * @return string */ public function column_cb( $key ) { return sprintf( '<input type="checkbox" name="key[]" value="%1$s" />', $key['key_id'] ); } /** * Return title column. * * @param array $key Key data. * @return string */ public function column_title( $key ) { $url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys&edit-key=' . $key['key_id'] ); $user_id = intval( $key['user_id'] ); // Check if current user can edit other users or if it's the same user. $can_edit = current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id; $output = '<strong>'; if ( $can_edit ) { $output .= '<a href="' . esc_url( $url ) . '" class="row-title">'; } if ( empty( $key['description'] ) ) { $output .= esc_html__( 'API key', 'woocommerce' ); } else { $output .= esc_html( $key['description'] ); } if ( $can_edit ) { $output .= '</a>'; } $output .= '</strong>'; // Get actions. $actions = array( /* translators: %s: API key ID. */ 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $key['key_id'] ), ); if ( $can_edit ) { $actions['edit'] = '<a href="' . esc_url( $url ) . '">' . __( 'View/Edit', 'woocommerce' ) . '</a>'; $actions['trash'] = '<a class="submitdelete" aria-label="' . esc_attr__( 'Revoke API key', 'woocommerce' ) . '" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key['key_id'], ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ), 'revoke' ) ) . '">' . esc_html__( 'Revoke', 'woocommerce' ) . '</a>'; } $row_actions = array(); foreach ( $actions as $action => $link ) { $row_actions[] = '<span class="' . esc_attr( $action ) . '">' . $link . '</span>'; } $output .= '<div class="row-actions">' . implode( ' | ', $row_actions ) . '</div>'; return $output; } /** * Return truncated consumer key column. * * @param array $key Key data. * @return string */ public function column_truncated_key( $key ) { return '<code>…' . esc_html( $key['truncated_key'] ) . '</code>'; } /** * Return user column. * * @param array $key Key data. * @return string */ public function column_user( $key ) { $user = get_user_by( 'id', $key['user_id'] ); if ( ! $user ) { return ''; } if ( current_user_can( 'edit_user', $user->ID ) ) { return '<a href="' . esc_url( add_query_arg( array( 'user_id' => $user->ID ), admin_url( 'user-edit.php' ) ) ) . '">' . esc_html( $user->display_name ) . '</a>'; } return esc_html( $user->display_name ); } /** * Return permissions column. * * @param array $key Key data. * @return string */ public function column_permissions( $key ) { $permission_key = $key['permissions']; $permissions = array( 'read' => __( 'Read', 'woocommerce' ), 'write' => __( 'Write', 'woocommerce' ), 'read_write' => __( 'Read/Write', 'woocommerce' ), ); if ( isset( $permissions[ $permission_key ] ) ) { return esc_html( $permissions[ $permission_key ] ); } else { return ''; } } /** * Return last access column. * * @param array $key Key data. * @return string */ public function column_last_access( $key ) { if ( ! empty( $key['last_access'] ) ) { /* translators: 1: last access date 2: last access time */ $date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key['last_access'] ) ) ); return apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key['last_access'] ); } return __( 'Unknown', 'woocommerce' ); } /** * Get bulk actions. * * @return array */ protected function get_bulk_actions() { if ( ! current_user_can( 'remove_users' ) ) { return array(); } return array( 'revoke' => __( 'Revoke', 'woocommerce' ), ); } /** * Search box. * * @param string $text Button text. * @param string $input_id Input ID. */ public function search_box( $text, $input_id ) { if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. return; } $input_id = $input_id . '-search-input'; $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. echo '<p class="search-box">'; echo '<label class="screen-reader-text" for="' . esc_attr( $input_id ) . '">' . esc_html( $text ) . ':</label>'; echo '<input type="search" id="' . esc_attr( $input_id ) . '" name="s" value="' . esc_attr( $search_query ) . '" />'; submit_button( $text, '', '', false, array( 'id' => 'search-submit', ) ); echo '</p>'; } /** * Prepare table list items. */ public function prepare_items() { global $wpdb; $per_page = $this->get_items_per_page( 'woocommerce_keys_per_page' ); $current_page = $this->get_pagenum(); if ( 1 < $current_page ) { $offset = $per_page * ( $current_page - 1 ); } else { $offset = 0; } $search = ''; if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. $search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) ) . "%' "; // WPCS: input var okay, CSRF ok. } // Get the API keys. $keys = $wpdb->get_results( "SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search}" . $wpdb->prepare( 'ORDER BY key_id DESC LIMIT %d OFFSET %d;', $per_page, $offset ), ARRAY_A ); // WPCS: unprepared SQL ok. $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search};" ); // WPCS: unprepared SQL ok. $this->items = $keys; // Set the pagination. $this->set_pagination_args( array( 'total_items' => $count, 'per_page' => $per_page, 'total_pages' => ceil( $count / $per_page ), ) ); } } includes/admin/class-wc-admin-webhooks-table-list.php 0000644 00000021644 15132754524 0016651 0 ustar 00 <?php /** * WooCommerce Webhooks Table List * * @package WooCommerce\Admin * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * Webooks table list class. */ class WC_Admin_Webhooks_Table_List extends WP_List_Table { /** * Initialize the webhook table list. */ public function __construct() { parent::__construct( array( 'singular' => 'webhook', 'plural' => 'webhooks', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No webhooks found.', 'woocommerce' ); } /** * Get list columns. * * @return array */ public function get_columns() { return array( 'cb' => '<input type="checkbox" />', 'title' => __( 'Name', 'woocommerce' ), 'status' => __( 'Status', 'woocommerce' ), 'topic' => __( 'Topic', 'woocommerce' ), 'delivery_url' => __( 'Delivery URL', 'woocommerce' ), ); } /** * Column cb. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_cb( $webhook ) { return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', $this->_args['singular'], $webhook->get_id() ); } /** * Return title column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_title( $webhook ) { $edit_link = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() ); $output = ''; // Title. $output .= '<strong><a href="' . esc_url( $edit_link ) . '" class="row-title">' . esc_html( $webhook->get_name() ) . '</a></strong>'; // Get actions. $actions = array( /* translators: %s: webhook ID. */ 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ), 'edit' => '<a href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Edit', 'woocommerce' ) . '</a>', /* translators: %s: webhook name */ 'delete' => '<a class="submitdelete" aria-label="' . esc_attr( sprintf( __( 'Delete "%s" permanently', 'woocommerce' ), $webhook->get_name() ) ) . '" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'delete' => $webhook->get_id(), ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) ), 'delete-webhook' ) ) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '</a>', ); $actions = apply_filters( 'webhook_row_actions', $actions, $webhook ); $row_actions = array(); foreach ( $actions as $action => $link ) { $row_actions[] = '<span class="' . esc_attr( $action ) . '">' . $link . '</span>'; } $output .= '<div class="row-actions">' . implode( ' | ', $row_actions ) . '</div>'; return $output; } /** * Return status column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_status( $webhook ) { return $webhook->get_i18n_status(); } /** * Return topic column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_topic( $webhook ) { return $webhook->get_topic(); } /** * Return delivery URL column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_delivery_url( $webhook ) { return $webhook->get_delivery_url(); } /** * Get the status label for webhooks. * * @param string $status_name Status name. * @param int $amount Amount of webhooks. * @return array */ private function get_status_label( $status_name, $amount ) { $statuses = wc_get_webhook_statuses(); if ( isset( $statuses[ $status_name ] ) ) { return array( 'singular' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $statuses[ $status_name ] ), $amount ), 'plural' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $statuses[ $status_name ] ), $amount ), 'context' => '', 'domain' => 'woocommerce', ); } return array( 'singular' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $status_name ), $amount ), 'plural' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $status_name ), $amount ), 'context' => '', 'domain' => 'woocommerce', ); } /** * Table list views. * * @return array */ protected function get_views() { $status_links = array(); $data_store = WC_Data_Store::load( 'webhook' ); $num_webhooks = $data_store->get_count_webhooks_by_status(); $total_webhooks = array_sum( (array) $num_webhooks ); $statuses = array_keys( wc_get_webhook_statuses() ); $class = empty( $_REQUEST['status'] ) ? ' class="current"' : ''; // WPCS: input var okay. CSRF ok. /* translators: %s: count */ $status_links['all'] = "<a href='admin.php?page=wc-settings&tab=advanced&section=webhooks'$class>" . sprintf( _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . '</a>'; foreach ( $statuses as $status_name ) { $class = ''; if ( empty( $num_webhooks[ $status_name ] ) ) { continue; } if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { // WPCS: input var okay, CSRF ok. $class = ' class="current"'; } $label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] ); $status_links[ $status_name ] = "<a href='admin.php?page=wc-settings&tab=advanced&section=webhooks&status=$status_name'$class>" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . '</a>'; } return $status_links; } /** * Get bulk actions. * * @return array */ protected function get_bulk_actions() { return array( 'delete' => __( 'Delete permanently', 'woocommerce' ), ); } /** * Process bulk actions. */ public function process_bulk_action() { $action = $this->current_action(); $webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) ); } if ( 'delete' === $action ) { WC_Admin_Webhooks::bulk_delete( $webhooks ); } } /** * Generate the table navigation above or below the table. * Included to remove extra nonce input. * * @param string $which The location of the extra table nav markup: 'top' or 'bottom'. */ protected function display_tablenav( $which ) { echo '<div class="tablenav ' . esc_attr( $which ) . '">'; if ( $this->has_items() ) { echo '<div class="alignleft actions bulkactions">'; $this->bulk_actions( $which ); echo '</div>'; } $this->extra_tablenav( $which ); $this->pagination( $which ); echo '<br class="clear" />'; echo '</div>'; } /** * Search box. * * @param string $text Button text. * @param string $input_id Input ID. */ public function search_box( $text, $input_id ) { if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. return; } $input_id = $input_id . '-search-input'; $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. echo '<p class="search-box">'; echo '<label class="screen-reader-text" for="' . esc_attr( $input_id ) . '">' . esc_html( $text ) . ':</label>'; echo '<input type="search" id="' . esc_attr( $input_id ) . '" name="s" value="' . esc_attr( $search_query ) . '" />'; submit_button( $text, '', '', false, array( 'id' => 'search-submit', ) ); echo '</p>'; } /** * Prepare table list items. */ public function prepare_items() { $per_page = $this->get_items_per_page( 'woocommerce_webhooks_per_page' ); $current_page = $this->get_pagenum(); // Query args. $args = array( 'limit' => $per_page, 'offset' => $per_page * ( $current_page - 1 ), ); // Handle the status query. if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok. $args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok. } if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok. } $args['paginate'] = true; // Get the webhooks. $data_store = WC_Data_Store::load( 'webhook' ); $webhooks = $data_store->search_webhooks( $args ); $this->items = array_map( 'wc_get_webhook', $webhooks->webhooks ); // Set the pagination. $this->set_pagination_args( array( 'total_items' => $webhooks->total, 'per_page' => $per_page, 'total_pages' => $webhooks->max_num_pages, ) ); } } includes/admin/class-wc-admin-customize.php 0000644 00000005004 15132754524 0015004 0 ustar 00 <?php /** * Setup customize items. * * @package WooCommerce\Admin\Customize * @version 3.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Admin_Customize', false ) ) : /** * WC_Admin_Customize Class. */ class WC_Admin_Customize { /** * Initialize customize actions. */ public function __construct() { // Include custom items to customizer nav menu settings. add_filter( 'customize_nav_menu_available_item_types', array( $this, 'register_customize_nav_menu_item_types' ) ); add_filter( 'customize_nav_menu_available_items', array( $this, 'register_customize_nav_menu_items' ), 10, 4 ); } /** * Register customize new nav menu item types. * This will register WooCommerce account endpoints as a nav menu item type. * * @since 3.1.0 * @param array $item_types Menu item types. * @return array */ public function register_customize_nav_menu_item_types( $item_types ) { $item_types[] = array( 'title' => __( 'WooCommerce Endpoints', 'woocommerce' ), 'type_label' => __( 'WooCommerce Endpoint', 'woocommerce' ), 'type' => 'woocommerce_nav', 'object' => 'woocommerce_endpoint', ); return $item_types; } /** * Register account endpoints to customize nav menu items. * * @since 3.1.0 * @param array $items List of nav menu items. * @param string $type Nav menu type. * @param string $object Nav menu object. * @param integer $page Page number. * @return array */ public function register_customize_nav_menu_items( $items = array(), $type = '', $object = '', $page = 0 ) { if ( 'woocommerce_endpoint' !== $object ) { return $items; } // Don't allow pagination since all items are loaded at once. if ( 0 < $page ) { return $items; } // Get items from account menu. $endpoints = wc_get_account_menu_items(); // Remove dashboard item. if ( isset( $endpoints['dashboard'] ) ) { unset( $endpoints['dashboard'] ); } // Include missing lost password. $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); foreach ( $endpoints as $endpoint => $title ) { $items[] = array( 'id' => $endpoint, 'title' => $title, 'type_label' => __( 'Custom Link', 'woocommerce' ), 'url' => esc_url_raw( wc_get_account_endpoint_url( $endpoint ) ), ); } return $items; } } endif; return new WC_Admin_Customize(); includes/admin/class-wc-admin-attributes.php 0000644 00000046250 15132754524 0015160 0 ustar 00 <?php /** * Attributes Page * * The attributes section lets users add custom attributes to assign to products - they can also be used in the "Filter Products by Attribute" widget. * * @package WooCommerce\Admin * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Admin_Attributes Class. */ class WC_Admin_Attributes { /** * Edited attribute ID. * * @var int */ private static $edited_attribute_id; /** * Handles output of the attributes page in admin. * * Shows the created attributes and lets you add new ones or edit existing ones. * The added attributes are stored in the database and can be used for layered navigation. */ public static function output() { $result = ''; $action = ''; // Action to perform: add, edit, delete or none. if ( ! empty( $_POST['add_new_attribute'] ) ) { // WPCS: CSRF ok. $action = 'add'; } elseif ( ! empty( $_POST['save_attribute'] ) && ! empty( $_GET['edit'] ) ) { // WPCS: CSRF ok. $action = 'edit'; } elseif ( ! empty( $_GET['delete'] ) ) { $action = 'delete'; } switch ( $action ) { case 'add': $result = self::process_add_attribute(); break; case 'edit': $result = self::process_edit_attribute(); break; case 'delete': $result = self::process_delete_attribute(); break; } if ( is_wp_error( $result ) ) { echo '<div id="woocommerce_errors" class="error"><p>' . wp_kses_post( $result->get_error_message() ) . '</p></div>'; } // Show admin interface. if ( ! empty( $_GET['edit'] ) ) { self::edit_attribute(); } else { self::add_attribute(); } } /** * Get and sanitize posted attribute data. * * @return array */ private static function get_posted_attribute() { $attribute = array( 'attribute_label' => isset( $_POST['attribute_label'] ) ? wc_clean( wp_unslash( $_POST['attribute_label'] ) ) : '', // WPCS: input var ok, CSRF ok. 'attribute_name' => isset( $_POST['attribute_name'] ) ? wc_sanitize_taxonomy_name( wp_unslash( $_POST['attribute_name'] ) ) : '', // WPCS: input var ok, CSRF ok, sanitization ok. 'attribute_type' => isset( $_POST['attribute_type'] ) ? wc_clean( wp_unslash( $_POST['attribute_type'] ) ) : 'select', // WPCS: input var ok, CSRF ok. 'attribute_orderby' => isset( $_POST['attribute_orderby'] ) ? wc_clean( wp_unslash( $_POST['attribute_orderby'] ) ) : '', // WPCS: input var ok, CSRF ok. 'attribute_public' => isset( $_POST['attribute_public'] ) ? 1 : 0, // WPCS: input var ok, CSRF ok. ); if ( empty( $attribute['attribute_type'] ) ) { $attribute['attribute_type'] = 'select'; } if ( empty( $attribute['attribute_label'] ) ) { $attribute['attribute_label'] = ucfirst( $attribute['attribute_name'] ); } if ( empty( $attribute['attribute_name'] ) ) { $attribute['attribute_name'] = wc_sanitize_taxonomy_name( $attribute['attribute_label'] ); } return $attribute; } /** * Add an attribute. * * @return bool|WP_Error */ private static function process_add_attribute() { check_admin_referer( 'woocommerce-add-new_attribute' ); $attribute = self::get_posted_attribute(); $args = array( 'name' => $attribute['attribute_label'], 'slug' => $attribute['attribute_name'], 'type' => $attribute['attribute_type'], 'order_by' => $attribute['attribute_orderby'], 'has_archives' => $attribute['attribute_public'], ); $id = wc_create_attribute( $args ); if ( is_wp_error( $id ) ) { return $id; } return true; } /** * Edit an attribute. * * @return bool|WP_Error */ private static function process_edit_attribute() { $attribute_id = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; check_admin_referer( 'woocommerce-save-attribute_' . $attribute_id ); $attribute = self::get_posted_attribute(); $args = array( 'name' => $attribute['attribute_label'], 'slug' => $attribute['attribute_name'], 'type' => $attribute['attribute_type'], 'order_by' => $attribute['attribute_orderby'], 'has_archives' => $attribute['attribute_public'], ); $id = wc_update_attribute( $attribute_id, $args ); if ( is_wp_error( $id ) ) { return $id; } self::$edited_attribute_id = $id; return true; } /** * Delete an attribute. * * @return bool */ private static function process_delete_attribute() { $attribute_id = isset( $_GET['delete'] ) ? absint( $_GET['delete'] ) : 0; check_admin_referer( 'woocommerce-delete-attribute_' . $attribute_id ); return wc_delete_attribute( $attribute_id ); } /** * Edit Attribute admin panel. * * Shows the interface for changing an attributes type between select and text. */ public static function edit_attribute() { global $wpdb; $edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; $attribute_to_edit = $wpdb->get_row( $wpdb->prepare( " SELECT attribute_type, attribute_label, attribute_name, attribute_orderby, attribute_public FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d ", $edit ) ); ?> <div class="wrap woocommerce"> <h1><?php esc_html_e( 'Edit attribute', 'woocommerce' ); ?></h1> <?php if ( ! $attribute_to_edit ) { echo '<div id="woocommerce_errors" class="error"><p>' . esc_html__( 'Error: non-existing attribute ID.', 'woocommerce' ) . '</p></div>'; } else { if ( self::$edited_attribute_id > 0 ) { echo '<div id="message" class="updated"><p>' . esc_html__( 'Attribute updated successfully', 'woocommerce' ) . '</p><p><a href="' . esc_url( admin_url( 'edit.php?post_type=product&page=product_attributes' ) ) . '">' . esc_html__( 'Back to Attributes', 'woocommerce' ) . '</a></p></div>'; self::$edited_attribute_id = null; } $att_type = $attribute_to_edit->attribute_type; $att_label = format_to_edit( $attribute_to_edit->attribute_label ); $att_name = $attribute_to_edit->attribute_name; $att_orderby = $attribute_to_edit->attribute_orderby; $att_public = $attribute_to_edit->attribute_public; ?> <form action="edit.php?post_type=product&page=product_attributes&edit=<?php echo absint( $edit ); ?>" method="post"> <table class="form-table"> <tbody> <?php do_action( 'woocommerce_before_edit_attribute_fields' ); ?> <tr class="form-field form-required"> <th scope="row" valign="top"> <label for="attribute_label"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label> </th> <td> <input name="attribute_label" id="attribute_label" type="text" value="<?php echo esc_attr( $att_label ); ?>" /> <p class="description"><?php esc_html_e( 'Name for the attribute (shown on the front-end).', 'woocommerce' ); ?></p> </td> </tr> <tr class="form-field form-required"> <th scope="row" valign="top"> <label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label> </th> <td> <input name="attribute_name" id="attribute_name" type="text" value="<?php echo esc_attr( $att_name ); ?>" maxlength="28" /> <p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p> </td> </tr> <tr class="form-field form-required"> <th scope="row" valign="top"> <label for="attribute_public"><?php esc_html_e( 'Enable archives?', 'woocommerce' ); ?></label> </th> <td> <input name="attribute_public" id="attribute_public" type="checkbox" value="1" <?php checked( $att_public, 1 ); ?> /> <p class="description"><?php esc_html_e( 'Enable this if you want this attribute to have product archives in your store.', 'woocommerce' ); ?></p> </td> </tr> <?php /** * Attribute types can change the way attributes are displayed on the frontend and admin. * * By Default WooCommerce only includes the `select` type. Others can be added with the * `product_attributes_type_selector` filter. If there is only the default type registered, * this setting will be hidden. */ if ( wc_has_custom_attribute_types() ) { ?> <tr class="form-field form-required"> <th scope="row" valign="top"> <label for="attribute_type"><?php esc_html_e( 'Type', 'woocommerce' ); ?></label> </th> <td> <select name="attribute_type" id="attribute_type"> <?php foreach ( wc_get_attribute_types() as $key => $value ) : ?> <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $att_type, $key ); ?>><?php echo esc_html( $value ); ?></option> <?php endforeach; ?> <?php /** * Deprecated action in favor of product_attributes_type_selector filter. * * @todo Remove in 4.0.0 * @deprecated 2.4.0 */ do_action( 'woocommerce_admin_attribute_types' ); ?> </select> <p class="description"><?php esc_html_e( "Determines how this attribute's values are displayed.", 'woocommerce' ); ?></p> </td> </tr> <?php } ?> <tr class="form-field form-required"> <th scope="row" valign="top"> <label for="attribute_orderby"><?php esc_html_e( 'Default sort order', 'woocommerce' ); ?></label> </th> <td> <select name="attribute_orderby" id="attribute_orderby"> <option value="menu_order" <?php selected( $att_orderby, 'menu_order' ); ?>><?php esc_html_e( 'Custom ordering', 'woocommerce' ); ?></option> <option value="name" <?php selected( $att_orderby, 'name' ); ?>><?php esc_html_e( 'Name', 'woocommerce' ); ?></option> <option value="name_num" <?php selected( $att_orderby, 'name_num' ); ?>><?php esc_html_e( 'Name (numeric)', 'woocommerce' ); ?></option> <option value="id" <?php selected( $att_orderby, 'id' ); ?>><?php esc_html_e( 'Term ID', 'woocommerce' ); ?></option> </select> <p class="description"><?php esc_html_e( 'Determines the sort order of the terms on the frontend shop product pages. If using custom ordering, you can drag and drop the terms in this attribute.', 'woocommerce' ); ?></p> </td> </tr> <?php do_action( 'woocommerce_after_edit_attribute_fields' ); ?> </tbody> </table> <p class="submit"><button type="submit" name="save_attribute" id="submit" class="button-primary" value="<?php esc_attr_e( 'Update', 'woocommerce' ); ?>"><?php esc_html_e( 'Update', 'woocommerce' ); ?></button></p> <?php wp_nonce_field( 'woocommerce-save-attribute_' . $edit ); ?> </form> <?php } ?> </div> <?php } /** * Add Attribute admin panel. * * Shows the interface for adding new attributes. */ public static function add_attribute() { ?> <div class="wrap woocommerce"> <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> <br class="clear" /> <div id="col-container"> <div id="col-right"> <div class="col-wrap"> <table class="widefat attributes-table wp-list-table ui-sortable" style="width:100%"> <thead> <tr> <th scope="col"><?php esc_html_e( 'Name', 'woocommerce' ); ?></th> <th scope="col"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></th> <?php if ( wc_has_custom_attribute_types() ) : ?> <th scope="col"><?php esc_html_e( 'Type', 'woocommerce' ); ?></th> <?php endif; ?> <th scope="col"><?php esc_html_e( 'Order by', 'woocommerce' ); ?></th> <th scope="col"><?php esc_html_e( 'Terms', 'woocommerce' ); ?></th> </tr> </thead> <tbody> <?php $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( $attribute_taxonomies ) : foreach ( $attribute_taxonomies as $tax ) : ?> <tr> <td> <strong><a href="edit-tags.php?taxonomy=<?php echo esc_attr( wc_attribute_taxonomy_name( $tax->attribute_name ) ); ?>&post_type=product"><?php echo esc_html( $tax->attribute_label ); ?></a></strong> <div class="row-actions"><span class="edit"><a href="<?php echo esc_url( add_query_arg( 'edit', $tax->attribute_id, 'edit.php?post_type=product&page=product_attributes' ) ); ?>"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | </span><span class="delete"><a class="delete" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'delete', $tax->attribute_id, 'edit.php?post_type=product&page=product_attributes' ), 'woocommerce-delete-attribute_' . $tax->attribute_id ) ); ?>"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></span></div> </td> <td><?php echo esc_html( $tax->attribute_name ); ?></td> <?php if ( wc_has_custom_attribute_types() ) : ?> <td><?php echo esc_html( wc_get_attribute_type_label( $tax->attribute_type ) ); ?> <?php echo $tax->attribute_public ? esc_html__( '(Public)', 'woocommerce' ) : ''; ?></td> <?php endif; ?> <td> <?php switch ( $tax->attribute_orderby ) { case 'name': esc_html_e( 'Name', 'woocommerce' ); break; case 'name_num': esc_html_e( 'Name (numeric)', 'woocommerce' ); break; case 'id': esc_html_e( 'Term ID', 'woocommerce' ); break; default: esc_html_e( 'Custom ordering', 'woocommerce' ); break; } ?> </td> <td class="attribute-terms"> <?php $taxonomy = wc_attribute_taxonomy_name( $tax->attribute_name ); if ( taxonomy_exists( $taxonomy ) ) { $terms = get_terms( $taxonomy, 'hide_empty=0' ); $terms_string = implode( ', ', wp_list_pluck( $terms, 'name' ) ); if ( $terms_string ) { echo esc_html( $terms_string ); } else { echo '<span class="na">–</span>'; } } else { echo '<span class="na">–</span>'; } ?> <br /><a href="edit-tags.php?taxonomy=<?php echo esc_attr( wc_attribute_taxonomy_name( $tax->attribute_name ) ); ?>&post_type=product" class="configure-terms"><?php esc_html_e( 'Configure terms', 'woocommerce' ); ?></a> </td> </tr> <?php endforeach; else : ?> <tr> <td colspan="6"><?php esc_html_e( 'No attributes currently exist.', 'woocommerce' ); ?></td> </tr> <?php endif; ?> </tbody> </table> </div> </div> <div id="col-left"> <div class="col-wrap"> <div class="form-wrap"> <h2><?php esc_html_e( 'Add new attribute', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'Attributes let you define extra product data, such as size or color. You can use these attributes in the shop sidebar using the "layered nav" widgets.', 'woocommerce' ); ?></p> <form action="edit.php?post_type=product&page=product_attributes" method="post"> <?php do_action( 'woocommerce_before_add_attribute_fields' ); ?> <div class="form-field"> <label for="attribute_label"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label> <input name="attribute_label" id="attribute_label" type="text" value="" /> <p class="description"><?php esc_html_e( 'Name for the attribute (shown on the front-end).', 'woocommerce' ); ?></p> </div> <div class="form-field"> <label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label> <input name="attribute_name" id="attribute_name" type="text" value="" maxlength="28" /> <p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p> </div> <div class="form-field"> <label for="attribute_public"><input name="attribute_public" id="attribute_public" type="checkbox" value="1" /> <?php esc_html_e( 'Enable Archives?', 'woocommerce' ); ?></label> <p class="description"><?php esc_html_e( 'Enable this if you want this attribute to have product archives in your store.', 'woocommerce' ); ?></p> </div> <?php /** * Attribute types can change the way attributes are displayed on the frontend and admin. * * By Default WooCommerce only includes the `select` type. Others can be added with the * `product_attributes_type_selector` filter. If there is only the default type registered, * this setting will be hidden. */ if ( wc_has_custom_attribute_types() ) { ?> <div class="form-field"> <label for="attribute_type"><?php esc_html_e( 'Type', 'woocommerce' ); ?></label> <select name="attribute_type" id="attribute_type"> <?php foreach ( wc_get_attribute_types() as $key => $value ) : ?> <option value="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $value ); ?></option> <?php endforeach; ?> <?php /** * Deprecated action in favor of product_attributes_type_selector filter. * * @todo Remove in 4.0.0 * @deprecated 2.4.0 */ do_action( 'woocommerce_admin_attribute_types' ); ?> </select> <p class="description"><?php esc_html_e( "Determines how this attribute's values are displayed.", 'woocommerce' ); ?></p> </div> <?php } ?> <div class="form-field"> <label for="attribute_orderby"><?php esc_html_e( 'Default sort order', 'woocommerce' ); ?></label> <select name="attribute_orderby" id="attribute_orderby"> <option value="menu_order"><?php esc_html_e( 'Custom ordering', 'woocommerce' ); ?></option> <option value="name"><?php esc_html_e( 'Name', 'woocommerce' ); ?></option> <option value="name_num"><?php esc_html_e( 'Name (numeric)', 'woocommerce' ); ?></option> <option value="id"><?php esc_html_e( 'Term ID', 'woocommerce' ); ?></option> </select> <p class="description"><?php esc_html_e( 'Determines the sort order of the terms on the frontend shop product pages. If using custom ordering, you can drag and drop the terms in this attribute.', 'woocommerce' ); ?></p> </div> <?php do_action( 'woocommerce_after_add_attribute_fields' ); ?> <p class="submit"><button type="submit" name="add_new_attribute" id="submit" class="button button-primary" value="<?php esc_attr_e( 'Add attribute', 'woocommerce' ); ?>"><?php esc_html_e( 'Add attribute', 'woocommerce' ); ?></button></p> <?php wp_nonce_field( 'woocommerce-add-new_attribute' ); ?> </form> </div> </div> </div> </div> <script type="text/javascript"> /* <![CDATA[ */ jQuery( 'a.delete' ).on( 'click', function() { if ( window.confirm( '<?php esc_html_e( 'Are you sure you want to delete this attribute?', 'woocommerce' ); ?>' ) ) { return true; } return false; }); /* ]]> */ </script> </div> <?php } } includes/admin/list-tables/class-wc-admin-list-table-orders.php 0000644 00000073266 15132754524 0020560 0 ustar 00 <?php /** * List tables: orders. * * @package WooCommerce\Admin * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_List_Table_Orders', false ) ) { return; } if ( ! class_exists( 'WC_Admin_List_Table', false ) ) { include_once __DIR__ . '/abstract-class-wc-admin-list-table.php'; } /** * WC_Admin_List_Table_Orders Class. */ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table { /** * Post type. * * @var string */ protected $list_table_type = 'shop_order'; /** * Constructor. */ public function __construct() { parent::__construct(); add_action( 'admin_notices', array( $this, 'bulk_admin_notices' ) ); add_action( 'admin_footer', array( $this, 'order_preview_template' ) ); add_filter( 'get_search_query', array( $this, 'search_label' ) ); add_filter( 'query_vars', array( $this, 'add_custom_query_var' ) ); add_action( 'parse_query', array( $this, 'search_custom_fields' ) ); } /** * Render blank state. */ protected function render_blank_state() { echo '<div class="woocommerce-BlankState">'; echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'When you receive a new order, it will appear here.', 'woocommerce' ) . '</h2>'; echo '<div class="woocommerce-BlankState-buttons">'; echo '<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://docs.woocommerce.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin">' . esc_html__( 'Learn more about orders', 'woocommerce' ) . '</a>'; echo '</div>'; do_action( 'wc_marketplace_suggestions_orders_empty_state' ); echo '</div>'; } /** * Define primary column. * * @return string */ protected function get_primary_column() { return 'order_number'; } /** * Get row actions to show in the list table. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ protected function get_row_actions( $actions, $post ) { return array(); } /** * Define hidden columns. * * @return array */ protected function define_hidden_columns() { return array( 'shipping_address', 'billing_address', 'wc_actions', ); } /** * Define which columns are sortable. * * @param array $columns Existing columns. * @return array */ public function define_sortable_columns( $columns ) { $custom = array( 'order_number' => 'ID', 'order_total' => 'order_total', 'order_date' => 'date', ); unset( $columns['comments'] ); return wp_parse_args( $custom, $columns ); } /** * Define which columns to show on this screen. * * @param array $columns Existing columns. * @return array */ public function define_columns( $columns ) { $show_columns = array(); $show_columns['cb'] = $columns['cb']; $show_columns['order_number'] = __( 'Order', 'woocommerce' ); $show_columns['order_date'] = __( 'Date', 'woocommerce' ); $show_columns['order_status'] = __( 'Status', 'woocommerce' ); $show_columns['billing_address'] = __( 'Billing', 'woocommerce' ); $show_columns['shipping_address'] = __( 'Ship to', 'woocommerce' ); $show_columns['order_total'] = __( 'Total', 'woocommerce' ); $show_columns['wc_actions'] = __( 'Actions', 'woocommerce' ); wp_enqueue_script( 'wc-orders' ); return $show_columns; } /** * Define bulk actions. * * @param array $actions Existing actions. * @return array */ public function define_bulk_actions( $actions ) { if ( isset( $actions['edit'] ) ) { unset( $actions['edit'] ); } $actions['mark_processing'] = __( 'Change status to processing', 'woocommerce' ); $actions['mark_on-hold'] = __( 'Change status to on-hold', 'woocommerce' ); $actions['mark_completed'] = __( 'Change status to completed', 'woocommerce' ); $actions['mark_cancelled'] = __( 'Change status to cancelled', 'woocommerce' ); if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) { $actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' ); } return $actions; } /** * Pre-fetch any data for the row each column has access to it. the_order global is there for bw compat. * * @param int $post_id Post ID being shown. */ protected function prepare_row_data( $post_id ) { global $the_order; if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { $this->object = wc_get_order( $post_id ); $the_order = $this->object; } } /** * Render columm: order_number. */ protected function render_order_number_column() { $buyer = ''; if ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) { /* translators: 1: first name 2: last name */ $buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->object->get_billing_first_name(), $this->object->get_billing_last_name() ) ); } elseif ( $this->object->get_billing_company() ) { $buyer = trim( $this->object->get_billing_company() ); } elseif ( $this->object->get_customer_id() ) { $user = get_user_by( 'id', $this->object->get_customer_id() ); $buyer = ucwords( $user->display_name ); } /** * Filter buyer name in list table orders. * * @since 3.7.0 * @param string $buyer Buyer name. * @param WC_Order $order Order data. */ $buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $this->object ); if ( $this->object->get_status() === 'trash' ) { echo '<strong>#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong>'; } else { echo '<a href="#" class="order-preview" data-order-id="' . absint( $this->object->get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '</a>'; echo '<a href="' . esc_url( admin_url( 'post.php?post=' . absint( $this->object->get_id() ) ) . '&action=edit' ) . '" class="order-view"><strong>#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>'; } } /** * Render columm: order_status. */ protected function render_order_status_column() { $tooltip = ''; $comment_count = get_comment_count( $this->object->get_id() ); $approved_comments_count = absint( $comment_count['approved'] ); if ( $approved_comments_count ) { $latest_notes = wc_get_order_notes( array( 'order_id' => $this->object->get_id(), 'limit' => 1, 'orderby' => 'date_created_gmt', ) ); $latest_note = current( $latest_notes ); if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) { $tooltip = wc_sanitize_tooltip( $latest_note->content ); } elseif ( isset( $latest_note->content ) ) { /* translators: %d: notes count */ $tooltip = wc_sanitize_tooltip( $latest_note->content . '<br/><small style="display:block">' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '</small>' ); } else { /* translators: %d: notes count */ $tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) ); } } if ( $tooltip ) { printf( '<mark class="order-status %s tips" data-tip="%s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), wp_kses_post( $tooltip ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); } else { printf( '<mark class="order-status %s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); } } /** * Render columm: order_date. */ protected function render_order_date_column() { $order_timestamp = $this->object->get_date_created() ? $this->object->get_date_created()->getTimestamp() : ''; if ( ! $order_timestamp ) { echo '–'; return; } // Check if the order was created within the last 24 hours, and not in the future. if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) { $show_date = sprintf( /* translators: %s: human-readable time difference */ _x( '%s ago', '%s = human-readable time difference', 'woocommerce' ), human_time_diff( $this->object->get_date_created()->getTimestamp(), time() ) ); } else { $show_date = $this->object->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); } printf( '<time datetime="%1$s" title="%2$s">%3$s</time>', esc_attr( $this->object->get_date_created()->date( 'c' ) ), esc_html( $this->object->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), esc_html( $show_date ) ); } /** * Render columm: order_total. */ protected function render_order_total_column() { if ( $this->object->get_payment_method_title() ) { /* translators: %s: method */ echo '<span class="tips" data-tip="' . esc_attr( sprintf( __( 'via %s', 'woocommerce' ), $this->object->get_payment_method_title() ) ) . '">' . wp_kses_post( $this->object->get_formatted_order_total() ) . '</span>'; } else { echo wp_kses_post( $this->object->get_formatted_order_total() ); } } /** * Render columm: wc_actions. */ protected function render_wc_actions_column() { echo '<p>'; do_action( 'woocommerce_admin_order_actions_start', $this->object ); $actions = array(); if ( $this->object->has_status( array( 'pending', 'on-hold' ) ) ) { $actions['processing'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Processing', 'woocommerce' ), 'action' => 'processing', ); } if ( $this->object->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { $actions['complete'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Complete', 'woocommerce' ), 'action' => 'complete', ); } $actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $this->object ); echo wc_render_action_buttons( $actions ); // WPCS: XSS ok. do_action( 'woocommerce_admin_order_actions_end', $this->object ); echo '</p>'; } /** * Render columm: billing_address. */ protected function render_billing_address_column() { $address = $this->object->get_formatted_billing_address(); if ( $address ) { echo esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) ); if ( $this->object->get_payment_method() ) { /* translators: %s: payment method */ echo '<span class="description">' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_payment_method_title() ) ) . '</span>'; // WPCS: XSS ok. } } else { echo '–'; } } /** * Render columm: shipping_address. */ protected function render_shipping_address_column() { $address = $this->object->get_formatted_shipping_address(); if ( $address ) { echo '<a target="_blank" href="' . esc_url( $this->object->get_shipping_address_map_url() ) . '">' . esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) ) . '</a>'; if ( $this->object->get_shipping_method() ) { /* translators: %s: shipping method */ echo '<span class="description">' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_shipping_method() ) ) . '</span>'; // WPCS: XSS ok. } } else { echo '–'; } } /** * Template for order preview. * * @since 3.3.0 */ public function order_preview_template() { ?> <script type="text/template" id="tmpl-wc-modal-view-order"> <div class="wc-backbone-modal wc-order-preview"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <mark class="order-status status-{{ data.status }}"><span>{{ data.status_name }}</span></mark> <?php /* translators: %s: order ID */ ?> <h1><?php echo esc_html( sprintf( __( 'Order #%s', 'woocommerce' ), '{{ data.order_number }}' ) ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span> </button> </header> <article> <?php do_action( 'woocommerce_admin_order_preview_start' ); ?> <div class="wc-order-preview-addresses"> <div class="wc-order-preview-address"> <h2><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h2> {{{ data.formatted_billing_address }}} <# if ( data.data.billing.email ) { #> <strong><?php esc_html_e( 'Email', 'woocommerce' ); ?></strong> <a href="mailto:{{ data.data.billing.email }}">{{ data.data.billing.email }}</a> <# } #> <# if ( data.data.billing.phone ) { #> <strong><?php esc_html_e( 'Phone', 'woocommerce' ); ?></strong> <a href="tel:{{ data.data.billing.phone }}">{{ data.data.billing.phone }}</a> <# } #> <# if ( data.payment_via ) { #> <strong><?php esc_html_e( 'Payment via', 'woocommerce' ); ?></strong> {{{ data.payment_via }}} <# } #> </div> <# if ( data.needs_shipping ) { #> <div class="wc-order-preview-address"> <h2><?php esc_html_e( 'Shipping details', 'woocommerce' ); ?></h2> <# if ( data.ship_to_billing ) { #> {{{ data.formatted_billing_address }}} <# } else { #> <a href="{{ data.shipping_address_map_url }}" target="_blank">{{{ data.formatted_shipping_address }}}</a> <# } #> <# if ( data.shipping_via ) { #> <strong><?php esc_html_e( 'Shipping method', 'woocommerce' ); ?></strong> {{ data.shipping_via }} <# } #> </div> <# } #> <# if ( data.data.customer_note ) { #> <div class="wc-order-preview-note"> <strong><?php esc_html_e( 'Note', 'woocommerce' ); ?></strong> {{ data.data.customer_note }} </div> <# } #> </div> {{{ data.item_html }}} <?php do_action( 'woocommerce_admin_order_preview_end' ); ?> </article> <footer> <div class="inner"> {{{ data.actions_html }}} <a class="button button-primary button-large" aria-label="<?php esc_attr_e( 'Edit this order', 'woocommerce' ); ?>" href="<?php echo esc_url( admin_url( 'post.php?action=edit' ) ); ?>&post={{ data.data.id }}"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <?php } /** * Get items to display in the preview as HTML. * * @param WC_Order $order Order object. * @return string */ public static function get_order_preview_item_html( $order ) { $hidden_order_itemmeta = apply_filters( 'woocommerce_hidden_order_itemmeta', array( '_qty', '_tax_class', '_product_id', '_variation_id', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', 'method_id', 'cost', '_reduced_stock', '_restock_refunded_items', ) ); $line_items = apply_filters( 'woocommerce_admin_order_preview_line_items', $order->get_items(), $order ); $columns = apply_filters( 'woocommerce_admin_order_preview_line_item_columns', array( 'product' => __( 'Product', 'woocommerce' ), 'quantity' => __( 'Quantity', 'woocommerce' ), 'tax' => __( 'Tax', 'woocommerce' ), 'total' => __( 'Total', 'woocommerce' ), ), $order ); if ( ! wc_tax_enabled() ) { unset( $columns['tax'] ); } $html = ' <div class="wc-order-preview-table-wrapper"> <table cellspacing="0" class="wc-order-preview-table"> <thead> <tr>'; foreach ( $columns as $column => $label ) { $html .= '<th class="wc-order-preview-table__column--' . esc_attr( $column ) . '">' . esc_html( $label ) . '</th>'; } $html .= ' </tr> </thead> <tbody>'; foreach ( $line_items as $item_id => $item ) { $product_object = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null; $row_class = apply_filters( 'woocommerce_admin_html_order_preview_item_class', '', $item, $order ); $html .= '<tr class="wc-order-preview-table__item wc-order-preview-table__item--' . esc_attr( $item_id ) . ( $row_class ? ' ' . esc_attr( $row_class ) : '' ) . '">'; foreach ( $columns as $column => $label ) { $html .= '<td class="wc-order-preview-table__column--' . esc_attr( $column ) . '">'; switch ( $column ) { case 'product': $html .= wp_kses_post( $item->get_name() ); if ( $product_object ) { $html .= '<div class="wc-order-item-sku">' . esc_html( $product_object->get_sku() ) . '</div>'; } $meta_data = $item->get_formatted_meta_data( '' ); if ( $meta_data ) { $html .= '<table cellspacing="0" class="wc-order-item-meta">'; foreach ( $meta_data as $meta_id => $meta ) { if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) { continue; } $html .= '<tr><th>' . wp_kses_post( $meta->display_key ) . ':</th><td>' . wp_kses_post( force_balance_tags( $meta->display_value ) ) . '</td></tr>'; } $html .= '</table>'; } break; case 'quantity': $html .= esc_html( $item->get_quantity() ); break; case 'tax': $html .= wc_price( $item->get_total_tax(), array( 'currency' => $order->get_currency() ) ); break; case 'total': $html .= wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); break; default: $html .= apply_filters( 'woocommerce_admin_order_preview_line_item_column_' . sanitize_key( $column ), '', $item, $item_id, $order ); break; } $html .= '</td>'; } $html .= '</tr>'; } $html .= ' </tbody> </table> </div>'; return $html; } /** * Get actions to display in the preview as HTML. * * @param WC_Order $order Order object. * @return string */ public static function get_order_preview_actions_html( $order ) { $actions = array(); $status_actions = array(); if ( $order->has_status( array( 'pending' ) ) ) { $status_actions['on-hold'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=on-hold&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'On-hold', 'woocommerce' ), 'title' => __( 'Change order status to on-hold', 'woocommerce' ), 'action' => 'on-hold', ); } if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) { $status_actions['processing'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Processing', 'woocommerce' ), 'title' => __( 'Change order status to processing', 'woocommerce' ), 'action' => 'processing', ); } if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { $status_actions['complete'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Completed', 'woocommerce' ), 'title' => __( 'Change order status to completed', 'woocommerce' ), 'action' => 'complete', ); } if ( $status_actions ) { $actions['status'] = array( 'group' => __( 'Change status: ', 'woocommerce' ), 'actions' => $status_actions, ); } return wc_render_action_buttons( apply_filters( 'woocommerce_admin_order_preview_actions', $actions, $order ) ); } /** * Get order details to send to the ajax endpoint for previews. * * @param WC_Order $order Order object. * @return array */ public static function order_preview_get_order_details( $order ) { if ( ! $order ) { return array(); } $payment_via = $order->get_payment_method_title(); $payment_method = $order->get_payment_method(); $payment_gateways = WC()->payment_gateways() ? WC()->payment_gateways->payment_gateways() : array(); $transaction_id = $order->get_transaction_id(); if ( $transaction_id ) { $url = isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_transaction_url( $order ) : false; if ( $url ) { $payment_via .= ' (<a href="' . esc_url( $url ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>)'; } else { $payment_via .= ' (' . esc_html( $transaction_id ) . ')'; } } $billing_address = $order->get_formatted_billing_address(); $shipping_address = $order->get_formatted_shipping_address(); return apply_filters( 'woocommerce_admin_order_preview_get_order_details', array( 'data' => $order->get_data(), 'order_number' => $order->get_order_number(), 'item_html' => self::get_order_preview_item_html( $order ), 'actions_html' => self::get_order_preview_actions_html( $order ), 'ship_to_billing' => wc_ship_to_billing_address_only(), 'needs_shipping' => $order->needs_shipping_address(), 'formatted_billing_address' => $billing_address ? $billing_address : __( 'N/A', 'woocommerce' ), 'formatted_shipping_address' => $shipping_address ? $shipping_address : __( 'N/A', 'woocommerce' ), 'shipping_address_map_url' => $order->get_shipping_address_map_url(), 'payment_via' => $payment_via, 'shipping_via' => $order->get_shipping_method(), 'status' => $order->get_status(), 'status_name' => wc_get_order_status_name( $order->get_status() ), ), $order ); } /** * Handle bulk actions. * * @param string $redirect_to URL to redirect to. * @param string $action Action name. * @param array $ids List of ids. * @return string */ public function handle_bulk_actions( $redirect_to, $action, $ids ) { $ids = apply_filters( 'woocommerce_bulk_action_ids', array_reverse( array_map( 'absint', $ids ) ), $action, 'order' ); $changed = 0; if ( 'remove_personal_data' === $action ) { $report_action = 'removed_personal_data'; foreach ( $ids as $id ) { $order = wc_get_order( $id ); if ( $order ) { do_action( 'woocommerce_remove_order_personal_data', $order ); $changed++; } } } elseif ( false !== strpos( $action, 'mark_' ) ) { $order_statuses = wc_get_order_statuses(); $new_status = substr( $action, 5 ); // Get the status name from action. $report_action = 'marked_' . $new_status; // Sanity check: bail out if this is actually not a status, or is not a registered status. if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { // Initialize payment gateways in case order has hooked status transition actions. WC()->payment_gateways(); foreach ( $ids as $id ) { $order = wc_get_order( $id ); $order->update_status( $new_status, __( 'Order status changed by bulk edit:', 'woocommerce' ), true ); do_action( 'woocommerce_order_edit_status', $id, $new_status ); $changed++; } } } if ( $changed ) { $redirect_to = add_query_arg( array( 'post_type' => $this->list_table_type, 'bulk_action' => $report_action, 'changed' => $changed, 'ids' => join( ',', $ids ), ), $redirect_to ); } return esc_url_raw( $redirect_to ); } /** * Show confirmation message that order status changed for number of orders. */ public function bulk_admin_notices() { global $post_type, $pagenow; // Bail out if not on shop order list page. if ( 'edit.php' !== $pagenow || 'shop_order' !== $post_type || ! isset( $_REQUEST['bulk_action'] ) ) { // WPCS: input var ok, CSRF ok. return; } $order_statuses = wc_get_order_statuses(); $number = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0; // WPCS: input var ok, CSRF ok. $bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); // WPCS: input var ok, CSRF ok. // Check if any status changes happened. foreach ( $order_statuses as $slug => $name ) { if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok. /* translators: %d: orders count */ $message = sprintf( _n( '%d order status changed.', '%d order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) ); echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>'; break; } } if ( 'removed_personal_data' === $bulk_action ) { // WPCS: input var ok, CSRF ok. /* translators: %d: orders count */ $message = sprintf( _n( 'Removed personal data from %d order.', 'Removed personal data from %d orders.', $number, 'woocommerce' ), number_format_i18n( $number ) ); echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>'; } } /** * See if we should render search filters or not. */ public function restrict_manage_posts() { global $typenow; if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) { $this->render_filters(); } } /** * Render any custom filters and search inputs for the list table. */ protected function render_filters() { $user_string = ''; $user_id = ''; if ( ! empty( $_GET['_customer_user'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended $user_id = absint( $_GET['_customer_user'] ); // WPCS: input var ok, sanitization ok. $user = get_user_by( 'id', $user_id ); $user_string = sprintf( /* translators: 1: user display name 2: user ID 3: user email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), $user->display_name, absint( $user->ID ), $user->user_email ); } ?> <select class="wc-customer-search" name="_customer_user" data-placeholder="<?php esc_attr_e( 'Filter by registered customer', 'woocommerce' ); ?>" data-allow_clear="true"> <option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option> </select> <?php } /** * Handle any filters. * * @param array $query_vars Query vars. * @return array */ public function request_query( $query_vars ) { global $typenow; if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) { return $this->query_filters( $query_vars ); } return $query_vars; } /** * Handle any custom filters. * * @param array $query_vars Query vars. * @return array */ protected function query_filters( $query_vars ) { global $wp_post_statuses; // Filter the orders by the posted customer. if ( ! empty( $_GET['_customer_user'] ) ) { // WPCS: input var ok. // @codingStandardsIgnoreStart. $query_vars['meta_query'] = array( array( 'key' => '_customer_user', 'value' => (int) $_GET['_customer_user'], // WPCS: input var ok, sanitization ok. 'compare' => '=', ), ); // @codingStandardsIgnoreEnd } // Sorting. if ( isset( $query_vars['orderby'] ) ) { if ( 'order_total' === $query_vars['orderby'] ) { // @codingStandardsIgnoreStart $query_vars = array_merge( $query_vars, array( 'meta_key' => '_order_total', 'orderby' => 'meta_value_num', ) ); // @codingStandardsIgnoreEnd } } // Status. if ( empty( $query_vars['post_status'] ) ) { $post_statuses = wc_get_order_statuses(); foreach ( $post_statuses as $status => $value ) { if ( isset( $wp_post_statuses[ $status ] ) && false === $wp_post_statuses[ $status ]->show_in_admin_all_list ) { unset( $post_statuses[ $status ] ); } } $query_vars['post_status'] = array_keys( $post_statuses ); } return $query_vars; } /** * Change the label when searching orders. * * @param mixed $query Current search query. * @return string */ public function search_label( $query ) { global $pagenow, $typenow; if ( 'edit.php' !== $pagenow || 'shop_order' !== $typenow || ! get_query_var( 'shop_order_search' ) || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return $query; } return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok. } /** * Query vars for custom searches. * * @param mixed $public_query_vars Array of query vars. * @return array */ public function add_custom_query_var( $public_query_vars ) { $public_query_vars[] = 'shop_order_search'; return $public_query_vars; } /** * Search custom fields as well as content. * * @param WP_Query $wp Query object. */ public function search_custom_fields( $wp ) { global $pagenow; if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $post_ids = wc_order_search( wc_clean( wp_unslash( $_GET['s'] ) ) ); // WPCS: input var ok, sanitization ok. if ( ! empty( $post_ids ) ) { // Remove "s" - we don't want to search order name. unset( $wp->query_vars['s'] ); // so we know we're doing this. $wp->query_vars['shop_order_search'] = true; // Search by found posts. $wp->query_vars['post__in'] = array_merge( $post_ids, array( 0 ) ); } } } includes/admin/list-tables/class-wc-admin-list-table-coupons.php 0000644 00000014436 15132754524 0020742 0 ustar 00 <?php /** * List tables: coupons. * * @package WooCommerce\Admin * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_List_Table_Coupons', false ) ) { return; } if ( ! class_exists( 'WC_Admin_List_Table', false ) ) { include_once __DIR__ . '/abstract-class-wc-admin-list-table.php'; } /** * WC_Admin_List_Table_Coupons Class. */ class WC_Admin_List_Table_Coupons extends WC_Admin_List_Table { /** * Post type. * * @var string */ protected $list_table_type = 'shop_coupon'; /** * Constructor. */ public function __construct() { parent::__construct(); add_filter( 'disable_months_dropdown', '__return_true' ); } /** * Render blank state. */ protected function render_blank_state() { echo '<div class="woocommerce-BlankState">'; echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'Coupons are a great way to offer discounts and rewards to your customers. They will appear here once created.', 'woocommerce' ) . '</h2>'; echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=shop_coupon' ) ) . '">' . esc_html__( 'Create your first coupon', 'woocommerce' ) . '</a>'; echo '<a class="woocommerce-BlankState-cta button" target="_blank" href="https://docs.woocommerce.com/document/coupon-management/?utm_source=blankslate&utm_medium=product&utm_content=couponsdoc&utm_campaign=woocommerceplugin">' . esc_html__( 'Learn more about coupons', 'woocommerce' ) . '</a>'; echo '</div>'; } /** * Define primary column. * * @return string */ protected function get_primary_column() { return 'coupon_code'; } /** * Get row actions to show in the list table. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ protected function get_row_actions( $actions, $post ) { unset( $actions['inline hide-if-no-js'] ); return $actions; } /** * Define which columns to show on this screen. * * @param array $columns Existing columns. * @return array */ public function define_columns( $columns ) { $show_columns = array(); $show_columns['cb'] = $columns['cb']; $show_columns['coupon_code'] = __( 'Code', 'woocommerce' ); $show_columns['type'] = __( 'Coupon type', 'woocommerce' ); $show_columns['amount'] = __( 'Coupon amount', 'woocommerce' ); $show_columns['description'] = __( 'Description', 'woocommerce' ); $show_columns['products'] = __( 'Product IDs', 'woocommerce' ); $show_columns['usage'] = __( 'Usage / Limit', 'woocommerce' ); $show_columns['expiry_date'] = __( 'Expiry date', 'woocommerce' ); return $show_columns; } /** * Pre-fetch any data for the row each column has access to it. the_coupon global is there for bw compat. * * @param int $post_id Post ID being shown. */ protected function prepare_row_data( $post_id ) { global $the_coupon; if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { $this->object = new WC_Coupon( $post_id ); $the_coupon = $this->object; } } /** * Render columm: coupon_code. */ protected function render_coupon_code_column() { global $post; $edit_link = get_edit_post_link( $this->object->get_id() ); $title = $this->object->get_code(); echo '<strong><a class="row-title" href="' . esc_url( $edit_link ) . '">' . esc_html( $title ) . '</a>'; _post_states( $post ); echo '</strong>'; } /** * Render columm: type. */ protected function render_type_column() { echo esc_html( wc_get_coupon_type( $this->object->get_discount_type() ) ); } /** * Render columm: amount. */ protected function render_amount_column() { echo esc_html( wc_format_localized_price( $this->object->get_amount() ) ); } /** * Render columm: products. */ protected function render_products_column() { $product_ids = $this->object->get_product_ids(); if ( count( $product_ids ) > 0 ) { echo esc_html( implode( ', ', $product_ids ) ); } else { echo '–'; } } /** * Render columm: usage_limit. */ protected function render_usage_limit_column() { $usage_limit = $this->object->get_usage_limit(); if ( $usage_limit ) { echo esc_html( $usage_limit ); } else { echo '–'; } } /** * Render columm: usage. */ protected function render_usage_column() { $usage_count = $this->object->get_usage_count(); $usage_limit = $this->object->get_usage_limit(); printf( /* translators: 1: count 2: limit */ __( '%1$s / %2$s', 'woocommerce' ), esc_html( $usage_count ), $usage_limit ? esc_html( $usage_limit ) : '∞' ); } /** * Render columm: expiry_date. */ protected function render_expiry_date_column() { $expiry_date = $this->object->get_date_expires(); if ( $expiry_date ) { echo esc_html( $expiry_date->date_i18n( 'F j, Y' ) ); } else { echo '–'; } } /** * Render columm: description. */ protected function render_description_column() { echo wp_kses_post( $this->object->get_description() ? $this->object->get_description() : '–' ); } /** * Render any custom filters and search inputs for the list table. */ protected function render_filters() { ?> <select name="coupon_type" id="dropdown_shop_coupon_type"> <option value=""><?php esc_html_e( 'Show all types', 'woocommerce' ); ?></option> <?php $types = wc_get_coupon_types(); foreach ( $types as $name => $type ) { echo '<option value="' . esc_attr( $name ) . '"'; if ( isset( $_GET['coupon_type'] ) ) { // WPCS: input var ok. selected( $name, wc_clean( wp_unslash( $_GET['coupon_type'] ) ) ); // WPCS: input var ok, sanitization ok. } echo '>' . esc_html( $type ) . '</option>'; } ?> </select> <?php } /** * Handle any custom filters. * * @param array $query_vars Query vars. * @return array */ protected function query_filters( $query_vars ) { if ( ! empty( $_GET['coupon_type'] ) ) { // WPCS: input var ok, sanitization ok. $query_vars['meta_key'] = 'discount_type'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key $query_vars['meta_value'] = wc_clean( wp_unslash( $_GET['coupon_type'] ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.VIP.SuperGlobalInputUsage.AccessDetected } return $query_vars; } } includes/admin/list-tables/class-wc-admin-list-table-products.php 0000644 00000056212 15132754524 0021115 0 ustar 00 <?php /** * List tables: products. * * @package WooCommerce\Admin * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_List_Table_Products', false ) ) { return; } if ( ! class_exists( 'WC_Admin_List_Table', false ) ) { include_once __DIR__ . '/abstract-class-wc-admin-list-table.php'; } /** * WC_Admin_List_Table_Products Class. */ class WC_Admin_List_Table_Products extends WC_Admin_List_Table { /** * Post type. * * @var string */ protected $list_table_type = 'product'; /** * Constructor. */ public function __construct() { parent::__construct(); add_filter( 'disable_months_dropdown', '__return_true' ); add_filter( 'query_vars', array( $this, 'add_custom_query_var' ) ); add_filter( 'views_edit-product', array( $this, 'product_views' ) ); add_filter( 'get_search_query', array( $this, 'search_label' ) ); add_filter( 'posts_clauses', array( $this, 'posts_clauses' ), 10, 2 ); } /** * Render blank state. */ protected function render_blank_state() { echo '<div class="woocommerce-BlankState">'; echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '</h2>'; echo '<div class="woocommerce-BlankState-buttons">'; echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ) . '">' . esc_html__( 'Create Product', 'woocommerce' ) . '</a>'; echo '<a class="woocommerce-BlankState-cta button" href="' . esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) . '">' . esc_html__( 'Start Import', 'woocommerce' ) . '</a>'; echo '</div>'; do_action( 'wc_marketplace_suggestions_products_empty_state' ); echo '</div>'; } /** * Define primary column. * * @return string */ protected function get_primary_column() { return 'name'; } /** * Get row actions to show in the list table. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ protected function get_row_actions( $actions, $post ) { /* translators: %d: product ID. */ return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions ); } /** * Define which columns are sortable. * * @param array $columns Existing columns. * @return array */ public function define_sortable_columns( $columns ) { $custom = array( 'price' => 'price', 'sku' => 'sku', 'name' => 'title', ); return wp_parse_args( $custom, $columns ); } /** * Define which columns to show on this screen. * * @param array $columns Existing columns. * @return array */ public function define_columns( $columns ) { if ( empty( $columns ) && ! is_array( $columns ) ) { $columns = array(); } unset( $columns['title'], $columns['comments'], $columns['date'] ); $show_columns = array(); $show_columns['cb'] = '<input type="checkbox" />'; $show_columns['thumb'] = '<span class="wc-image tips" data-tip="' . esc_attr__( 'Image', 'woocommerce' ) . '">' . __( 'Image', 'woocommerce' ) . '</span>'; $show_columns['name'] = __( 'Name', 'woocommerce' ); if ( wc_product_sku_enabled() ) { $show_columns['sku'] = __( 'SKU', 'woocommerce' ); } if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); } $show_columns['price'] = __( 'Price', 'woocommerce' ); $show_columns['product_cat'] = __( 'Categories', 'woocommerce' ); $show_columns['product_tag'] = __( 'Tags', 'woocommerce' ); $show_columns['featured'] = '<span class="wc-featured parent-tips" data-tip="' . esc_attr__( 'Featured', 'woocommerce' ) . '">' . __( 'Featured', 'woocommerce' ) . '</span>'; $show_columns['date'] = __( 'Date', 'woocommerce' ); return array_merge( $show_columns, $columns ); } /** * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat. * * @param int $post_id Post ID being shown. */ protected function prepare_row_data( $post_id ) { global $the_product; if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { $the_product = wc_get_product( $post_id ); $this->object = $the_product; } } /** * Render column: thumb. */ protected function render_thumb_column() { echo '<a href="' . esc_url( get_edit_post_link( $this->object->get_id() ) ) . '">' . $this->object->get_image( 'thumbnail' ) . '</a>'; // WPCS: XSS ok. } /** * Render column: name. */ protected function render_name_column() { global $post; $edit_link = get_edit_post_link( $this->object->get_id() ); $title = _draft_or_post_title(); echo '<strong><a class="row-title" href="' . esc_url( $edit_link ) . '">' . esc_html( $title ) . '</a>'; _post_states( $post ); echo '</strong>'; if ( $this->object->get_parent_id() > 0 ) { echo ' ← <a href="' . esc_url( get_edit_post_link( $this->object->get_parent_id() ) ) . '">' . get_the_title( $this->object->get_parent_id() ) . '</a>'; // @codingStandardsIgnoreLine. } get_inline_data( $post ); /* Custom inline data for woocommerce. */ echo ' <div class="hidden" id="woocommerce_inline_' . absint( $this->object->get_id() ) . '"> <div class="menu_order">' . esc_html( $this->object->get_menu_order() ) . '</div> <div class="sku">' . esc_html( $this->object->get_sku() ) . '</div> <div class="regular_price">' . esc_html( $this->object->get_regular_price() ) . '</div> <div class="sale_price">' . esc_html( $this->object->get_sale_price() ) . '</div> <div class="weight">' . esc_html( $this->object->get_weight() ) . '</div> <div class="length">' . esc_html( $this->object->get_length() ) . '</div> <div class="width">' . esc_html( $this->object->get_width() ) . '</div> <div class="height">' . esc_html( $this->object->get_height() ) . '</div> <div class="shipping_class">' . esc_html( $this->object->get_shipping_class() ) . '</div> <div class="visibility">' . esc_html( $this->object->get_catalog_visibility() ) . '</div> <div class="stock_status">' . esc_html( $this->object->get_stock_status() ) . '</div> <div class="stock">' . esc_html( $this->object->get_stock_quantity() ) . '</div> <div class="manage_stock">' . esc_html( wc_bool_to_string( $this->object->get_manage_stock() ) ) . '</div> <div class="featured">' . esc_html( wc_bool_to_string( $this->object->get_featured() ) ) . '</div> <div class="product_type">' . esc_html( $this->object->get_type() ) . '</div> <div class="product_is_virtual">' . esc_html( wc_bool_to_string( $this->object->get_virtual() ) ) . '</div> <div class="tax_status">' . esc_html( $this->object->get_tax_status() ) . '</div> <div class="tax_class">' . esc_html( $this->object->get_tax_class() ) . '</div> <div class="backorders">' . esc_html( $this->object->get_backorders() ) . '</div> <div class="low_stock_amount">' . esc_html( $this->object->get_low_stock_amount() ) . '</div> </div> '; } /** * Render column: sku. */ protected function render_sku_column() { echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : '<span class="na">–</span>'; } /** * Render column: price. */ protected function render_price_column() { echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : '<span class="na">–</span>'; } /** * Render column: product_cat. */ protected function render_product_cat_column() { $terms = get_the_terms( $this->object->get_id(), 'product_cat' ); if ( ! $terms ) { echo '<span class="na">–</span>'; } else { $termlist = array(); foreach ( $terms as $term ) { $termlist[] = '<a href="' . esc_url( admin_url( 'edit.php?product_cat=' . $term->slug . '&post_type=product' ) ) . ' ">' . esc_html( $term->name ) . '</a>'; } echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. } } /** * Render column: product_tag. */ protected function render_product_tag_column() { $terms = get_the_terms( $this->object->get_id(), 'product_tag' ); if ( ! $terms ) { echo '<span class="na">–</span>'; } else { $termlist = array(); foreach ( $terms as $term ) { $termlist[] = '<a href="' . esc_url( admin_url( 'edit.php?product_tag=' . $term->slug . '&post_type=product' ) ) . ' ">' . esc_html( $term->name ) . '</a>'; } echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. } } /** * Render column: featured. */ protected function render_featured_column() { $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' ); echo '<a href="' . esc_url( $url ) . '" aria-label="' . esc_attr__( 'Toggle featured', 'woocommerce' ) . '">'; if ( $this->object->is_featured() ) { echo '<span class="wc-featured tips" data-tip="' . esc_attr__( 'Yes', 'woocommerce' ) . '">' . esc_html__( 'Yes', 'woocommerce' ) . '</span>'; } else { echo '<span class="wc-featured not-featured tips" data-tip="' . esc_attr__( 'No', 'woocommerce' ) . '">' . esc_html__( 'No', 'woocommerce' ) . '</span>'; } echo '</a>'; } /** * Render column: is_in_stock. */ protected function render_is_in_stock_column() { if ( $this->object->is_on_backorder() ) { $stock_html = '<mark class="onbackorder">' . __( 'On backorder', 'woocommerce' ) . '</mark>'; } elseif ( $this->object->is_in_stock() ) { $stock_html = '<mark class="instock">' . __( 'In stock', 'woocommerce' ) . '</mark>'; } else { $stock_html = '<mark class="outofstock">' . __( 'Out of stock', 'woocommerce' ) . '</mark>'; } if ( $this->object->managing_stock() ) { $stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')'; } echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) ); } /** * Query vars for custom searches. * * @param mixed $public_query_vars Array of query vars. * @return array */ public function add_custom_query_var( $public_query_vars ) { $public_query_vars[] = 'sku'; return $public_query_vars; } /** * Render any custom filters and search inputs for the list table. */ protected function render_filters() { $filters = apply_filters( 'woocommerce_products_admin_list_table_filters', array( 'product_category' => array( $this, 'render_products_category_filter' ), 'product_type' => array( $this, 'render_products_type_filter' ), 'stock_status' => array( $this, 'render_products_stock_status_filter' ), ) ); ob_start(); foreach ( $filters as $filter_callback ) { call_user_func( $filter_callback ); } $output = ob_get_clean(); echo apply_filters( 'woocommerce_product_filters', $output ); // WPCS: XSS ok. } /** * Render the product category filter for the list table. * * @since 3.5.0 */ protected function render_products_category_filter() { $categories_count = (int) wp_count_terms( 'product_cat' ); if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) { wc_product_dropdown_categories( array( 'option_select_text' => __( 'Filter by category', 'woocommerce' ), 'hide_empty' => 0, ) ); } else { $current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // WPCS: input var ok, CSRF ok. $current_category = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false; ?> <select class="wc-category-search" name="product_cat" data-placeholder="<?php esc_attr_e( 'Filter by category', 'woocommerce' ); ?>" data-allow_clear="true"> <?php if ( $current_category_slug && $current_category ) : ?> <option value="<?php echo esc_attr( $current_category_slug ); ?>" selected="selected"><?php echo esc_html( htmlspecialchars( wp_kses_post( $current_category->name ) ) ); ?></option> <?php endif; ?> </select> <?php } } /** * Render the product type filter for the list table. * * @since 3.5.0 */ protected function render_products_type_filter() { $current_product_type = isset( $_REQUEST['product_type'] ) ? wc_clean( wp_unslash( $_REQUEST['product_type'] ) ) : false; // WPCS: input var ok, sanitization ok. $output = '<select name="product_type" id="dropdown_product_type"><option value="">' . esc_html__( 'Filter by product type', 'woocommerce' ) . '</option>'; foreach ( wc_get_product_types() as $value => $label ) { $output .= '<option value="' . esc_attr( $value ) . '" '; $output .= selected( $value, $current_product_type, false ); $output .= '>' . esc_html( $label ) . '</option>'; if ( 'simple' === $value ) { $output .= '<option value="downloadable" '; $output .= selected( 'downloadable', $current_product_type, false ); $output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . esc_html__( 'Downloadable', 'woocommerce' ) . '</option>'; $output .= '<option value="virtual" '; $output .= selected( 'virtual', $current_product_type, false ); $output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . esc_html__( 'Virtual', 'woocommerce' ) . '</option>'; } } $output .= '</select>'; echo $output; // WPCS: XSS ok. } /** * Render the stock status filter for the list table. * * @since 3.5.0 */ public function render_products_stock_status_filter() { $current_stock_status = isset( $_REQUEST['stock_status'] ) ? wc_clean( wp_unslash( $_REQUEST['stock_status'] ) ) : false; // WPCS: input var ok, sanitization ok. $stock_statuses = wc_get_product_stock_status_options(); $output = '<select name="stock_status"><option value="">' . esc_html__( 'Filter by stock status', 'woocommerce' ) . '</option>'; foreach ( $stock_statuses as $status => $label ) { $output .= '<option ' . selected( $status, $current_stock_status, false ) . ' value="' . esc_attr( $status ) . '">' . esc_html( $label ) . '</option>'; } $output .= '</select>'; echo $output; // WPCS: XSS ok. } /** * Search by SKU or ID for products. * * @deprecated 4.4.0 Logic moved to query_filters. * @param string $where Where clause SQL. * @return string */ public function sku_search( $where ) { wc_deprecated_function( 'WC_Admin_List_Table_Products::sku_search', '4.4.0', 'Logic moved to query_filters.' ); return $where; } /** * Change views on the edit product screen. * * @param array $views Array of views. * @return array */ public function product_views( $views ) { global $wp_query; // Products do not have authors. unset( $views['mine'] ); // Add sorting link. if ( current_user_can( 'edit_others_products' ) ) { $class = ( isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) ? 'current' : ''; $query_string = remove_query_arg( array( 'orderby', 'order' ) ); $query_string = add_query_arg( 'orderby', rawurlencode( 'menu_order title' ), $query_string ); $query_string = add_query_arg( 'order', rawurlencode( 'ASC' ), $query_string ); $views['byorder'] = '<a href="' . esc_url( $query_string ) . '" class="' . esc_attr( $class ) . '">' . __( 'Sorting', 'woocommerce' ) . '</a>'; } return $views; } /** * Change the label when searching products * * @param string $query Search Query. * @return string */ public function search_label( $query ) { global $pagenow, $typenow; if ( 'edit.php' !== $pagenow || 'product' !== $typenow || ! get_query_var( 'product_search' ) || ! isset( $_GET['s'] ) ) { // WPCS: input var ok. return $query; } return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok. } /** * Handle any custom filters. * * @param array $query_vars Query vars. * @return array */ protected function query_filters( $query_vars ) { $this->remove_ordering_args(); // Custom order by arguments. if ( isset( $query_vars['orderby'] ) ) { $orderby = strtolower( $query_vars['orderby'] ); $order = isset( $query_vars['order'] ) ? strtoupper( $query_vars['order'] ) : 'DESC'; if ( 'price' === $orderby ) { $callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses'; add_filter( 'posts_clauses', array( $this, $callback ) ); } if ( 'sku' === $orderby ) { $callback = 'DESC' === $order ? 'order_by_sku_desc_post_clauses' : 'order_by_sku_asc_post_clauses'; add_filter( 'posts_clauses', array( $this, $callback ) ); } } // Type filtering. if ( isset( $query_vars['product_type'] ) ) { if ( 'downloadable' === $query_vars['product_type'] ) { $query_vars['product_type'] = ''; add_filter( 'posts_clauses', array( $this, 'filter_downloadable_post_clauses' ) ); } elseif ( 'virtual' === $query_vars['product_type'] ) { $query_vars['product_type'] = ''; add_filter( 'posts_clauses', array( $this, 'filter_virtual_post_clauses' ) ); } } // Stock status filter. if ( ! empty( $_GET['stock_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended add_filter( 'posts_clauses', array( $this, 'filter_stock_status_post_clauses' ) ); } // Shipping class taxonomy. if ( ! empty( $_GET['product_shipping_class'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $query_vars['tax_query'][] = array( 'taxonomy' => 'product_shipping_class', 'field' => 'slug', 'terms' => sanitize_title( wp_unslash( $_GET['product_shipping_class'] ) ), 'operator' => 'IN', ); } // Search using CRUD. if ( ! empty( $query_vars['s'] ) ) { $data_store = WC_Data_Store::load( 'product' ); $ids = $data_store->search_products( wc_clean( wp_unslash( $query_vars['s'] ) ), '', true, true ); $query_vars['post__in'] = array_merge( $ids, array( 0 ) ); $query_vars['product_search'] = true; unset( $query_vars['s'] ); } return $query_vars; } /** * Undocumented function * * @param array $args Array of SELECT statement pieces (from, where, etc). * @param WP_Query $query WP_Query instance. * @return array */ public function posts_clauses( $args, $query ) { return $args; } /** * Remove ordering queries. * * @param array $posts Posts array, keeping this for backwards compatibility defaulting to empty array. * @return array */ public function remove_ordering_args( $posts = array() ) { remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_sku_asc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_sku_desc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'filter_downloadable_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'filter_virtual_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'filter_stock_status_post_clauses' ) ); return $posts; // Keeping this here for backward compatibility. } /** * Handle numeric price sorting. * * @param array $args Query args. * @return array */ public function order_by_price_asc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC '; return $args; } /** * Handle numeric price sorting. * * @param array $args Query args. * @return array */ public function order_by_price_desc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC '; return $args; } /** * Handle sku sorting. * * @param array $args Query args. * @return array */ public function order_by_sku_asc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.sku ASC, wc_product_meta_lookup.product_id ASC '; return $args; } /** * Handle sku sorting. * * @param array $args Query args. * @return array */ public function order_by_sku_desc_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['orderby'] = ' wc_product_meta_lookup.sku DESC, wc_product_meta_lookup.product_id DESC '; return $args; } /** * Filter by type. * * @param array $args Query args. * @return array */ public function filter_downloadable_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= ' AND wc_product_meta_lookup.downloadable=1 '; return $args; } /** * Filter by type. * * @param array $args Query args. * @return array */ public function filter_virtual_post_clauses( $args ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= ' AND wc_product_meta_lookup.virtual=1 '; return $args; } /** * Filter by stock status. * * @param array $args Query args. * @return array */ public function filter_stock_status_post_clauses( $args ) { global $wpdb; if ( ! empty( $_GET['stock_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.stock_status=%s ', wc_clean( wp_unslash( $_GET['stock_status'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } return $args; } /** * Join wc_product_meta_lookup to posts if not already joined. * * @param string $sql SQL join. * @return string */ private function append_product_sorting_table_join( $sql ) { global $wpdb; if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; } return $sql; } /** * Modifies post query so that it includes parent products whose variations have particular shipping class assigned. * * @param array $pieces Array of SELECT statement pieces (from, where, etc). * @param WP_Query $wp_query WP_Query instance. * @return array Array of products, including parents of variations. */ public function add_variation_parents_for_shipping_class( $pieces, $wp_query ) { global $wpdb; if ( isset( $_GET['product_shipping_class'] ) && '0' !== $_GET['product_shipping_class'] ) { // WPCS: input var ok. $replaced_where = str_replace( ".post_type = 'product'", ".post_type = 'product_variation'", $pieces['where'] ); $pieces['where'] .= " OR {$wpdb->posts}.ID in ( SELECT {$wpdb->posts}.post_parent FROM {$wpdb->posts} LEFT JOIN {$wpdb->term_relationships} ON ({$wpdb->posts}.ID = {$wpdb->term_relationships}.object_id) WHERE 1=1 $replaced_where )"; return $pieces; } return $pieces; } } includes/admin/list-tables/abstract-class-wc-admin-list-table.php 0000644 00000015371 15132754524 0021056 0 ustar 00 <?php /** * List tables. * * @package WooCommerce\Admin * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_List_Table', false ) ) { return; } /** * WC_Admin_List_Table Class. */ abstract class WC_Admin_List_Table { /** * Post type. * * @var string */ protected $list_table_type = ''; /** * Object being shown on the row. * * @var object|null */ protected $object = null; /** * Constructor. */ public function __construct() { if ( $this->list_table_type ) { add_action( 'manage_posts_extra_tablenav', array( $this, 'maybe_render_blank_state' ) ); add_filter( 'view_mode_post_types', array( $this, 'disable_view_mode' ) ); add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) ); add_filter( 'request', array( $this, 'request_query' ) ); add_filter( 'post_row_actions', array( $this, 'row_actions' ), 100, 2 ); add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 ); add_filter( 'list_table_primary_column', array( $this, 'list_table_primary_column' ), 10, 2 ); add_filter( 'manage_edit-' . $this->list_table_type . '_sortable_columns', array( $this, 'define_sortable_columns' ) ); add_filter( 'manage_' . $this->list_table_type . '_posts_columns', array( $this, 'define_columns' ) ); add_filter( 'bulk_actions-edit-' . $this->list_table_type, array( $this, 'define_bulk_actions' ) ); add_action( 'manage_' . $this->list_table_type . '_posts_custom_column', array( $this, 'render_columns' ), 10, 2 ); add_filter( 'handle_bulk_actions-edit-' . $this->list_table_type, array( $this, 'handle_bulk_actions' ), 10, 3 ); } } /** * Show blank slate. * * @param string $which String which tablenav is being shown. */ public function maybe_render_blank_state( $which ) { global $post_type; if ( $post_type === $this->list_table_type && 'bottom' === $which ) { $counts = (array) wp_count_posts( $post_type ); unset( $counts['auto-draft'] ); $count = array_sum( $counts ); if ( 0 < $count ) { return; } $this->render_blank_state(); echo '<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions, .wrap .subsubsub { display: none; } #posts-filter .tablenav.bottom { height: auto; } </style>'; } } /** * Render blank state. Extend to add content. */ protected function render_blank_state() {} /** * Removes this type from list of post types that support "View Mode" switching. * View mode is seen on posts where you can switch between list or excerpt. Our post types don't support * it, so we want to hide the useless UI from the screen options tab. * * @param array $post_types Array of post types supporting view mode. * @return array Array of post types supporting view mode, without this type. */ public function disable_view_mode( $post_types ) { unset( $post_types[ $this->list_table_type ] ); return $post_types; } /** * See if we should render search filters or not. */ public function restrict_manage_posts() { global $typenow; if ( $this->list_table_type === $typenow ) { $this->render_filters(); } } /** * Handle any filters. * * @param array $query_vars Query vars. * @return array */ public function request_query( $query_vars ) { global $typenow; if ( $this->list_table_type === $typenow ) { return $this->query_filters( $query_vars ); } return $query_vars; } /** * Render any custom filters and search inputs for the list table. */ protected function render_filters() {} /** * Handle any custom filters. * * @param array $query_vars Query vars. * @return array */ protected function query_filters( $query_vars ) { return $query_vars; } /** * Set row actions. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ public function row_actions( $actions, $post ) { if ( $this->list_table_type === $post->post_type ) { return $this->get_row_actions( $actions, $post ); } return $actions; } /** * Get row actions to show in the list table. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ protected function get_row_actions( $actions, $post ) { return $actions; } /** * Adjust which columns are displayed by default. * * @param array $hidden Current hidden columns. * @param object $screen Current screen. * @return array */ public function default_hidden_columns( $hidden, $screen ) { if ( isset( $screen->id ) && 'edit-' . $this->list_table_type === $screen->id ) { $hidden = array_merge( $hidden, $this->define_hidden_columns() ); } return $hidden; } /** * Set list table primary column. * * @param string $default Default value. * @param string $screen_id Current screen ID. * @return string */ public function list_table_primary_column( $default, $screen_id ) { if ( 'edit-' . $this->list_table_type === $screen_id && $this->get_primary_column() ) { return $this->get_primary_column(); } return $default; } /** * Define primary column. * * @return array */ protected function get_primary_column() { return ''; } /** * Define hidden columns. * * @return array */ protected function define_hidden_columns() { return array(); } /** * Define which columns are sortable. * * @param array $columns Existing columns. * @return array */ public function define_sortable_columns( $columns ) { return $columns; } /** * Define which columns to show on this screen. * * @param array $columns Existing columns. * @return array */ public function define_columns( $columns ) { return $columns; } /** * Define bulk actions. * * @param array $actions Existing actions. * @return array */ public function define_bulk_actions( $actions ) { return $actions; } /** * Pre-fetch any data for the row each column has access to it. * * @param int $post_id Post ID being shown. */ protected function prepare_row_data( $post_id ) {} /** * Render individual columns. * * @param string $column Column ID to render. * @param int $post_id Post ID being shown. */ public function render_columns( $column, $post_id ) { $this->prepare_row_data( $post_id ); if ( ! $this->object ) { return; } if ( is_callable( array( $this, 'render_' . $column . '_column' ) ) ) { $this->{"render_{$column}_column"}(); } } /** * Handle bulk actions. * * @param string $redirect_to URL to redirect to. * @param string $action Action name. * @param array $ids List of ids. * @return string */ public function handle_bulk_actions( $redirect_to, $action, $ids ) { return esc_url_raw( $redirect_to ); } } includes/admin/class-wc-admin-importers.php 0000644 00000024134 15132754524 0015013 0 ustar 00 <?php /** * Init WooCommerce data importers. * * @package WooCommerce\Admin */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * WC_Admin_Importers Class. */ class WC_Admin_Importers { /** * Array of importer IDs. * * @var string[] */ protected $importers = array(); /** * Constructor. */ public function __construct() { if ( ! $this->import_allowed() ) { return; } add_action( 'admin_menu', array( $this, 'add_to_menus' ) ); add_action( 'admin_init', array( $this, 'register_importers' ) ); add_action( 'admin_head', array( $this, 'hide_from_menus' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); add_action( 'wp_ajax_woocommerce_do_ajax_product_import', array( $this, 'do_ajax_product_import' ) ); // Register WooCommerce importers. $this->importers['product_importer'] = array( 'menu' => 'edit.php?post_type=product', 'name' => __( 'Product Import', 'woocommerce' ), 'capability' => 'import', 'callback' => array( $this, 'product_importer' ), ); } /** * Return true if WooCommerce imports are allowed for current user, false otherwise. * * @return bool Whether current user can perform imports. */ protected function import_allowed() { return current_user_can( 'edit_products' ) && current_user_can( 'import' ); } /** * Add menu items for our custom importers. */ public function add_to_menus() { foreach ( $this->importers as $id => $importer ) { add_submenu_page( $importer['menu'], $importer['name'], $importer['name'], $importer['capability'], $id, $importer['callback'] ); } } /** * Hide menu items from view so the pages exist, but the menu items do not. */ public function hide_from_menus() { global $submenu; foreach ( $this->importers as $id => $importer ) { if ( isset( $submenu[ $importer['menu'] ] ) ) { foreach ( $submenu[ $importer['menu'] ] as $key => $menu ) { if ( $id === $menu[2] ) { unset( $submenu[ $importer['menu'] ][ $key ] ); } } } } } /** * Register importer scripts. */ public function admin_scripts() { $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_register_script( 'wc-product-import', WC()->plugin_url() . '/assets/js/admin/wc-product-import' . $suffix . '.js', array( 'jquery' ), $version, true ); } /** * The product importer. * * This has a custom screen - the Tools > Import item is a placeholder. * If we're on that screen, redirect to the custom one. */ public function product_importer() { if ( Constants::is_defined( 'WP_LOAD_IMPORTERS' ) ) { wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=product_importer' ) ); exit; } include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php'; $importer = new WC_Product_CSV_Importer_Controller(); $importer->dispatch(); } /** * Register WordPress based importers. */ public function register_importers() { if ( Constants::is_defined( 'WP_LOAD_IMPORTERS' ) ) { add_action( 'import_start', array( $this, 'post_importer_compatibility' ) ); register_importer( 'woocommerce_product_csv', __( 'WooCommerce products (CSV)', 'woocommerce' ), __( 'Import <strong>products</strong> to your store via a csv file.', 'woocommerce' ), array( $this, 'product_importer' ) ); register_importer( 'woocommerce_tax_rate_csv', __( 'WooCommerce tax rates (CSV)', 'woocommerce' ), __( 'Import <strong>tax rates</strong> to your store via a csv file.', 'woocommerce' ), array( $this, 'tax_rates_importer' ) ); } } /** * The tax rate importer which extends WP_Importer. */ public function tax_rates_importer() { require_once ABSPATH . 'wp-admin/includes/import.php'; if ( ! class_exists( 'WP_Importer' ) ) { $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php'; if ( file_exists( $class_wp_importer ) ) { require $class_wp_importer; } } require dirname( __FILE__ ) . '/importers/class-wc-tax-rate-importer.php'; $importer = new WC_Tax_Rate_Importer(); $importer->dispatch(); } /** * When running the WP XML importer, ensure attributes exist. * * WordPress import should work - however, it fails to import custom product attribute taxonomies. * This code grabs the file before it is imported and ensures the taxonomies are created. */ public function post_importer_compatibility() { global $wpdb; if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { // PHPCS: input var ok, CSRF ok. return; } $id = absint( $_POST['import_id'] ); // PHPCS: input var ok. $file = get_attached_file( $id ); $parser = new WXR_Parser(); $import_data = $parser->parse( $file ); if ( isset( $import_data['posts'] ) && ! empty( $import_data['posts'] ) ) { foreach ( $import_data['posts'] as $post ) { if ( 'product' === $post['post_type'] && ! empty( $post['terms'] ) ) { foreach ( $post['terms'] as $term ) { if ( strstr( $term['domain'], 'pa_' ) ) { if ( ! taxonomy_exists( $term['domain'] ) ) { $attribute_name = wc_attribute_taxonomy_slug( $term['domain'] ); // Create the taxonomy. if ( ! in_array( $attribute_name, wc_get_attribute_taxonomies(), true ) ) { wc_create_attribute( array( 'name' => $attribute_name, 'slug' => $attribute_name, 'type' => 'select', 'order_by' => 'menu_order', 'has_archives' => false, ) ); } // Register the taxonomy now so that the import works! register_taxonomy( $term['domain'], apply_filters( 'woocommerce_taxonomy_objects_' . $term['domain'], array( 'product' ) ), apply_filters( 'woocommerce_taxonomy_args_' . $term['domain'], array( 'hierarchical' => true, 'show_ui' => false, 'query_var' => true, 'rewrite' => false, ) ) ); } } } } } } } /** * Ajax callback for importing one batch of products from a CSV. */ public function do_ajax_product_import() { global $wpdb; check_ajax_referer( 'wc-product-import', 'security' ); if ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok. wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) ); } include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php'; include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; $file = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok. $params = array( 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), 'parse' => true, ); // Log failures. if ( 0 !== $params['start_pos'] ) { $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); } else { $error_log = array(); } $importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params ); $results = $importer->import(); $percent_complete = $importer->get_percent_complete(); $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); if ( 100 === $percent_complete ) { // @codingStandardsIgnoreStart. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); $wpdb->delete( $wpdb->posts, array( 'post_type' => 'product', 'post_status' => 'importing', ) ); $wpdb->delete( $wpdb->posts, array( 'post_type' => 'product_variation', 'post_status' => 'importing', ) ); // @codingStandardsIgnoreEnd. // Clean up orphaned data. $wpdb->query( " DELETE {$wpdb->posts}.* FROM {$wpdb->posts} LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' " ); $wpdb->query( " DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id WHERE wp.ID IS NULL " ); // @codingStandardsIgnoreStart. $wpdb->query( " DELETE tr.* FROM {$wpdb->term_relationships} tr LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE wp.ID IS NULL AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) " ); // @codingStandardsIgnoreEnd. // Send success. wp_send_json_success( array( 'position' => 'done', 'percentage' => 100, 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), 'imported' => count( $results['imported'] ), 'failed' => count( $results['failed'] ), 'updated' => count( $results['updated'] ), 'skipped' => count( $results['skipped'] ), ) ); } else { wp_send_json_success( array( 'position' => $importer->get_file_position(), 'percentage' => $percent_complete, 'imported' => count( $results['imported'] ), 'failed' => count( $results['failed'] ), 'updated' => count( $results['updated'] ), 'skipped' => count( $results['skipped'] ), ) ); } } } new WC_Admin_Importers(); includes/admin/class-wc-admin-reports.php 0000644 00000012350 15132754524 0014462 0 ustar 00 <?php /** * Admin Reports * * Functions used for displaying sales and customer reports in admin. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Reports * @version 2.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_Reports', false ) ) { return; } /** * WC_Admin_Reports Class. */ class WC_Admin_Reports { /** * Handles output of the reports page in admin. */ public static function output() { $reports = self::get_reports(); $first_tab = array_keys( $reports ); $current_tab = ! empty( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $reports ) ? sanitize_title( $_GET['tab'] ) : $first_tab[0]; $current_report = isset( $_GET['report'] ) ? sanitize_title( $_GET['report'] ) : current( array_keys( $reports[ $current_tab ]['reports'] ) ); include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; include_once dirname( __FILE__ ) . '/views/html-admin-page-reports.php'; } /** * Returns the definitions for the reports to show in admin. * * @return array */ public static function get_reports() { $reports = array( 'orders' => array( 'title' => __( 'Orders', 'woocommerce' ), 'reports' => array( 'sales_by_date' => array( 'title' => __( 'Sales by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'sales_by_product' => array( 'title' => __( 'Sales by product', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'sales_by_category' => array( 'title' => __( 'Sales by category', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'coupon_usage' => array( 'title' => __( 'Coupons by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'downloads' => array( 'title' => __( 'Customer downloads', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), 'customers' => array( 'title' => __( 'Customers', 'woocommerce' ), 'reports' => array( 'customers' => array( 'title' => __( 'Customers vs. guests', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'customer_list' => array( 'title' => __( 'Customer list', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), 'stock' => array( 'title' => __( 'Stock', 'woocommerce' ), 'reports' => array( 'low_in_stock' => array( 'title' => __( 'Low in stock', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'out_of_stock' => array( 'title' => __( 'Out of stock', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'most_stocked' => array( 'title' => __( 'Most stocked', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), ); if ( wc_tax_enabled() ) { $reports['taxes'] = array( 'title' => __( 'Taxes', 'woocommerce' ), 'reports' => array( 'taxes_by_code' => array( 'title' => __( 'Taxes by code', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'taxes_by_date' => array( 'title' => __( 'Taxes by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ); } $reports = apply_filters( 'woocommerce_admin_reports', $reports ); $reports = apply_filters( 'woocommerce_reports_charts', $reports ); // Backwards compatibility. foreach ( $reports as $key => $report_group ) { if ( isset( $reports[ $key ]['charts'] ) ) { $reports[ $key ]['reports'] = $reports[ $key ]['charts']; } foreach ( $reports[ $key ]['reports'] as $report_key => $report ) { if ( isset( $reports[ $key ]['reports'][ $report_key ]['function'] ) ) { $reports[ $key ]['reports'][ $report_key ]['callback'] = $reports[ $key ]['reports'][ $report_key ]['function']; } } } return $reports; } /** * Get a report from our reports subfolder. * * @param string $name */ public static function get_report( $name ) { $name = sanitize_title( str_replace( '_', '-', $name ) ); $class = 'WC_Report_' . str_replace( '-', '_', $name ); include_once apply_filters( 'wc_admin_reports_path', 'reports/class-wc-report-' . $name . '.php', $name, $class ); if ( ! class_exists( $class ) ) { return; } $report = new $class(); $report->output_report(); } } includes/admin/class-wc-admin-log-table-list.php 0000644 00000022432 15132754524 0015605 0 ustar 00 <?php /** * WooCommerce Log Table List * * @author WooThemes * @category Admin * @package WooCommerce\Admin * @version 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } class WC_Admin_Log_Table_List extends WP_List_Table { /** * Initialize the log table list. */ public function __construct() { parent::__construct( array( 'singular' => 'log', 'plural' => 'logs', 'ajax' => false, ) ); } /** * Display level dropdown * * @global wpdb $wpdb */ public function level_dropdown() { $levels = array( array( 'value' => WC_Log_Levels::EMERGENCY, 'label' => __( 'Emergency', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::ALERT, 'label' => __( 'Alert', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::CRITICAL, 'label' => __( 'Critical', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::ERROR, 'label' => __( 'Error', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::WARNING, 'label' => __( 'Warning', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::NOTICE, 'label' => __( 'Notice', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::INFO, 'label' => __( 'Info', 'woocommerce' ), ), array( 'value' => WC_Log_Levels::DEBUG, 'label' => __( 'Debug', 'woocommerce' ), ), ); $selected_level = isset( $_REQUEST['level'] ) ? $_REQUEST['level'] : ''; ?> <label for="filter-by-level" class="screen-reader-text"><?php esc_html_e( 'Filter by level', 'woocommerce' ); ?></label> <select name="level" id="filter-by-level"> <option<?php selected( $selected_level, '' ); ?> value=""><?php esc_html_e( 'All levels', 'woocommerce' ); ?></option> <?php foreach ( $levels as $l ) { printf( '<option%1$s value="%2$s">%3$s</option>', selected( $selected_level, $l['value'], false ), esc_attr( $l['value'] ), esc_html( $l['label'] ) ); } ?> </select> <?php } /** * Get list columns. * * @return array */ public function get_columns() { return array( 'cb' => '<input type="checkbox" />', 'timestamp' => __( 'Timestamp', 'woocommerce' ), 'level' => __( 'Level', 'woocommerce' ), 'message' => __( 'Message', 'woocommerce' ), 'source' => __( 'Source', 'woocommerce' ), ); } /** * Column cb. * * @param array $log * @return string */ public function column_cb( $log ) { return sprintf( '<input type="checkbox" name="log[]" value="%1$s" />', esc_attr( $log['log_id'] ) ); } /** * Timestamp column. * * @param array $log * @return string */ public function column_timestamp( $log ) { return esc_html( mysql2date( 'Y-m-d H:i:s', $log['timestamp'] ) ); } /** * Level column. * * @param array $log * @return string */ public function column_level( $log ) { $level_key = WC_Log_Levels::get_severity_level( $log['level'] ); $levels = array( 'emergency' => __( 'Emergency', 'woocommerce' ), 'alert' => __( 'Alert', 'woocommerce' ), 'critical' => __( 'Critical', 'woocommerce' ), 'error' => __( 'Error', 'woocommerce' ), 'warning' => __( 'Warning', 'woocommerce' ), 'notice' => __( 'Notice', 'woocommerce' ), 'info' => __( 'Info', 'woocommerce' ), 'debug' => __( 'Debug', 'woocommerce' ), ); if ( ! isset( $levels[ $level_key ] ) ) { return ''; } $level = $levels[ $level_key ]; $level_class = sanitize_html_class( 'log-level--' . $level_key ); return '<span class="log-level ' . $level_class . '">' . esc_html( $level ) . '</span>'; } /** * Message column. * * @param array $log * @return string */ public function column_message( $log ) { return esc_html( $log['message'] ); } /** * Source column. * * @param array $log * @return string */ public function column_source( $log ) { return esc_html( $log['source'] ); } /** * Get bulk actions. * * @return array */ protected function get_bulk_actions() { return array( 'delete' => __( 'Delete', 'woocommerce' ), ); } /** * Extra controls to be displayed between bulk actions and pagination. * * @param string $which */ protected function extra_tablenav( $which ) { if ( 'top' === $which ) { echo '<div class="alignleft actions">'; $this->level_dropdown(); $this->source_dropdown(); submit_button( __( 'Filter', 'woocommerce' ), '', 'filter-action', false ); echo '</div>'; } } /** * Get a list of sortable columns. * * @return array */ protected function get_sortable_columns() { return array( 'timestamp' => array( 'timestamp', true ), 'level' => array( 'level', true ), 'source' => array( 'source', true ), ); } /** * Display source dropdown * * @global wpdb $wpdb */ protected function source_dropdown() { global $wpdb; $sources = $wpdb->get_col( "SELECT DISTINCT source FROM {$wpdb->prefix}woocommerce_log WHERE source != '' ORDER BY source ASC" ); if ( ! empty( $sources ) ) { $selected_source = isset( $_REQUEST['source'] ) ? $_REQUEST['source'] : ''; ?> <label for="filter-by-source" class="screen-reader-text"><?php esc_html_e( 'Filter by source', 'woocommerce' ); ?></label> <select name="source" id="filter-by-source"> <option<?php selected( $selected_source, '' ); ?> value=""><?php esc_html_e( 'All sources', 'woocommerce' ); ?></option> <?php foreach ( $sources as $s ) { printf( '<option%1$s value="%2$s">%3$s</option>', selected( $selected_source, $s, false ), esc_attr( $s ), esc_html( $s ) ); } ?> </select> <?php } } /** * Prepare table list items. * * @global wpdb $wpdb */ public function prepare_items() { global $wpdb; $this->prepare_column_headers(); $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); $where = $this->get_items_query_where(); $order = $this->get_items_query_order(); $limit = $this->get_items_query_limit(); $offset = $this->get_items_query_offset(); $query_items = " SELECT log_id, timestamp, level, message, source FROM {$wpdb->prefix}woocommerce_log {$where} {$order} {$limit} {$offset} "; $this->items = $wpdb->get_results( $query_items, ARRAY_A ); $query_count = "SELECT COUNT(log_id) FROM {$wpdb->prefix}woocommerce_log {$where}"; $total_items = $wpdb->get_var( $query_count ); $this->set_pagination_args( array( 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => ceil( $total_items / $per_page ), ) ); } /** * Get prepared LIMIT clause for items query * * @global wpdb $wpdb * * @return string Prepared LIMIT clause for items query. */ protected function get_items_query_limit() { global $wpdb; $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); return $wpdb->prepare( 'LIMIT %d', $per_page ); } /** * Get prepared OFFSET clause for items query * * @global wpdb $wpdb * * @return string Prepared OFFSET clause for items query. */ protected function get_items_query_offset() { global $wpdb; $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); $current_page = $this->get_pagenum(); if ( 1 < $current_page ) { $offset = $per_page * ( $current_page - 1 ); } else { $offset = 0; } return $wpdb->prepare( 'OFFSET %d', $offset ); } /** * Get prepared ORDER BY clause for items query * * @return string Prepared ORDER BY clause for items query. */ protected function get_items_query_order() { $valid_orders = array( 'level', 'source', 'timestamp' ); if ( ! empty( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], $valid_orders ) ) { $by = wc_clean( $_REQUEST['orderby'] ); } else { $by = 'timestamp'; } $by = esc_sql( $by ); if ( ! empty( $_REQUEST['order'] ) && 'asc' === strtolower( $_REQUEST['order'] ) ) { $order = 'ASC'; } else { $order = 'DESC'; } return "ORDER BY {$by} {$order}, log_id {$order}"; } /** * Get prepared WHERE clause for items query * * @global wpdb $wpdb * * @return string Prepared WHERE clause for items query. */ protected function get_items_query_where() { global $wpdb; $where_conditions = array(); $where_values = array(); if ( ! empty( $_REQUEST['level'] ) && WC_Log_Levels::is_valid_level( $_REQUEST['level'] ) ) { $where_conditions[] = 'level >= %d'; $where_values[] = WC_Log_Levels::get_level_severity( $_REQUEST['level'] ); } if ( ! empty( $_REQUEST['source'] ) ) { $where_conditions[] = 'source = %s'; $where_values[] = wc_clean( $_REQUEST['source'] ); } if ( ! empty( $_REQUEST['s'] ) ) { $where_conditions[] = 'message like %s'; $where_values[] = '%' . $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) . '%'; } if ( empty( $where_conditions ) ) { return ''; } return $wpdb->prepare( 'WHERE 1 = 1 AND ' . implode( ' AND ', $where_conditions ), $where_values ); } /** * Set _column_headers property for table list */ protected function prepare_column_headers() { $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns(), ); } } includes/admin/class-wc-admin-api-keys.php 0000644 00000017464 15132754524 0014521 0 ustar 00 <?php /** * WooCommerce Admin API Keys Class * * @package WooCommerce\Admin * @version 2.4.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Admin_API_Keys. */ class WC_Admin_API_Keys { /** * Initialize the API Keys admin actions. */ public function __construct() { add_action( 'admin_init', array( $this, 'actions' ) ); add_action( 'woocommerce_settings_page_init', array( $this, 'screen_option' ) ); add_filter( 'woocommerce_save_settings_advanced_keys', array( $this, 'allow_save_settings' ) ); } /** * Check if should allow save settings. * This prevents "Your settings have been saved." notices on the table list. * * @param bool $allow If allow save settings. * @return bool */ public function allow_save_settings( $allow ) { if ( ! isset( $_GET['create-key'], $_GET['edit-key'] ) ) { // WPCS: input var okay, CSRF ok. return false; } return $allow; } /** * Check if is API Keys settings page. * * @return bool */ private function is_api_keys_settings_page() { return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'keys' === $_GET['section']; // WPCS: input var okay, CSRF ok. } /** * Page output. */ public static function page_output() { // Hide the save button. $GLOBALS['hide_save_button'] = true; if ( isset( $_GET['create-key'] ) || isset( $_GET['edit-key'] ) ) { $key_id = isset( $_GET['edit-key'] ) ? absint( $_GET['edit-key'] ) : 0; // WPCS: input var okay, CSRF ok. $key_data = self::get_key_data( $key_id ); $user_id = (int) $key_data['user_id']; if ( $key_id && $user_id && ! current_user_can( 'edit_user', $user_id ) ) { if ( get_current_user_id() !== $user_id ) { wp_die( esc_html__( 'You do not have permission to edit this API Key', 'woocommerce' ) ); } } include dirname( __FILE__ ) . '/settings/views/html-keys-edit.php'; } else { self::table_list_output(); } } /** * Add screen option. */ public function screen_option() { global $keys_table_list; if ( ! isset( $_GET['create-key'] ) && ! isset( $_GET['edit-key'] ) && $this->is_api_keys_settings_page() ) { // WPCS: input var okay, CSRF ok. $keys_table_list = new WC_Admin_API_Keys_Table_List(); // Add screen option. add_screen_option( 'per_page', array( 'default' => 10, 'option' => 'woocommerce_keys_per_page', ) ); } } /** * Table list output. */ private static function table_list_output() { global $wpdb, $keys_table_list; echo '<h2 class="wc-table-list-header">' . esc_html__( 'REST API', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1' ) ) . '" class="add-new-h2">' . esc_html__( 'Add key', 'woocommerce' ) . '</a></h2>'; // Get the API keys count. $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1;" ); if ( absint( $count ) && $count > 0 ) { $keys_table_list->prepare_items(); echo '<input type="hidden" name="page" value="wc-settings" />'; echo '<input type="hidden" name="tab" value="advanced" />'; echo '<input type="hidden" name="section" value="keys" />'; $keys_table_list->views(); $keys_table_list->search_box( __( 'Search key', 'woocommerce' ), 'key' ); $keys_table_list->display(); } else { echo '<div class="woocommerce-BlankState woocommerce-BlankState--api">'; ?> <h2 class="woocommerce-BlankState-message"><?php esc_html_e( 'The WooCommerce REST API allows external apps to view and manage store data. Access is granted only to those with valid API keys.', 'woocommerce' ); ?></h2> <a class="woocommerce-BlankState-cta button-primary button" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1' ) ); ?>"><?php esc_html_e( 'Create an API key', 'woocommerce' ); ?></a> <style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions { display: none; }</style> <?php } } /** * Get key data. * * @param int $key_id API Key ID. * @return array */ private static function get_key_data( $key_id ) { global $wpdb; $empty = array( 'key_id' => 0, 'user_id' => '', 'description' => '', 'permissions' => '', 'truncated_key' => '', 'last_access' => '', ); if ( 0 === $key_id ) { return $empty; } $key = $wpdb->get_row( $wpdb->prepare( "SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ), ARRAY_A ); if ( is_null( $key ) ) { return $empty; } return $key; } /** * API Keys admin actions. */ public function actions() { if ( $this->is_api_keys_settings_page() ) { // Revoke key. if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. $this->revoke_key(); } // Bulk actions. if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['key'] ) ) { // WPCS: input var okay, CSRF ok. $this->bulk_actions(); } } } /** * Notices. */ public static function notices() { if ( isset( $_GET['revoked'] ) ) { // WPCS: input var okay, CSRF ok. $revoked = absint( $_GET['revoked'] ); // WPCS: input var okay, CSRF ok. /* translators: %d: count */ WC_Admin_Settings::add_message( sprintf( _n( '%d API key permanently revoked.', '%d API keys permanently revoked.', $revoked, 'woocommerce' ), $revoked ) ); } } /** * Revoke key. */ private function revoke_key() { global $wpdb; check_admin_referer( 'revoke' ); if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. $key_id = absint( $_REQUEST['revoke-key'] ); // WPCS: input var okay, CSRF ok. $user_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) ); if ( $key_id && $user_id && ( current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id ) ) { $this->remove_key( $key_id ); } else { wp_die( esc_html__( 'You do not have permission to revoke this API Key', 'woocommerce' ) ); } } wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => 1 ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); exit(); } /** * Bulk actions. */ private function bulk_actions() { check_admin_referer( 'woocommerce-settings' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit API Keys', 'woocommerce' ) ); } if ( isset( $_REQUEST['action'] ) ) { // WPCS: input var okay, CSRF ok. $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ); // WPCS: input var okay, CSRF ok. $keys = isset( $_REQUEST['key'] ) ? array_map( 'absint', (array) $_REQUEST['key'] ) : array(); // WPCS: input var okay, CSRF ok. if ( 'revoke' === $action ) { $this->bulk_revoke_key( $keys ); } } } /** * Bulk revoke key. * * @param array $keys API Keys. */ private function bulk_revoke_key( $keys ) { if ( ! current_user_can( 'remove_users' ) ) { wp_die( esc_html__( 'You do not have permission to revoke API Keys', 'woocommerce' ) ); } $qty = 0; foreach ( $keys as $key_id ) { $result = $this->remove_key( $key_id ); if ( $result ) { $qty++; } } // Redirect to webhooks page. wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => $qty ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); exit(); } /** * Remove key. * * @param int $key_id API Key ID. * @return bool */ private function remove_key( $key_id ) { global $wpdb; $delete = $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) ); return $delete; } } new WC_Admin_API_Keys(); includes/admin/class-wc-admin.php 0000644 00000024057 15132754524 0012775 0 ustar 00 <?php /** * WooCommerce Admin * * @class WC_Admin * @package WooCommerce\Admin * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Admin class. */ class WC_Admin { /** * Constructor. */ public function __construct() { add_action( 'init', array( $this, 'includes' ) ); add_action( 'current_screen', array( $this, 'conditional_includes' ) ); add_action( 'admin_init', array( $this, 'buffer' ), 1 ); add_action( 'admin_init', array( $this, 'preview_emails' ) ); add_action( 'admin_init', array( $this, 'prevent_admin_access' ) ); add_action( 'admin_init', array( $this, 'admin_redirects' ) ); add_action( 'admin_footer', 'wc_print_js', 25 ); add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ), 1 ); add_action( 'init', array( 'WC_Site_Tracking', 'init' ) ); // Disable WXR export of schedule action posts. add_filter( 'action_scheduler_post_type_args', array( $this, 'disable_webhook_post_export' ) ); // Add body class for WP 5.3+ compatibility. add_filter( 'admin_body_class', array( $this, 'include_admin_body_class' ), 9999 ); // Add body class for Marketplace and My Subscriptions pages. if ( isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] ) { add_filter( 'admin_body_class', array( 'WC_Admin_Addons', 'filter_admin_body_classes' ) ); } } /** * Output buffering allows admin screens to make redirects later on. */ public function buffer() { ob_start(); } /** * Include any classes we need within admin. */ public function includes() { include_once __DIR__ . '/wc-admin-functions.php'; include_once __DIR__ . '/wc-meta-box-functions.php'; include_once __DIR__ . '/class-wc-admin-post-types.php'; include_once __DIR__ . '/class-wc-admin-taxonomies.php'; include_once __DIR__ . '/class-wc-admin-menus.php'; include_once __DIR__ . '/class-wc-admin-customize.php'; include_once __DIR__ . '/class-wc-admin-notices.php'; include_once __DIR__ . '/class-wc-admin-assets.php'; include_once __DIR__ . '/class-wc-admin-api-keys.php'; include_once __DIR__ . '/class-wc-admin-webhooks.php'; include_once __DIR__ . '/class-wc-admin-pointers.php'; include_once __DIR__ . '/class-wc-admin-importers.php'; include_once __DIR__ . '/class-wc-admin-exporters.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-footer-pixel.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-site-tracking.php'; // Help Tabs. if ( apply_filters( 'woocommerce_enable_admin_help_tab', true ) ) { include_once __DIR__ . '/class-wc-admin-help.php'; } // Helper. include_once __DIR__ . '/helper/class-wc-helper.php'; // Marketplace suggestions & related REST API. include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-suggestions.php'; include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-updater.php'; } /** * Include admin files conditionally. */ public function conditional_includes() { $screen = get_current_screen(); if ( ! $screen ) { return; } switch ( $screen->id ) { case 'dashboard': case 'dashboard-network': include __DIR__ . '/class-wc-admin-dashboard-setup.php'; include __DIR__ . '/class-wc-admin-dashboard.php'; break; case 'options-permalink': include __DIR__ . '/class-wc-admin-permalink-settings.php'; break; case 'plugins': include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php'; break; case 'update-core': include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php'; break; case 'users': case 'user': case 'profile': case 'user-edit': include __DIR__ . '/class-wc-admin-profile.php'; break; } } /** * Handle redirects to setup/welcome page after install and updates. * * The user must have access rights, and we must ignore the network/bulk plugin updaters. */ public function admin_redirects() { // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. // That means OBW would never be shown. if ( wc_is_running_from_async_action_scheduler() ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended // Nonced plugin install redirects. if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) { $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); } else { $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); } wp_safe_redirect( $url ); exit; } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. */ public function prevent_admin_access() { $prevent_access = false; if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! is_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) { $has_cap = false; $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); foreach ( $access_caps as $access_cap ) { if ( current_user_can( $access_cap ) ) { $has_cap = true; break; } } if ( ! $has_cap ) { $prevent_access = true; } } if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } /** * Preview email template. */ public function preview_emails() { if ( isset( $_GET['preview_woocommerce_mail'] ) ) { if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) { die( 'Security check' ); } // load the mailer class. $mailer = WC()->mailer(); // get the preview email subject. $email_heading = __( 'HTML email template', 'woocommerce' ); // get the preview email content. ob_start(); include __DIR__ . '/views/html-email-template-preview.php'; $message = ob_get_clean(); // create a new email. $email = new WC_Email(); // wrap the content with the email template and then add styles. $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); // print the preview email. // phpcs:ignore WordPress.Security.EscapeOutput echo $message; // phpcs:enable exit; } } /** * Change the admin footer text on WooCommerce admin pages. * * @since 2.3 * @param string $footer_text text to be rendered in the footer. * @return string */ public function admin_footer_text( $footer_text ) { if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { return $footer_text; } $current_screen = get_current_screen(); $wc_pages = wc_get_screen_ids(); // Set only WC pages. $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); // Check to make sure we're on a WooCommerce admin page. if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) { // Change the footer text. if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { $footer_text = sprintf( /* translators: 1: WooCommerce 2:: five stars */ __( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ), sprintf( '<strong>%s</strong>', esc_html__( 'WooCommerce', 'woocommerce' ) ), '<a href="https://wordpress.org/support/plugin/woocommerce/reviews?rate=5#new-post" target="_blank" class="wc-rating-link" aria-label="' . esc_attr__( 'five star', 'woocommerce' ) . '" data-rated="' . esc_attr__( 'Thanks :)', 'woocommerce' ) . '">★★★★★</a>' ); wc_enqueue_js( "jQuery( 'a.wc-rating-link' ).on( 'click', function() { jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); });" ); } else { $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); } } return $footer_text; } /** * Check on a Jetpack install queued by the Setup Wizard. * * See: WC_Admin_Setup_Wizard::install_jetpack() */ public function setup_wizard_check_jetpack() { $jetpack_active = class_exists( 'Jetpack' ); wp_send_json_success( array( 'is_active' => $jetpack_active ? 'yes' : 'no', ) ); } /** * Disable WXR export of scheduled action posts. * * @since 3.6.2 * * @param array $args Scehduled action post type registration args. * * @return array */ public function disable_webhook_post_export( $args ) { $args['can_export'] = false; return $args; } /** * Include admin classes. * * @since 4.2.0 * @param string $classes Body classes string. * @return string */ public function include_admin_body_class( $classes ) { if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { return $classes; } $raw_version = get_bloginfo( 'version' ); $version_parts = explode( '-', $raw_version ); $version = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version; // Add WP 5.3+ compatibility class. if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) { $classes .= ' wc-wp-version-gte-53'; } // Add WP 5.5+ compatibility class. if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) { $classes .= ' wc-wp-version-gte-55'; } return $classes; } } return new WC_Admin(); includes/admin/class-wc-admin-webhooks.php 0000644 00000026347 15132754524 0014620 0 ustar 00 <?php /** * WooCommerce Admin Webhooks Class * * @package WooCommerce\Admin * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Admin_Webhooks. */ class WC_Admin_Webhooks { /** * Initialize the webhooks admin actions. */ public function __construct() { add_action( 'admin_init', array( $this, 'actions' ) ); add_action( 'woocommerce_settings_page_init', array( $this, 'screen_option' ) ); add_filter( 'woocommerce_save_settings_advanced_webhooks', array( $this, 'allow_save_settings' ) ); } /** * Check if should allow save settings. * This prevents "Your settings have been saved." notices on the table list. * * @param bool $allow If allow save settings. * @return bool */ public function allow_save_settings( $allow ) { if ( ! isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok. return false; } return $allow; } /** * Check if is webhook settings page. * * @return bool */ private function is_webhook_settings_page() { return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'webhooks' === $_GET['section']; // WPCS: input var okay, CSRF ok. } /** * Save method. */ private function save() { check_admin_referer( 'woocommerce-settings' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to update Webhooks', 'woocommerce' ) ); } $errors = array(); $webhook_id = isset( $_POST['webhook_id'] ) ? absint( $_POST['webhook_id'] ) : 0; // WPCS: input var okay, CSRF ok. $webhook = new WC_Webhook( $webhook_id ); // Name. if ( ! empty( $_POST['webhook_name'] ) ) { // WPCS: input var okay, CSRF ok. $name = sanitize_text_field( wp_unslash( $_POST['webhook_name'] ) ); // WPCS: input var okay, CSRF ok. } else { $name = sprintf( /* translators: %s: date */ __( 'Webhook created on %s', 'woocommerce' ), // @codingStandardsIgnoreStart strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) // @codingStandardsIgnoreEnd ); } $webhook->set_name( $name ); if ( ! $webhook->get_user_id() ) { $webhook->set_user_id( get_current_user_id() ); } // Status. $webhook->set_status( ! empty( $_POST['webhook_status'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_status'] ) ) : 'disabled' ); // WPCS: input var okay, CSRF ok. // Delivery URL. $delivery_url = ! empty( $_POST['webhook_delivery_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_delivery_url'] ) ) : ''; // WPCS: input var okay, CSRF ok. if ( wc_is_valid_url( $delivery_url ) ) { $webhook->set_delivery_url( $delivery_url ); } // Secret. $secret = ! empty( $_POST['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_secret'] ) ) : wp_generate_password( 50, true, true ); // WPCS: input var okay, CSRF ok. $webhook->set_secret( $secret ); // Topic. if ( ! empty( $_POST['webhook_topic'] ) ) { // WPCS: input var okay, CSRF ok. $resource = ''; $event = ''; switch ( $_POST['webhook_topic'] ) { // WPCS: input var okay, CSRF ok. case 'action': $resource = 'action'; $event = ! empty( $_POST['webhook_action_event'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_action_event'] ) ) : ''; // WPCS: input var okay, CSRF ok. break; default: list( $resource, $event ) = explode( '.', sanitize_text_field( wp_unslash( $_POST['webhook_topic'] ) ) ); // WPCS: input var okay, CSRF ok. break; } $topic = $resource . '.' . $event; if ( wc_is_webhook_valid_topic( $topic ) ) { $webhook->set_topic( $topic ); } else { $errors[] = __( 'Webhook topic unknown. Please select a valid topic.', 'woocommerce' ); } } // API version. $rest_api_versions = wc_get_webhook_rest_api_versions(); $webhook->set_api_version( ! empty( $_POST['webhook_api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_api_version'] ) ) : end( $rest_api_versions ) ); // WPCS: input var okay, CSRF ok. $webhook->save(); // Run actions. do_action( 'woocommerce_webhook_options_save', $webhook->get_id() ); if ( $errors ) { // Redirect to webhook edit page to avoid settings save actions. wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( implode( '|', $errors ) ) ) ); exit(); } elseif ( isset( $_POST['webhook_status'] ) && 'active' === $_POST['webhook_status'] && $webhook->get_pending_delivery() ) { // WPCS: input var okay, CSRF ok. // Ping the webhook at the first time that is activated. $result = $webhook->deliver_ping(); if ( is_wp_error( $result ) ) { // Redirect to webhook edit page to avoid settings save actions. wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( $result->get_error_message() ) ) ); exit(); } } // Redirect to webhook edit page to avoid settings save actions. wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&updated=1' ) ); exit(); } /** * Bulk delete. * * @param array $webhooks List of webhooks IDs. */ public static function bulk_delete( $webhooks ) { foreach ( $webhooks as $webhook_id ) { $webhook = new WC_Webhook( (int) $webhook_id ); $webhook->delete( true ); } $qty = count( $webhooks ); $status = isset( $_GET['status'] ) ? '&status=' . sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; // WPCS: input var okay, CSRF ok. // Redirect to webhooks page. wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' . $status . '&deleted=' . $qty ) ); exit(); } /** * Delete webhook. */ private function delete() { check_admin_referer( 'delete-webhook' ); if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. $webhook_id = absint( $_GET['delete'] ); // WPCS: input var okay, CSRF ok. if ( $webhook_id ) { $this->bulk_delete( array( $webhook_id ) ); } } } /** * Webhooks admin actions. */ public function actions() { if ( $this->is_webhook_settings_page() ) { // Save. if ( isset( $_POST['save'] ) && isset( $_POST['webhook_id'] ) ) { // WPCS: input var okay, CSRF ok. $this->save(); } // Delete webhook. if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. $this->delete(); } } } /** * Page output. */ public static function page_output() { // Hide the save button. $GLOBALS['hide_save_button'] = true; if ( isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok. $webhook_id = absint( $_GET['edit-webhook'] ); // WPCS: input var okay, CSRF ok. $webhook = new WC_Webhook( $webhook_id ); include __DIR__ . '/settings/views/html-webhooks-edit.php'; return; } self::table_list_output(); } /** * Notices. */ public static function notices() { if ( isset( $_GET['deleted'] ) ) { // WPCS: input var okay, CSRF ok. $deleted = absint( $_GET['deleted'] ); // WPCS: input var okay, CSRF ok. /* translators: %d: count */ WC_Admin_Settings::add_message( sprintf( _n( '%d webhook permanently deleted.', '%d webhooks permanently deleted.', $deleted, 'woocommerce' ), $deleted ) ); } if ( isset( $_GET['updated'] ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::add_message( __( 'Webhook updated successfully.', 'woocommerce' ) ); } if ( isset( $_GET['created'] ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::add_message( __( 'Webhook created successfully.', 'woocommerce' ) ); } if ( isset( $_GET['error'] ) ) { // WPCS: input var okay, CSRF ok. foreach ( explode( '|', sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) as $message ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::add_error( trim( $message ) ); } } } /** * Add screen option. */ public function screen_option() { global $webhooks_table_list; if ( ! isset( $_GET['edit-webhook'] ) && $this->is_webhook_settings_page() ) { // WPCS: input var okay, CSRF ok. $webhooks_table_list = new WC_Admin_Webhooks_Table_List(); // Add screen option. add_screen_option( 'per_page', array( 'default' => 10, 'option' => 'woocommerce_webhooks_per_page', ) ); } } /** * Table list output. */ private static function table_list_output() { global $webhooks_table_list; echo '<h2 class="wc-table-list-header">' . esc_html__( 'Webhooks', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=0' ) ) . '" class="add-new-h2">' . esc_html__( 'Add webhook', 'woocommerce' ) . '</a></h2>'; // Get the webhooks count. $data_store = WC_Data_Store::load( 'webhook' ); $num_webhooks = $data_store->get_count_webhooks_by_status(); $count = array_sum( $num_webhooks ); if ( 0 < $count ) { $webhooks_table_list->process_bulk_action(); $webhooks_table_list->prepare_items(); echo '<input type="hidden" name="page" value="wc-settings" />'; echo '<input type="hidden" name="tab" value="advanced" />'; echo '<input type="hidden" name="section" value="webhooks" />'; $webhooks_table_list->views(); $webhooks_table_list->search_box( __( 'Search webhooks', 'woocommerce' ), 'webhook' ); $webhooks_table_list->display(); } else { echo '<div class="woocommerce-BlankState woocommerce-BlankState--webhooks">'; ?> <h2 class="woocommerce-BlankState-message"><?php esc_html_e( 'Webhooks are event notifications sent to URLs of your choice. They can be used to integrate with third-party services which support them.', 'woocommerce' ); ?></h2> <a class="woocommerce-BlankState-cta button-primary button" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=0' ) ); ?>"><?php esc_html_e( 'Create a new webhook', 'woocommerce' ); ?></a> <style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions { display: none; }</style> <?php } } /** * Logs output. * * @deprecated 3.3.0 * @param WC_Webhook $webhook Deprecated. */ public static function logs_output( $webhook = 'deprecated' ) { wc_deprecated_function( 'WC_Admin_Webhooks::logs_output', '3.3' ); } /** * Get the webhook topic data. * * @param WC_Webhook $webhook Webhook instance. * * @return array */ public static function get_topic_data( $webhook ) { $topic = $webhook->get_topic(); $event = ''; $resource = ''; if ( $topic ) { list( $resource, $event ) = explode( '.', $topic ); if ( 'action' === $resource ) { $topic = 'action'; } elseif ( ! in_array( $resource, array( 'coupon', 'customer', 'order', 'product' ), true ) ) { $topic = 'custom'; } } return array( 'topic' => $topic, 'event' => $event, 'resource' => $resource, ); } /** * Get the logs navigation. * * @deprecated 3.3.0 * @param int $total Deprecated. * @param WC_Webhook $webhook Deprecated. */ public static function get_logs_navigation( $total, $webhook ) { wc_deprecated_function( 'WC_Admin_Webhooks::get_logs_navigation', '3.3' ); } } new WC_Admin_Webhooks(); includes/admin/class-wc-admin-help.php 0000644 00000011273 15132754524 0013717 0 ustar 00 <?php /** * Add some content to the help tab * * @package WooCommerce\Admin * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_Help', false ) ) { return new WC_Admin_Help(); } /** * WC_Admin_Help Class. */ class WC_Admin_Help { /** * Hook in tabs. */ public function __construct() { add_action( 'current_screen', array( $this, 'add_tabs' ), 50 ); } /** * Add help tabs. */ public function add_tabs() { $screen = get_current_screen(); if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids() ) ) { return; } $screen->add_help_tab( array( 'id' => 'woocommerce_support_tab', 'title' => __( 'Help & Support', 'woocommerce' ), 'content' => '<h2>' . __( 'Help & Support', 'woocommerce' ) . '</h2>' . '<p>' . sprintf( /* translators: %s: Documentation URL */ __( 'Should you need help understanding, using, or extending WooCommerce, <a href="%s">please read our documentation</a>. You will find all kinds of resources including snippets, tutorials and much more.', 'woocommerce' ), 'https://docs.woocommerce.com/documentation/plugins/woocommerce/?utm_source=helptab&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin' ) . '</p>' . '<p>' . sprintf( /* translators: %s: Forum URL */ __( 'For further assistance with WooCommerce core, use the <a href="%1$s">community forum</a>. For help with premium extensions sold on WooCommerce.com, <a href="%2$s">open a support request at WooCommerce.com</a>.', 'woocommerce' ), 'https://wordpress.org/support/plugin/woocommerce', 'https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin' ) . '</p>' . '<p>' . __( 'Before asking for help, we recommend checking the system status page to identify any problems with your configuration.', 'woocommerce' ) . '</p>' . '<p><a href="' . admin_url( 'admin.php?page=wc-status' ) . '" class="button button-primary">' . __( 'System status', 'woocommerce' ) . '</a> <a href="https://wordpress.org/support/plugin/woocommerce" class="button">' . __( 'Community forum', 'woocommerce' ) . '</a> <a href="https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin" class="button">' . __( 'WooCommerce.com support', 'woocommerce' ) . '</a></p>', ) ); $screen->add_help_tab( array( 'id' => 'woocommerce_bugs_tab', 'title' => __( 'Found a bug?', 'woocommerce' ), 'content' => '<h2>' . __( 'Found a bug?', 'woocommerce' ) . '</h2>' . /* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */ '<p>' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via <a href="%1$s">Github issues</a>. Ensure you read the <a href="%2$s">contribution guide</a> prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your <a href="%3$s">system status report</a>.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '</p>' . '<p><a href="https://github.com/woocommerce/woocommerce/issues/new?template=4-Bug-report.md" class="button button-primary">' . __( 'Report a bug', 'woocommerce' ) . '</a> <a href="' . admin_url( 'admin.php?page=wc-status' ) . '" class="button">' . __( 'System status', 'woocommerce' ) . '</a></p>', ) ); $screen->set_help_sidebar( '<p><strong>' . __( 'For more information:', 'woocommerce' ) . '</strong></p>' . '<p><a href="https://woocommerce.com/?utm_source=helptab&utm_medium=product&utm_content=about&utm_campaign=woocommerceplugin" target="_blank">' . __( 'About WooCommerce', 'woocommerce' ) . '</a></p>' . '<p><a href="https://wordpress.org/plugins/woocommerce/" target="_blank">' . __( 'WordPress.org project', 'woocommerce' ) . '</a></p>' . '<p><a href="https://github.com/woocommerce/woocommerce/" target="_blank">' . __( 'Github project', 'woocommerce' ) . '</a></p>' . '<p><a href="https://woocommerce.com/storefront/?utm_source=helptab&utm_medium=product&utm_content=wcthemes&utm_campaign=woocommerceplugin" target="_blank">' . __( 'Official theme', 'woocommerce' ) . '</a></p>' . '<p><a href="https://woocommerce.com/product-category/woocommerce-extensions/?utm_source=helptab&utm_medium=product&utm_content=wcextensions&utm_campaign=woocommerceplugin" target="_blank">' . __( 'Official extensions', 'woocommerce' ) . '</a></p>' ); } } return new WC_Admin_Help(); includes/admin/wc-meta-box-functions.php 0000644 00000026506 15132754524 0014325 0 ustar 00 <?php /** * WooCommerce Meta Box Functions * * @author WooThemes * @category Core * @package WooCommerce\Admin\Functions * @version 2.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * Output a text input box. * * @param array $field */ function woocommerce_wp_text_input( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'short'; $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; $field['type'] = isset( $field['type'] ) ? $field['type'] : 'text'; $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; $data_type = empty( $field['data_type'] ) ? '' : $field['data_type']; switch ( $data_type ) { case 'price': $field['class'] .= ' wc_input_price'; $field['value'] = wc_format_localized_price( $field['value'] ); break; case 'decimal': $field['class'] .= ' wc_input_decimal'; $field['value'] = wc_format_localized_decimal( $field['value'] ); break; case 'stock': $field['class'] .= ' wc_input_stock'; $field['value'] = wc_stock_amount( $field['value'] ); break; case 'url': $field['class'] .= ' wc_input_url'; $field['value'] = esc_url( $field['value'] ); break; default: break; } // Custom attribute handling $custom_attributes = array(); if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { foreach ( $field['custom_attributes'] as $attribute => $value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; } } echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '"> <label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>'; if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { echo wc_help_tip( $field['description'] ); } echo '<input type="' . esc_attr( $field['type'] ) . '" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['value'] ) . '" placeholder="' . esc_attr( $field['placeholder'] ) . '" ' . implode( ' ', $custom_attributes ) . ' /> '; if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>'; } echo '</p>'; } /** * Output a hidden input box. * * @param array $field */ function woocommerce_wp_hidden_input( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['class'] = isset( $field['class'] ) ? $field['class'] : ''; echo '<input type="hidden" class="' . esc_attr( $field['class'] ) . '" name="' . esc_attr( $field['id'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['value'] ) . '" /> '; } /** * Output a textarea input box. * * @param array $field */ function woocommerce_wp_textarea_input( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'short'; $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; $field['rows'] = isset( $field['rows'] ) ? $field['rows'] : 2; $field['cols'] = isset( $field['cols'] ) ? $field['cols'] : 20; // Custom attribute handling $custom_attributes = array(); if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { foreach ( $field['custom_attributes'] as $attribute => $value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; } } echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '"> <label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>'; if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { echo wc_help_tip( $field['description'] ); } echo '<textarea class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" placeholder="' . esc_attr( $field['placeholder'] ) . '" rows="' . esc_attr( $field['rows'] ) . '" cols="' . esc_attr( $field['cols'] ) . '" ' . implode( ' ', $custom_attributes ) . '>' . esc_textarea( $field['value'] ) . '</textarea> '; if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>'; } echo '</p>'; } /** * Output a checkbox input box. * * @param array $field */ function woocommerce_wp_checkbox( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'checkbox'; $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['cbvalue'] = isset( $field['cbvalue'] ) ? $field['cbvalue'] : 'yes'; $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; // Custom attribute handling $custom_attributes = array(); if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { foreach ( $field['custom_attributes'] as $attribute => $value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; } } echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '"> <label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>'; if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { echo wc_help_tip( $field['description'] ); } echo '<input type="checkbox" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['cbvalue'] ) . '" ' . checked( $field['value'], $field['cbvalue'], false ) . ' ' . implode( ' ', $custom_attributes ) . '/> '; if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>'; } echo '</p>'; } /** * Output a select input box. * * @param array $field Data about the field to render. */ function woocommerce_wp_select( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field = wp_parse_args( $field, array( 'class' => 'select short', 'style' => '', 'wrapper_class' => '', 'value' => get_post_meta( $thepostid, $field['id'], true ), 'name' => $field['id'], 'desc_tip' => false, 'custom_attributes' => array(), ) ); $wrapper_attributes = array( 'class' => $field['wrapper_class'] . " form-field {$field['id']}_field", ); $label_attributes = array( 'for' => $field['id'], ); $field_attributes = (array) $field['custom_attributes']; $field_attributes['style'] = $field['style']; $field_attributes['id'] = $field['id']; $field_attributes['name'] = $field['name']; $field_attributes['class'] = $field['class']; $tooltip = ! empty( $field['description'] ) && false !== $field['desc_tip'] ? $field['description'] : ''; $description = ! empty( $field['description'] ) && false === $field['desc_tip'] ? $field['description'] : ''; ?> <p <?php echo wc_implode_html_attributes( $wrapper_attributes ); // WPCS: XSS ok. ?>> <label <?php echo wc_implode_html_attributes( $label_attributes ); // WPCS: XSS ok. ?>><?php echo wp_kses_post( $field['label'] ); ?></label> <?php if ( $tooltip ) : ?> <?php echo wc_help_tip( $tooltip ); // WPCS: XSS ok. ?> <?php endif; ?> <select <?php echo wc_implode_html_attributes( $field_attributes ); // WPCS: XSS ok. ?>> <?php foreach ( $field['options'] as $key => $value ) { echo '<option value="' . esc_attr( $key ) . '"' . wc_selected( $key, $field['value'] ) . '>' . esc_html( $value ) . '</option>'; } ?> </select> <?php if ( $description ) : ?> <span class="description"><?php echo wp_kses_post( $description ); ?></span> <?php endif; ?> </p> <?php } /** * Output a radio input box. * * @param array $field */ function woocommerce_wp_radio( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'select short'; $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; echo '<fieldset class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '"><legend>' . wp_kses_post( $field['label'] ) . '</legend>'; if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { echo wc_help_tip( $field['description'] ); } echo '<ul class="wc-radios">'; foreach ( $field['options'] as $key => $value ) { echo '<li><label><input name="' . esc_attr( $field['name'] ) . '" value="' . esc_attr( $key ) . '" type="radio" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" ' . checked( esc_attr( $field['value'] ), esc_attr( $key ), false ) . ' /> ' . esc_html( $value ) . '</label> </li>'; } echo '</ul>'; if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>'; } echo '</fieldset>'; } includes/admin/class-wc-admin-assets.php 0000644 00000075626 15132754524 0014305 0 ustar 00 <?php /** * Load assets * * @package WooCommerce\Admin * @version 3.7.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Admin_Assets', false ) ) : /** * WC_Admin_Assets Class. */ class WC_Admin_Assets { /** * Hook in tabs. */ public function __construct() { add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); } /** * Enqueue styles. */ public function admin_styles() { global $wp_scripts; $version = Constants::get_constant( 'WC_VERSION' ); $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; // Register admin styles. wp_register_style( 'woocommerce_admin_menu_styles', WC()->plugin_url() . '/assets/css/menu.css', array(), $version ); wp_register_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), $version ); wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), $version ); wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), $version ); wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), $version, 'print' ); wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), $version ); wp_register_style( 'woocommerce_admin_privacy_styles', WC()->plugin_url() . '/assets/css/privacy.css', array(), $version ); // Add RTL support for admin styles. wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' ); wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' ); wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' ); wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' ); wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' ); wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' ); if ( $screen && $screen->is_block_editor() ) { wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version ); wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' ); } // Sitewide menu CSS. wp_enqueue_style( 'woocommerce_admin_menu_styles' ); // Admin styles for WC pages only. if ( in_array( $screen_id, wc_get_screen_ids() ) ) { wp_enqueue_style( 'woocommerce_admin_styles' ); wp_enqueue_style( 'jquery-ui-style' ); wp_enqueue_style( 'wp-color-picker' ); } if ( in_array( $screen_id, array( 'dashboard' ) ) ) { wp_enqueue_style( 'woocommerce_admin_dashboard_styles' ); } if ( in_array( $screen_id, array( 'woocommerce_page_wc-reports', 'toplevel_page_wc-reports' ) ) ) { wp_enqueue_style( 'woocommerce_admin_print_reports_styles' ); } // Privacy Policy Guide css for back-compat. if ( isset( $_GET['wp-privacy-policy-guide'] ) || in_array( $screen_id, array( 'privacy-policy-guide' ) ) ) { wp_enqueue_style( 'woocommerce_admin_privacy_styles' ); } // @deprecated 2.3. if ( has_action( 'woocommerce_admin_css' ) ) { do_action( 'woocommerce_admin_css' ); wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); } if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { wp_enqueue_style( 'woocommerce_admin_marketplace_styles' ); } } /** * Enqueue scripts. */ public function admin_scripts() { global $wp_query, $post; $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); // Register scripts. wp_register_script( 'woocommerce_admin', WC()->plugin_url() . '/assets/js/admin/woocommerce_admin' . $suffix . '.js', array( 'jquery', 'jquery-blockui', 'jquery-ui-sortable', 'jquery-ui-widget', 'jquery-ui-core', 'jquery-tiptip' ), $version ); wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.70', true ); wp_register_script( 'jquery-tiptip', WC()->plugin_url() . '/assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $version, true ); wp_register_script( 'round', WC()->plugin_url() . '/assets/js/round/round' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'wc-admin-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'accounting', 'round', 'wc-enhanced-select', 'plupload-all', 'stupidtable', 'jquery-tiptip' ), $version ); wp_register_script( 'zeroclipboard', WC()->plugin_url() . '/assets/js/zeroclipboard/jquery.zeroclipboard' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'qrcode', WC()->plugin_url() . '/assets/js/jquery-qrcode/jquery.qrcode' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'stupidtable', WC()->plugin_url() . '/assets/js/stupidtable/stupidtable' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'serializejson', WC()->plugin_url() . '/assets/js/jquery-serializejson/jquery.serializejson' . $suffix . '.js', array( 'jquery' ), '2.8.1' ); wp_register_script( 'flot', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'flot-resize', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.resize' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); wp_register_script( 'flot-time', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.time' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); wp_register_script( 'flot-pie', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.pie' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); wp_register_script( 'flot-stack', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.stack' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); wp_register_script( 'wc-settings-tax', WC()->plugin_url() . '/assets/js/admin/settings-views-html-settings-tax' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); wp_register_script( 'wc-backbone-modal', WC()->plugin_url() . '/assets/js/admin/backbone-modal' . $suffix . '.js', array( 'underscore', 'backbone', 'wp-util' ), $version ); wp_register_script( 'wc-shipping-zones', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zones' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-enhanced-select', 'wc-backbone-modal' ), $version ); wp_register_script( 'wc-shipping-zone-methods', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zone-methods' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); wp_register_script( 'wc-shipping-classes', WC()->plugin_url() . '/assets/js/admin/wc-shipping-classes' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone' ), $version ); wp_register_script( 'wc-clipboard', WC()->plugin_url() . '/assets/js/admin/wc-clipboard' . $suffix . '.js', array( 'jquery' ), $version ); wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' ); wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.6' ); wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), $version ); wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true ); wp_localize_script( 'wc-enhanced-select', 'wc_enhanced_select_params', array( 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'search_products_nonce' => wp_create_nonce( 'search-products' ), 'search_customers_nonce' => wp_create_nonce( 'search-customers' ), 'search_categories_nonce' => wp_create_nonce( 'search-categories' ), 'search_pages_nonce' => wp_create_nonce( 'search-pages' ), ) ); wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2' ); wp_localize_script( 'accounting', 'accounting_params', array( 'mon_decimal_point' => wc_get_price_decimal_separator(), ) ); wp_register_script( 'wc-orders', WC()->plugin_url() . '/assets/js/admin/wc-orders' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); wp_localize_script( 'wc-orders', 'wc_orders_params', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'preview_nonce' => wp_create_nonce( 'woocommerce-preview-order' ), ) ); // WooCommerce admin pages. if ( in_array( $screen_id, wc_get_screen_ids() ) ) { wp_enqueue_script( 'iris' ); wp_enqueue_script( 'woocommerce_admin' ); wp_enqueue_script( 'wc-enhanced-select' ); wp_enqueue_script( 'jquery-ui-sortable' ); wp_enqueue_script( 'jquery-ui-autocomplete' ); $locale = localeconv(); $decimal = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; $params = array( /* translators: %s: decimal */ 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), /* translators: %s: price decimal separator */ 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), 'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ), 'decimal_point' => $decimal, 'mon_decimal_point' => wc_get_price_decimal_separator(), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'strings' => array( 'import_products' => __( 'Import', 'woocommerce' ), 'export_products' => __( 'Export', 'woocommerce' ), ), 'nonces' => array( 'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ), ), 'urls' => array( 'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null, 'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null, ), ); wp_localize_script( 'woocommerce_admin', 'woocommerce_admin', $params ); } // Edit product category pages. if ( in_array( $screen_id, array( 'edit-product_cat' ) ) ) { wp_enqueue_media(); } // Products. if ( in_array( $screen_id, array( 'edit-product' ) ) ) { wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version ); $params = array( 'strings' => array( 'allow_reviews' => esc_js( __( 'Enable reviews', 'woocommerce' ) ), ), ); wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); } // Meta boxes. if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { wp_enqueue_media(); wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); wp_enqueue_script( 'wc-admin-product-meta-boxes' ); wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); $params = array( 'post_id' => isset( $post->ID ) ? $post->ID : '', 'plugin_url' => WC()->plugin_url(), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'woocommerce_placeholder_img_src' => wc_placeholder_img_src(), 'add_variation_nonce' => wp_create_nonce( 'add-variation' ), 'link_variation_nonce' => wp_create_nonce( 'link-variations' ), 'delete_variations_nonce' => wp_create_nonce( 'delete-variations' ), 'load_variations_nonce' => wp_create_nonce( 'load-variations' ), 'save_variations_nonce' => wp_create_nonce( 'save-variations' ), 'bulk_edit_variations_nonce' => wp_create_nonce( 'bulk-edit-variations' ), /* translators: %d: Number of variations */ 'i18n_link_all_variations' => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), 'i18n_enter_a_value' => esc_js( __( 'Enter a value', 'woocommerce' ) ), 'i18n_enter_menu_order' => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ), 'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ), 'i18n_delete_all_variations' => esc_js( __( 'Are you sure you want to delete all variations? This cannot be undone.', 'woocommerce' ) ), 'i18n_last_warning' => esc_js( __( 'Last warning, are you sure?', 'woocommerce' ) ), 'i18n_choose_image' => esc_js( __( 'Choose an image', 'woocommerce' ) ), 'i18n_set_image' => esc_js( __( 'Set variation image', 'woocommerce' ) ), 'i18n_variation_added' => esc_js( __( 'variation added', 'woocommerce' ) ), 'i18n_variations_added' => esc_js( __( 'variations added', 'woocommerce' ) ), 'i18n_no_variations_added' => esc_js( __( 'No variations added', 'woocommerce' ) ), 'i18n_remove_variation' => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ), 'i18n_scheduled_sale_start' => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), 'i18n_scheduled_sale_end' => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), 'i18n_edited_variations' => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ), 'i18n_variation_count_single' => esc_js( __( '%qty% variation', 'woocommerce' ) ), 'i18n_variation_count_plural' => esc_js( __( '%qty% variations', 'woocommerce' ) ), 'variations_per_page' => absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ), ); wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); } if ( in_array( str_replace( 'edit-', '', $screen_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { $default_location = wc_get_customer_default_location(); wp_enqueue_script( 'wc-admin-order-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-order' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'wc-backbone-modal', 'selectWoo', 'wc-clipboard' ), $version ); wp_localize_script( 'wc-admin-order-meta-boxes', 'woocommerce_admin_meta_boxes_order', array( 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), 'default_country' => isset( $default_location['country'] ) ? $default_location['country'] : '', 'default_state' => isset( $default_location['state'] ) ? $default_location['state'] : '', 'placeholder_name' => esc_attr__( 'Name (required)', 'woocommerce' ), 'placeholder_value' => esc_attr__( 'Value (required)', 'woocommerce' ), ) ); } if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); wp_localize_script( 'wc-admin-coupon-meta-boxes', 'woocommerce_admin_meta_boxes_coupon', array( 'generate_button_text' => esc_html__( 'Generate coupon code', 'woocommerce' ), 'characters' => apply_filters( 'woocommerce_coupon_code_generator_characters', 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' ), 'char_length' => apply_filters( 'woocommerce_coupon_code_generator_character_length', 8 ), 'prefix' => apply_filters( 'woocommerce_coupon_code_generator_prefix', '' ), 'suffix' => apply_filters( 'woocommerce_coupon_code_generator_suffix', '' ), ) ); } if ( in_array( str_replace( 'edit-', '', $screen_id ), array_merge( array( 'shop_coupon', 'product' ), wc_get_order_types( 'order-meta-boxes' ) ) ) ) { $post_id = isset( $post->ID ) ? $post->ID : ''; $currency = ''; $remove_item_notice = __( 'Are you sure you want to remove the selected items?', 'woocommerce' ); $remove_fee_notice = __( 'Are you sure you want to remove the selected fees?', 'woocommerce' ); $remove_shipping_notice = __( 'Are you sure you want to remove the selected shipping?', 'woocommerce' ); if ( $post_id && in_array( get_post_type( $post_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { $order = wc_get_order( $post_id ); if ( $order ) { $currency = $order->get_currency(); if ( ! $order->has_status( array( 'pending', 'failed', 'cancelled' ) ) ) { $remove_item_notice = $remove_item_notice . ' ' . __( "You may need to manually restore the item's stock.", 'woocommerce' ); } } } $params = array( 'remove_item_notice' => $remove_item_notice, 'remove_fee_notice' => $remove_fee_notice, 'remove_shipping_notice' => $remove_shipping_notice, 'i18n_select_items' => __( 'Please select some items.', 'woocommerce' ), 'i18n_do_refund' => __( 'Are you sure you wish to process this refund? This action cannot be undone.', 'woocommerce' ), 'i18n_delete_refund' => __( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce' ), 'i18n_delete_tax' => __( 'Are you sure you wish to delete this tax column? This action cannot be undone.', 'woocommerce' ), 'remove_item_meta' => __( 'Remove this item meta?', 'woocommerce' ), 'remove_attribute' => __( 'Remove this attribute?', 'woocommerce' ), 'name_label' => __( 'Name', 'woocommerce' ), 'remove_label' => __( 'Remove', 'woocommerce' ), 'click_to_toggle' => __( 'Click to toggle', 'woocommerce' ), 'values_label' => __( 'Value(s)', 'woocommerce' ), 'text_attribute_tip' => __( 'Enter some text, or some attributes by pipe (|) separating values.', 'woocommerce' ), 'visible_label' => __( 'Visible on the product page', 'woocommerce' ), 'used_for_variations_label' => __( 'Used for variations', 'woocommerce' ), 'new_attribute_prompt' => __( 'Enter a name for the new attribute term:', 'woocommerce' ), 'calc_totals' => __( 'Recalculate totals? This will calculate taxes based on the customers country (or the store base country) and update totals.', 'woocommerce' ), 'copy_billing' => __( 'Copy billing information to shipping information? This will remove any currently entered shipping information.', 'woocommerce' ), 'load_billing' => __( "Load the customer's billing information? This will remove any currently entered billing information.", 'woocommerce' ), 'load_shipping' => __( "Load the customer's shipping information? This will remove any currently entered shipping information.", 'woocommerce' ), 'featured_label' => __( 'Featured', 'woocommerce' ), 'prices_include_tax' => esc_attr( get_option( 'woocommerce_prices_include_tax' ) ), 'tax_based_on' => esc_attr( get_option( 'woocommerce_tax_based_on' ) ), 'round_at_subtotal' => esc_attr( get_option( 'woocommerce_tax_round_at_subtotal' ) ), 'no_customer_selected' => __( 'No customer selected', 'woocommerce' ), 'plugin_url' => WC()->plugin_url(), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'order_item_nonce' => wp_create_nonce( 'order-item' ), 'add_attribute_nonce' => wp_create_nonce( 'add-attribute' ), 'save_attributes_nonce' => wp_create_nonce( 'save-attributes' ), 'calc_totals_nonce' => wp_create_nonce( 'calc-totals' ), 'get_customer_details_nonce' => wp_create_nonce( 'get-customer-details' ), 'search_products_nonce' => wp_create_nonce( 'search-products' ), 'grant_access_nonce' => wp_create_nonce( 'grant-access' ), 'revoke_access_nonce' => wp_create_nonce( 'revoke-access' ), 'add_order_note_nonce' => wp_create_nonce( 'add-order-note' ), 'delete_order_note_nonce' => wp_create_nonce( 'delete-order-note' ), 'calendar_image' => WC()->plugin_url() . '/assets/images/calendar.png', 'post_id' => isset( $post->ID ) ? $post->ID : '', 'base_country' => WC()->countries->get_base_country(), 'currency_format_num_decimals' => wc_get_price_decimals(), 'currency_format_symbol' => get_woocommerce_currency_symbol( $currency ), 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS. 'rounding_precision' => wc_get_rounding_precision(), 'tax_rounding_mode' => wc_get_tax_rounding_mode(), 'product_types' => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ), 'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ), 'i18n_permission_revoke' => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ), 'i18n_tax_rate_already_exists' => __( 'You cannot add the same tax rate twice!', 'woocommerce' ), 'i18n_delete_note' => __( 'Are you sure you wish to delete this note? This action cannot be undone.', 'woocommerce' ), 'i18n_apply_coupon' => __( 'Enter a coupon code to apply. Discounts are applied to line totals, before taxes.', 'woocommerce' ), 'i18n_add_fee' => __( 'Enter a fixed amount or percentage to apply as a fee.', 'woocommerce' ), ); wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params ); } // Term ordering - only when sorting by term_order. if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); wp_enqueue_script( 'woocommerce_term_ordering' ); $taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : ''; $woocommerce_term_order_params = array( 'taxonomy' => $taxonomy, ); wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); } // Product sorting - only when sorting by menu order on the products page. if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { wp_register_script( 'woocommerce_product_ordering', WC()->plugin_url() . '/assets/js/admin/product-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version, true ); wp_enqueue_script( 'woocommerce_product_ordering' ); } // Reports Pages. if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); wp_enqueue_script( 'wc-reports' ); wp_enqueue_script( 'flot' ); wp_enqueue_script( 'flot-resize' ); wp_enqueue_script( 'flot-time' ); wp_enqueue_script( 'flot-pie' ); wp_enqueue_script( 'flot-stack' ); } // API settings. if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { wp_register_script( 'wc-api-keys', WC()->plugin_url() . '/assets/js/admin/api-keys' . $suffix . '.js', array( 'jquery', 'woocommerce_admin', 'underscore', 'backbone', 'wp-util', 'qrcode', 'wc-clipboard' ), $version, true ); wp_enqueue_script( 'wc-api-keys' ); wp_localize_script( 'wc-api-keys', 'woocommerce_admin_api_keys', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'update_api_nonce' => wp_create_nonce( 'update-api-key' ), 'clipboard_failed' => esc_html__( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ), ) ); } // System status. if ( $wc_screen_id . '_page_wc-status' === $screen_id ) { wp_register_script( 'wc-admin-system-status', WC()->plugin_url() . '/assets/js/admin/system-status' . $suffix . '.js', array( 'wc-clipboard' ), $version ); wp_enqueue_script( 'wc-admin-system-status' ); wp_localize_script( 'wc-admin-system-status', 'woocommerce_admin_system_status', array( 'delete_log_confirmation' => esc_js( __( 'Are you sure you want to delete this log?', 'woocommerce' ) ), 'run_tool_confirmation' => esc_js( __( 'Are you sure you want to run this tool?', 'woocommerce' ) ), ) ); } if ( in_array( $screen_id, array( 'user-edit', 'profile' ) ) ) { wp_register_script( 'wc-users', WC()->plugin_url() . '/assets/js/admin/users' . $suffix . '.js', array( 'jquery', 'wc-enhanced-select', 'selectWoo' ), $version, true ); wp_enqueue_script( 'wc-users' ); wp_localize_script( 'wc-users', 'wc_users_params', array( 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), ) ); } if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { $active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) ); wp_register_script( 'marketplace-suggestions', WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js', array( 'jquery', 'underscore', 'js-cookie' ), $version, true ); wp_localize_script( 'marketplace-suggestions', 'marketplace_suggestions', array( 'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ), 'active_plugins' => $active_plugin_slugs, 'dismissed_suggestions' => WC_Marketplace_Suggestions::get_dismissed_suggestions(), 'suggestions_data' => WC_Marketplace_Suggestions::get_suggestions_api_data(), 'manage_suggestions_url' => admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ), 'in_app_purchase_params' => WC_Admin_Addons::get_in_app_purchase_url_params(), 'i18n_marketplace_suggestions_default_cta' => esc_html__( 'Learn More', 'woocommerce' ), 'i18n_marketplace_suggestions_dismiss_tooltip' => esc_attr__( 'Dismiss this suggestion', 'woocommerce' ), 'i18n_marketplace_suggestions_manage_suggestions' => esc_html__( 'Manage suggestions', 'woocommerce' ), ) ); wp_enqueue_script( 'marketplace-suggestions' ); } } } endif; return new WC_Admin_Assets(); includes/admin/class-wc-admin-status.php 0000644 00000033646 15132754524 0014322 0 ustar 00 <?php /** * Debug/Status page * * @package WooCommerce\Admin\System Status * @version 2.2.0 */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\ArrayUtil; defined( 'ABSPATH' ) || exit; /** * WC_Admin_Status Class. */ class WC_Admin_Status { /** * Handles output of the reports page in admin. */ public static function output() { include_once __DIR__ . '/views/html-admin-page-status.php'; } /** * Handles output of report. */ public static function status_report() { include_once __DIR__ . '/views/html-admin-page-status-report.php'; } /** * Handles output of tools. */ public static function status_tools() { if ( ! class_exists( 'WC_REST_System_Status_Tools_Controller' ) ) { wp_die( 'Cannot load the REST API to access WC_REST_System_Status_Tools_Controller.' ); } $tools = self::get_tools(); $tool_requires_refresh = false; if ( ! empty( $_GET['action'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'debug_action' ) ) { // WPCS: input var ok, sanitization ok. $tools_controller = new WC_REST_System_Status_Tools_Controller(); $action = wc_clean( wp_unslash( $_GET['action'] ) ); // WPCS: input var ok. if ( array_key_exists( $action, $tools ) ) { $response = $tools_controller->execute_tool( $action ); $tool = $tools[ $action ]; $tool_requires_refresh = ArrayUtil::get_value_or_default( $tool, 'requires_refresh', false ); $tool = array( 'id' => $action, 'name' => $tool['name'], 'action' => $tool['button'], 'description' => $tool['desc'], 'disabled' => ArrayUtil::get_value_or_default( $tool, 'disabled', false ), ); $tool = array_merge( $tool, $response ); /** * Fires after a WooCommerce system status tool has been executed. * * @param array $tool Details about the tool that has been executed. */ do_action( 'woocommerce_system_status_tool_executed', $tool ); } else { $response = array( 'success' => false, 'message' => __( 'Tool does not exist.', 'woocommerce' ), ); } if ( $response['success'] ) { echo '<div class="updated inline"><p>' . esc_html( $response['message'] ) . '</p></div>'; } else { echo '<div class="error inline"><p>' . esc_html( $response['message'] ) . '</p></div>'; } } // Display message if settings settings have been saved. if ( isset( $_REQUEST['settings-updated'] ) ) { // WPCS: input var ok. echo '<div class="updated inline"><p>' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '</p></div>'; } if ( $tool_requires_refresh ) { $tools = self::get_tools(); } include_once __DIR__ . '/views/html-admin-page-status-tools.php'; } /** * Get tools. * * @return array of tools */ public static function get_tools() { $tools_controller = new WC_REST_System_Status_Tools_Controller(); return $tools_controller->get_tools(); } /** * Show the logs page. */ public static function status_logs() { $log_handler = Constants::get_constant( 'WC_LOG_HANDLER' ); if ( 'WC_Log_Handler_DB' === $log_handler ) { self::status_logs_db(); } else { self::status_logs_file(); } } /** * Show the log page contents for file log handler. */ public static function status_logs_file() { $logs = self::scan_log_files(); if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ] ) ) { // WPCS: input var ok, CSRF ok. $viewed_log = $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ]; // WPCS: input var ok, CSRF ok. } elseif ( ! empty( $logs ) ) { $viewed_log = current( $logs ); } $handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : ''; if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok, CSRF ok. self::remove_log(); } include_once __DIR__ . '/views/html-admin-page-status-logs.php'; } /** * Show the log page contents for db log handler. */ public static function status_logs_db() { if ( ! empty( $_REQUEST['flush-logs'] ) ) { // WPCS: input var ok, CSRF ok. self::flush_db_logs(); } if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) { // WPCS: input var ok, CSRF ok. self::log_table_bulk_actions(); } $log_table_list = new WC_Admin_Log_Table_List(); $log_table_list->prepare_items(); include_once __DIR__ . '/views/html-admin-page-status-logs-db.php'; } /** * Retrieve metadata from a file. Based on WP Core's get_file_data function. * * @since 2.1.1 * @param string $file Path to the file. * @return string */ public static function get_file_version( $file ) { // Avoid notices if file does not exist. if ( ! file_exists( $file ) ) { return ''; } // We don't need to write to the file, so just open for reading. $fp = fopen( $file, 'r' ); // @codingStandardsIgnoreLine. // Pull only the first 8kiB of the file in. $file_data = fread( $fp, 8192 ); // @codingStandardsIgnoreLine. // PHP will close file handle, but we are good citizens. fclose( $fp ); // @codingStandardsIgnoreLine. // Make sure we catch CR-only line endings. $file_data = str_replace( "\r", "\n", $file_data ); $version = ''; if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) { $version = _cleanup_header_comment( $match[1] ); } return $version; } /** * Return the log file handle. * * @param string $filename Filename to get the handle for. * @return string */ public static function get_log_file_handle( $filename ) { return substr( $filename, 0, strlen( $filename ) > 48 ? strlen( $filename ) - 48 : strlen( $filename ) - 4 ); } /** * Scan the template files. * * @param string $template_path Path to the template directory. * @return array */ public static function scan_template_files( $template_path ) { $files = @scandir( $template_path ); // @codingStandardsIgnoreLine. $result = array(); if ( ! empty( $files ) ) { foreach ( $files as $key => $value ) { if ( ! in_array( $value, array( '.', '..' ), true ) ) { if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) { $sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value ); foreach ( $sub_files as $sub_file ) { $result[] = $value . DIRECTORY_SEPARATOR . $sub_file; } } else { $result[] = $value; } } } } return $result; } /** * Scan the log files. * * @return array */ public static function scan_log_files() { return WC_Log_Handler_File::get_log_files(); } /** * Get latest version of a theme by slug. * * @param object $theme WP_Theme object. * @return string Version number if found. */ public static function get_latest_theme_version( $theme ) { include_once ABSPATH . 'wp-admin/includes/theme.php'; $api = themes_api( 'theme_information', array( 'slug' => $theme->get_stylesheet(), 'fields' => array( 'sections' => false, 'tags' => false, ), ) ); $update_theme_version = 0; // Check .org for updates. if ( is_object( $api ) && ! is_wp_error( $api ) ) { $update_theme_version = $api->version; } elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { // Check WooThemes Theme Version. $theme_dir = substr( strtolower( str_replace( ' ', '', $theme->Name ) ), 0, 45 ); // @codingStandardsIgnoreLine. $theme_version_data = get_transient( $theme_dir . '_version_data' ); if ( false === $theme_version_data ) { $theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); if ( ! empty( $cl_lines ) ) { foreach ( $cl_lines as $line_num => $cl_line ) { if ( preg_match( '/^[0-9]/', $cl_line ) ) { $theme_date = str_replace( '.', '-', trim( substr( $cl_line, 0, strpos( $cl_line, '-' ) ) ) ); $theme_version = preg_replace( '~[^0-9,.]~', '', stristr( $cl_line, 'version' ) ); $theme_update = trim( str_replace( '*', '', $cl_lines[ $line_num + 1 ] ) ); $theme_version_data = array( 'date' => $theme_date, 'version' => $theme_version, 'update' => $theme_update, 'changelog' => $theme_changelog, ); set_transient( $theme_dir . '_version_data', $theme_version_data, DAY_IN_SECONDS ); break; } } } } if ( ! empty( $theme_version_data['version'] ) ) { $update_theme_version = $theme_version_data['version']; } } return $update_theme_version; } /** * Remove/delete the chosen file. */ public static function remove_log() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'remove_log' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok. $log_handler = new WC_Log_Handler_File(); $log_handler->remove( wp_unslash( $_REQUEST['handle'] ) ); // WPCS: input var ok, sanitization ok. } wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } /** * Clear DB log table. * * @since 3.0.0 */ private static function flush_db_logs() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } WC_Log_Handler_DB::flush(); wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } /** * Bulk DB log table actions. * * @since 3.0.0 */ private static function log_table_bulk_actions() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } $log_ids = array_map( 'absint', (array) isset( $_REQUEST['log'] ) ? wp_unslash( $_REQUEST['log'] ) : array() ); // WPCS: input var ok, sanitization ok. if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // WPCS: input var ok, sanitization ok. WC_Log_Handler_DB::delete( $log_ids ); wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } } /** * Prints table info if a base table is not present. */ private static function output_tables_info() { $missing_tables = WC_Install::verify_base_tables( false ); if ( 0 === count( $missing_tables ) ) { return; } ?> <br> <strong style="color:#a00;"> <span class="dashicons dashicons-warning"></span> <?php echo esc_html( sprintf( // translators: Comma seperated list of missing tables. __( 'Missing base tables: %s. Some WooCommerce functionality may not work as expected.', 'woocommerce' ), implode( ', ', $missing_tables ) ) ); ?> </strong> <?php } /** * Prints the information about plugins for the system status report. * Used for both active and inactive plugins sections. * * @param array $plugins List of plugins to display. * @param array $untested_plugins List of plugins that haven't been tested with the current WooCommerce version. * @return void */ private static function output_plugins_info( $plugins, $untested_plugins ) { $wc_version = Constants::get_constant( 'WC_VERSION' ); if ( 'major' === Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) { // Since we're only testing against major, we don't need to show minor and patch version. $wc_version = $wc_version[0] . '.0'; } foreach ( $plugins as $plugin ) { if ( ! empty( $plugin['name'] ) ) { // Link the plugin name to the plugin url if available. $plugin_name = esc_html( $plugin['name'] ); if ( ! empty( $plugin['url'] ) ) { $plugin_name = '<a href="' . esc_url( $plugin['url'] ) . '" aria-label="' . esc_attr__( 'Visit plugin homepage', 'woocommerce' ) . '" target="_blank">' . $plugin_name . '</a>'; } $has_newer_version = false; $version_string = $plugin['version']; $network_string = ''; if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) ) { if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) { /* translators: 1: current version. 2: latest version */ $version_string = sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $plugin['version'], $plugin['version_latest'] ); } if ( false !== $plugin['network_activated'] ) { $network_string = ' – <strong style="color: black;">' . esc_html__( 'Network enabled', 'woocommerce' ) . '</strong>'; } } $untested_string = ''; if ( array_key_exists( $plugin['plugin'], $untested_plugins ) ) { $untested_string = ' – <strong style="color: #a00;">'; /* translators: %s: version */ $untested_string .= esc_html( sprintf( __( 'Installed version not tested with active version of WooCommerce %s', 'woocommerce' ), $wc_version ) ); $untested_string .= '</strong>'; } ?> <tr> <td><?php echo wp_kses_post( $plugin_name ); ?></td> <td class="help"> </td> <td> <?php /* translators: %s: plugin author */ printf( esc_html__( 'by %s', 'woocommerce' ), esc_html( $plugin['author_name'] ) ); echo ' – ' . esc_html( $version_string ) . $untested_string . $network_string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php } } } } includes/admin/class-wc-admin-menus.php 0000644 00000041220 15132754524 0014111 0 ustar 00 <?php /** * Setup menus in WP admin. * * @package WooCommerce\Admin * @version 2.5.0 */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Admin_Menus', false ) ) { return new WC_Admin_Menus(); } /** * WC_Admin_Menus Class. */ class WC_Admin_Menus { /** * Hook in tabs. */ public function __construct() { // Add menus. add_action( 'admin_menu', array( $this, 'menu_highlight' ) ); add_action( 'admin_menu', array( $this, 'menu_order_count' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ), 9 ); add_action( 'admin_menu', array( $this, 'reports_menu' ), 20 ); add_action( 'admin_menu', array( $this, 'settings_menu' ), 50 ); add_action( 'admin_menu', array( $this, 'status_menu' ), 60 ); if ( apply_filters( 'woocommerce_show_addons_page', true ) ) { add_action( 'admin_menu', array( $this, 'addons_menu' ), 70 ); } add_filter( 'menu_order', array( $this, 'menu_order' ) ); add_filter( 'custom_menu_order', array( $this, 'custom_menu_order' ) ); add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 ); add_filter( 'submenu_file', array( $this, 'update_menu_highlight' ), 10, 2 ); add_filter( 'admin_title', array( $this, 'update_my_subscriptions_title' ) ); // Add endpoints custom URLs in Appearance > Menus > Pages. add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) ); // Admin bar menus. if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) { add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 ); } // Handle saving settings earlier than load-{page} hook to avoid race conditions in conditional menus. add_action( 'wp_loaded', array( $this, 'save_settings' ) ); } /** * Add menu items. */ public function admin_menu() { global $menu; $woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg=='; if ( current_user_can( 'edit_others_shop_orders' ) ) { $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. } add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' ); add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); } /** * Add menu item. */ public function reports_menu() { if ( current_user_can( 'edit_others_shop_orders' ) ) { add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); } else { add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); } } /** * Add menu item. */ public function settings_menu() { $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ), 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); } /** * Loads gateways and shipping methods into memory for use within settings. */ public function settings_page_init() { WC()->payment_gateways(); WC()->shipping(); // Include settings pages. WC_Admin_Settings::get_settings_pages(); // Add any posted messages. if ( ! empty( $_GET['wc_error'] ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::add_error( wp_kses_post( wp_unslash( $_GET['wc_error'] ) ) ); // WPCS: input var okay, CSRF ok. } if ( ! empty( $_GET['wc_message'] ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::add_message( wp_kses_post( wp_unslash( $_GET['wc_message'] ) ) ); // WPCS: input var okay, CSRF ok. } do_action( 'woocommerce_settings_page_init' ); } /** * Handle saving of settings. * * @return void */ public function save_settings() { global $current_tab, $current_section; // We should only save on the settings page. if ( ! is_admin() || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // Include settings pages. WC_Admin_Settings::get_settings_pages(); // Get current tab/section. $current_tab = empty( $_GET['tab'] ) ? 'general' : sanitize_title( wp_unslash( $_GET['tab'] ) ); // WPCS: input var okay, CSRF ok. $current_section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( wp_unslash( $_REQUEST['section'] ) ); // WPCS: input var okay, CSRF ok. // Save settings if data has been posted. if ( '' !== $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}_{$current_section}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::save(); } elseif ( '' === $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. WC_Admin_Settings::save(); } } /** * Add menu item. */ public function status_menu() { add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ), 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); } /** * Addons menu item. */ public function addons_menu() { $count_html = WC_Helper_Updater::get_updates_count_html(); /* translators: %s: extensions count */ $menu_title = sprintf( __( 'My Subscriptions %s', 'woocommerce' ), $count_html ); add_submenu_page( 'woocommerce', __( 'WooCommerce Marketplace', 'woocommerce' ), __( 'Marketplace', 'woocommerce' ), 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); add_submenu_page( 'woocommerce', __( 'My WooCommerce.com Subscriptions', 'woocommerce' ), $menu_title, 'manage_woocommerce', 'wc-addons§ion=helper', array( $this, 'addons_page' ) ); } /** * Highlights the correct top level admin menu item for post type add screens. */ public function menu_highlight() { global $parent_file, $submenu_file, $post_type; switch ( $post_type ) { case 'shop_order': case 'shop_coupon': $parent_file = 'woocommerce'; // WPCS: override ok. break; case 'product': $screen = get_current_screen(); if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) { $submenu_file = 'product_attributes'; // WPCS: override ok. $parent_file = 'edit.php?post_type=product'; // WPCS: override ok. } break; } } /** * Adds the order processing count to the menu. */ public function menu_order_count() { global $submenu; if ( isset( $submenu['woocommerce'] ) ) { // Remove 'WooCommerce' sub menu item. unset( $submenu['woocommerce'][0] ); // Add count if user has access. if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'edit_others_shop_orders' ) ) { $order_count = apply_filters( 'woocommerce_menu_order_count', wc_processing_order_count() ); if ( $order_count ) { foreach ( $submenu['woocommerce'] as $key => $menu_item ) { if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) { $submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins count-' . esc_attr( $order_count ) . '"><span class="processing-count">' . number_format_i18n( $order_count ) . '</span></span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited break; } } } } } } /** * Reorder the WC menu items in admin. * * @param int $menu_order Menu order. * @return array */ public function menu_order( $menu_order ) { // Initialize our custom order array. $woocommerce_menu_order = array(); // Get the index of our custom separator. $woocommerce_separator = array_search( 'separator-woocommerce', $menu_order, true ); // Get index of product menu. $woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order, true ); // Loop through menu order and do some rearranging. foreach ( $menu_order as $index => $item ) { if ( 'woocommerce' === $item ) { $woocommerce_menu_order[] = 'separator-woocommerce'; $woocommerce_menu_order[] = $item; $woocommerce_menu_order[] = 'edit.php?post_type=product'; unset( $menu_order[ $woocommerce_separator ] ); unset( $menu_order[ $woocommerce_product ] ); } elseif ( ! in_array( $item, array( 'separator-woocommerce' ), true ) ) { $woocommerce_menu_order[] = $item; } } // Return order. return $woocommerce_menu_order; } /** * Custom menu order. * * @param bool $enabled Whether custom menu ordering is already enabled. * @return bool */ public function custom_menu_order( $enabled ) { return $enabled || current_user_can( 'edit_others_shop_orders' ); } /** * Validate screen options on update. * * @param bool|int $status Screen option value. Default false to skip. * @param string $option The option name. * @param int $value The number of rows to use. */ public function set_screen_option( $status, $option, $value ) { if ( in_array( $option, array( 'woocommerce_keys_per_page', 'woocommerce_webhooks_per_page' ), true ) ) { return $value; } return $status; } /** * Init the reports page. */ public function reports_page() { WC_Admin_Reports::output(); } /** * Init the settings page. */ public function settings_page() { WC_Admin_Settings::output(); } /** * Init the attributes page. */ public function attributes_page() { WC_Admin_Attributes::output(); } /** * Init the status page. */ public function status_page() { WC_Admin_Status::output(); } /** * Init the addons page. */ public function addons_page() { WC_Admin_Addons::output(); } /** * Add custom nav meta box. * * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/. */ public function add_nav_menu_meta_boxes() { add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' ); } /** * Output menu links. */ public function nav_menu_links() { // Get items from account menu. $endpoints = wc_get_account_menu_items(); // Remove dashboard item. if ( isset( $endpoints['dashboard'] ) ) { unset( $endpoints['dashboard'] ); } // Include missing lost password. $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); ?> <div id="posttype-woocommerce-endpoints" class="posttypediv"> <div id="tabs-panel-woocommerce-endpoints" class="tabs-panel tabs-panel-active"> <ul id="woocommerce-endpoints-checklist" class="categorychecklist form-no-clear"> <?php $i = -1; foreach ( $endpoints as $key => $value ) : ?> <li> <label class="menu-item-title"> <input type="checkbox" class="menu-item-checkbox" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-object-id]" value="<?php echo esc_attr( $i ); ?>" /> <?php echo esc_html( $value ); ?> </label> <input type="hidden" class="menu-item-type" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-type]" value="custom" /> <input type="hidden" class="menu-item-title" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-title]" value="<?php echo esc_attr( $value ); ?>" /> <input type="hidden" class="menu-item-url" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-url]" value="<?php echo esc_url( wc_get_account_endpoint_url( $key ) ); ?>" /> <input type="hidden" class="menu-item-classes" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-classes]" /> </li> <?php $i--; endforeach; ?> </ul> </div> <p class="button-controls"> <span class="list-controls"> <a href="<?php echo esc_url( admin_url( 'nav-menus.php?page-tab=all&selectall=1#posttype-woocommerce-endpoints' ) ); ?>" class="select-all"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a> </span> <span class="add-to-menu"> <button type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to menu', 'woocommerce' ); ?>" name="add-post-type-menu-item" id="submit-posttype-woocommerce-endpoints"><?php esc_html_e( 'Add to menu', 'woocommerce' ); ?></button> <span class="spinner"></span> </span> </p> </div> <?php } /** * Add the "Visit Store" link in admin bar main menu. * * @since 2.4.0 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance. */ public function admin_bar_menus( $wp_admin_bar ) { if ( ! is_admin() || ! is_admin_bar_showing() ) { return; } // Show only when the user is a member of this site, or they're a super admin. if ( ! is_user_member_of_blog() && ! is_super_admin() ) { return; } // Don't display when shop page is the same of the page on front. if ( intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) { return; } // Add an option to visit the store. $wp_admin_bar->add_node( array( 'parent' => 'site-name', 'id' => 'view-store', 'title' => __( 'Visit Store', 'woocommerce' ), 'href' => wc_get_page_permalink( 'shop' ), ) ); } /** * Highlight the My Subscriptions menu item when on that page * * @param string $submenu_file The submenu file. * @param string $parent_file currently opened page. * * @return string */ public function update_menu_highlight( $submenu_file, $parent_file ) { if ( 'woocommerce' === $parent_file && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { $submenu_file = 'wc-addons§ion=helper'; } return $submenu_file; } /** * Update the My Subscriptions document title when on that page. * We want to maintain existing page URL but add it as a separate page, * which requires updating it manually. * * @param string $admin_title existing page title. * @return string */ public function update_my_subscriptions_title( $admin_title ) { if ( isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { $admin_title = 'My WooCommerce.com Subscriptions'; } return $admin_title; } } return new WC_Admin_Menus(); includes/admin/notes/class-wc-notes-refund-returns.php 0000644 00000003642 15132754524 0017143 0 ustar 00 <?php /** * Refund and Returns Policy Page Note Provider. * * Adds notes to the merchant's inbox concerning the created page. * * @package WooCommerce */ defined( 'ABSPATH' ) || exit; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Admin\Notes\Note; /** * WC_Notes_Refund_Returns. */ class WC_Notes_Refund_Returns { /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-refund-returns-page'; /** * Maybe add a note to the inbox. * * @param int $page_id The ID of the page. */ public static function possibly_add_note( $page_id ) { $data_store = \WC_Data_Store::load( 'admin-note' ); // Do we already have this note? $note_id = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_id ) ) { $note = new Note( $note_id ); if ( false !== $note || $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) { // note actioned -> don't show it. return; } } // Add note. $note = self::get_note( $page_id ); $note->save(); delete_option( 'woocommerce_refund_returns_page_created' ); } /** * Get the note. * * @param int $page_id The ID of the page. * @return object $note The note object. */ public static function get_note( $page_id ) { $note = new Note(); $note->set_title( __( 'Setup a Refund and Returns Policy page to boost your store\'s credibility.', 'woocommerce' ) ); $note->set_content( __( 'We have created a sample draft Refund and Returns Policy page for you. Please have a look and update it to fit your store.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-core' ); $note->add_action( 'notify-refund-returns-page', __( 'Edit page', 'woocommerce' ), admin_url( sprintf( 'post.php?post=%d&action=edit', (int) $page_id ) ) ); return $note; } } includes/admin/notes/class-wc-notes-run-db-update.php 0000644 00000024642 15132754524 0016632 0 ustar 00 <?php /** * WooCommerce: Db update note. * * Adds a note to complete the WooCommerce db update after the upgrade in the WC Admin context. * * @package WooCommerce */ defined( 'ABSPATH' ) || exit; use \Automattic\Jetpack\Constants; use Automattic\WooCommerce\Admin\Notes\Note; /** * WC_Notes_Run_Db_Update. */ class WC_Notes_Run_Db_Update { const NOTE_NAME = 'wc-update-db-reminder'; /** * Attach hooks. */ public function __construct() { // If the old notice gets dismissed, also hide this new one. add_action( 'woocommerce_hide_update_notice', array( __CLASS__, 'set_notice_actioned' ) ); // Not using Jetpack\Constants here as it can run before 'plugin_loaded' is done. if ( defined( 'DOING_AJAX' ) && DOING_AJAX || defined( 'DOING_CRON' ) && DOING_CRON || ! is_admin() ) { return; } add_action( 'current_screen', array( __CLASS__, 'show_reminder' ) ); } /** * Get current notice id from the database. * * Retrieves the first notice of this type. * * @return int|void Note id or null in case no note was found. */ private static function get_current_notice() { try { $data_store = \WC_Data_Store::load( 'admin-note' ); } catch ( Exception $e ) { return; } $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( empty( $note_ids ) ) { return; } if ( count( $note_ids ) > 1 ) { // Remove weird duplicates. Leave the first one. $current_notice = array_shift( $note_ids ); foreach ( $note_ids as $note_id ) { $note = new Note( $note_id ); $data_store->delete( $note ); } return $current_notice; } return current( $note_ids ); } /** * Set this notice to an actioned one, so that it's no longer displayed. */ public static function set_notice_actioned() { $note_id = self::get_current_notice(); if ( ! $note_id ) { return; } $note = new Note( $note_id ); $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } /** * Check whether the note is up to date for a fresh display. * * The check tests if * - actions are set up for the first 'Update database' notice, and * - URL for note's action is equal to the given URL (to check for potential nonce update). * * @param Note $note Note to check. * @param string $update_url URL to check the note against. * @param array<int, string> $current_actions List of actions to check for. * @return bool */ private static function note_up_to_date( $note, $update_url, $current_actions ) { $actions = $note->get_actions(); return count( $current_actions ) === count( array_intersect( wp_list_pluck( $actions, 'name' ), $current_actions ) ) && in_array( $update_url, wp_list_pluck( $actions, 'query' ), true ); } /** * Create and set up the first (out of 3) 'Database update needed' notice and store it in the database. * * If a $note_id is given, the method updates the note instead of creating a new one. * * @param integer $note_id Note db record to update. * @return int Created/Updated note id */ private static function update_needed_notice( $note_id = null ) { $update_url = add_query_arg( array( 'do_update_woocommerce' => 'true', ), wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' ) ); $note_actions = array( array( 'name' => 'update-db_run', 'label' => __( 'Update WooCommerce Database', 'woocommerce' ), 'url' => $update_url, 'status' => 'unactioned', 'primary' => true, 'nonce_action' => 'wc_db_update', 'nonce_name' => 'wc_db_update_nonce', ), array( 'name' => 'update-db_learn-more', 'label' => __( 'Learn more about updates', 'woocommerce' ), 'url' => 'https://docs.woocommerce.com/document/how-to-update-woocommerce/', 'status' => 'unactioned', 'primary' => false, ), ); if ( $note_id ) { $note = new Note( $note_id ); } else { $note = new Note(); } // Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run). if ( self::note_up_to_date( $note, $update_url, wp_list_pluck( $note_actions, 'name' ) ) ) { return $note_id; } $note->set_title( __( 'WooCommerce database update required', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce has been updated! To keep things running smoothly, we have to update your database to the newest version.', 'woocommerce' ) /* translators: %1$s: opening <a> tag %2$s: closing </a> tag*/ . sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-core' ); // In case db version is out of sync with WC version or during the next update, the notice needs to show up again, // so set it to unactioned. $note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED ); // Set new actions. $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); if ( isset( $note_action['nonce_action'] ) ) { $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); } } return $note->save(); } /** * Update the existing note with $note_id with information about the db upgrade being in progress. * * This is the second out of 3 notices displayed to the user. * * @param int $note_id Note id to update. */ private static function update_in_progress_notice( $note_id ) { // Same actions as in includes/admin/views/html-notice-updating.php. This just redirects, performs no action, so without nonce. $pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' ); $cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' ); $cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' ); $note = new Note( $note_id ); $note->set_title( __( 'WooCommerce database update in progress', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ) ); $note->clear_actions(); $note->add_action( 'update-db_see-progress', $cron_cta, $pending_actions_url, 'unactioned', false ); $note->save(); } /** * Update the existing note with $note_id with information that db upgrade is done. * * This is the last notice (3 out of 3 notices) displayed to the user. * * @param int $note_id Note id to update. */ private static function update_done_notice( $note_id ) { $hide_notices_url = html_entity_decode( // to convert &s to normal &, otherwise produces invalid link. add_query_arg( array( 'wc-hide-notice' => 'update', ), wc_get_current_admin_url() ? remove_query_arg( 'do_update_woocommerce', wc_get_current_admin_url() ) : admin_url( 'admin.php?page=wc-settings' ) ) ); $note_actions = array( array( 'name' => 'update-db_done', 'label' => __( 'Thanks!', 'woocommerce' ), 'url' => $hide_notices_url, 'status' => 'actioned', 'primary' => true, 'nonce_action' => 'woocommerce_hide_notices_nonce', 'nonce_name' => '_wc_notice_nonce', ), ); $note = new Note( $note_id ); // Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run). if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) { return $note_id; } $note->set_title( __( 'WooCommerce database update done', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ) ); $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); if ( isset( $note_action['nonce_action'] ) ) { $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); } } $note->save(); } /** * Prepare the correct content of the db update note to be displayed by WC Admin. * * This one gets called on each page load, so try to bail quickly. * * If the db needs an update, the notice should be always shown. * If the db does not need an update, but the notice has *not* been actioned (i.e. after the db update, when * store owner hasn't acknowledged the successful db update), still show the Thanks notice. * If the db does not need an update, and the notice has been actioned, then notice should *not* be shown. * The notice should also be hidden if the db does not need an update and the notice does not exist. */ public static function show_reminder() { $needs_db_update = \WC_Install::needs_db_update(); $note_id = self::get_current_notice(); if ( ! $needs_db_update ) { // Db update not needed && note does not exist -> don't show it. if ( ! $note_id ) { return; } $note = new Note( $note_id ); if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) { // Db update not needed && note actioned -> don't show it. return; } else { // Db update not needed && notice is unactioned -> Thank you note. self::update_done_notice( $note_id ); return; } } else { // Db needs update &&. if ( ! $note_id ) { // Db needs update && no notice exists -> create one that shows Nudge to update. $note_id = self::update_needed_notice(); } $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Db needs update && db update is scheduled -> update note to In progress. self::update_in_progress_notice( $note_id ); } else { // Db needs update && db update is not scheduled -> Nudge to run the db update. self::update_needed_notice( $note_id ); } } } } includes/admin/class-wc-admin-duplicate-product.php 0000644 00000022464 15132754524 0016423 0 ustar 00 <?php /** * Duplicate product functionality * * @package WooCommerce\Admin * @version 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_Duplicate_Product', false ) ) { return new WC_Admin_Duplicate_Product(); } /** * WC_Admin_Duplicate_Product Class. */ class WC_Admin_Duplicate_Product { /** * Constructor. */ public function __construct() { add_action( 'admin_action_duplicate_product', array( $this, 'duplicate_product_action' ) ); add_filter( 'post_row_actions', array( $this, 'dupe_link' ), 10, 2 ); add_action( 'post_submitbox_start', array( $this, 'dupe_button' ) ); } /** * Show the "Duplicate" link in admin products list. * * @param array $actions Array of actions. * @param WP_Post $post Post object. * @return array */ public function dupe_link( $actions, $post ) { global $the_product; if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) { return $actions; } if ( 'product' !== $post->post_type ) { return $actions; } // Add Class to Delete Permanently link in row actions. if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) { $the_product = wc_get_product( $post ); } if ( 'publish' === $post->post_status && $the_product && 0 < $the_product->get_total_sales() ) { $actions['trash'] = sprintf( '<a href="%s" class="submitdelete trash-product" aria-label="%s">%s</a>', get_delete_post_link( $the_product->get_id(), '', false ), /* translators: %s: post title */ esc_attr( sprintf( __( 'Move “%s” to the Trash', 'woocommerce' ), $the_product->get_name() ) ), esc_html__( 'Trash', 'woocommerce' ) ); } $actions['duplicate'] = '<a href="' . wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . $post->ID ), 'woocommerce-duplicate-product_' . $post->ID ) . '" aria-label="' . esc_attr__( 'Make a duplicate from this product', 'woocommerce' ) . '" rel="permalink">' . esc_html__( 'Duplicate', 'woocommerce' ) . '</a>'; return $actions; } /** * Show the dupe product link in admin. */ public function dupe_button() { global $post; if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) { return; } if ( ! is_object( $post ) ) { return; } if ( 'product' !== $post->post_type ) { return; } $notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID ); ?> <div id="duplicate-action"><a class="submitduplicate duplication" href="<?php echo esc_url( $notify_url ); ?>"><?php esc_html_e( 'Copy to a new draft', 'woocommerce' ); ?></a></div> <?php } /** * Duplicate a product action. */ public function duplicate_product_action() { if ( empty( $_REQUEST['post'] ) ) { wp_die( esc_html__( 'No product to duplicate has been supplied!', 'woocommerce' ) ); } $product_id = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : ''; check_admin_referer( 'woocommerce-duplicate-product_' . $product_id ); $product = wc_get_product( $product_id ); if ( false === $product ) { /* translators: %s: product id */ wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), esc_html( $product_id ) ) ); } $duplicate = $this->product_duplicate( $product ); // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table. do_action( 'woocommerce_product_duplicate', $duplicate, $product ); wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' ); // Redirect to the edit screen for the new draft page. wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) ); exit; } /** * Function to create the duplicate of the product. * * @param WC_Product $product The product to duplicate. * @return WC_Product The duplicate. */ public function product_duplicate( $product ) { /** * Filter to allow us to exclude meta keys from product duplication.. * * @param array $exclude_meta The keys to exclude from the duplicate. * @param array $existing_meta_keys The meta keys that the product already has. * @since 2.6 */ $meta_to_exclude = array_filter( apply_filters( 'woocommerce_duplicate_product_exclude_meta', array(), array_map( function ( $datum ) { return $datum->key; }, $product->get_meta_data() ) ) ); $duplicate = clone $product; $duplicate->set_id( 0 ); /* translators: %s contains the name of the original product. */ $duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) ); $duplicate->set_total_sales( 0 ); if ( '' !== $product->get_sku( 'edit' ) ) { $duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) ); } $duplicate->set_status( 'draft' ); $duplicate->set_date_created( null ); $duplicate->set_slug( '' ); $duplicate->set_rating_counts( 0 ); $duplicate->set_average_rating( 0 ); $duplicate->set_review_count( 0 ); foreach ( $meta_to_exclude as $meta_key ) { $duplicate->delete_meta_data( $meta_key ); } /** * This action can be used to modify the object further before it is created - it will be passed by reference. * * @since 3.0 */ do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product ); // Save parent product. $duplicate->save(); // Duplicate children of a variable product. if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( 'variable' ) ) { foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); $child_duplicate = clone $child; $child_duplicate->set_parent_id( $duplicate->get_id() ); $child_duplicate->set_id( 0 ); $child_duplicate->set_date_created( null ); // If we wait and let the insertion generate the slug, we will see extreme performance degradation // in the case where a product is used as a template. Every time the template is duplicated, each // variation will query every consecutive slug until it finds an empty one. To avoid this, we can // optimize the generation ourselves, avoiding the issue altogether. $this->generate_unique_slug( $child_duplicate ); if ( '' !== $child->get_sku( 'edit' ) ) { $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) ); } foreach ( $meta_to_exclude as $meta_key ) { $child_duplicate->delete_meta_data( $meta_key ); } /** * This action can be used to modify the object further before it is created - it will be passed by reference. * * @since 3.0 */ do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child ); $child_duplicate->save(); } // Get new object to reflect new children. $duplicate = wc_get_product( $duplicate->get_id() ); } return $duplicate; } /** * Get a product from the database to duplicate. * * @deprecated 3.0.0 * @param mixed $id The ID of the product to duplicate. * @return object|bool * @see duplicate_product */ private function get_product_to_duplicate( $id ) { global $wpdb; $id = absint( $id ); if ( ! $id ) { return false; } $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { $id = $post->post_parent; $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); } return $post; } /** * Generates a unique slug for a given product. We do this so that we can override the * behavior of wp_unique_post_slug(). The normal slug generation will run single * select queries on every non-unique slug, resulting in very bad performance. * * @param WC_Product $product The product to generate a slug for. * @since 3.9.0 */ private function generate_unique_slug( $product ) { global $wpdb; // We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug. // This will allow us to find the next-highest suffix that is unique. While this does not support gap // filling, this shouldn't matter for our use-case. $root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() ); $results = $wpdb->get_results( $wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' ) ); // The slug is already unique! if ( empty( $results ) ) { return; } // Find the maximum suffix so we can ensure uniqueness. $max_suffix = 1; foreach ( $results as $result ) { // Pull a numerical suffix off the slug after the last hyphen. $suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) ); if ( $suffix > $max_suffix ) { $max_suffix = $suffix; } } $product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) ); } } return new WC_Admin_Duplicate_Product(); includes/admin/class-wc-admin-pointers.php 0000644 00000022473 15132754524 0014636 0 ustar 00 <?php /** * Adds and controls pointers for contextual help/tutorials * * @package WooCommerce\Admin\Pointers * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Admin_Pointers Class. */ class WC_Admin_Pointers { /** * Constructor. */ public function __construct() { add_action( 'admin_enqueue_scripts', array( $this, 'setup_pointers_for_screen' ) ); } /** * Setup pointers for screen. */ public function setup_pointers_for_screen() { $screen = get_current_screen(); if ( ! $screen ) { return; } switch ( $screen->id ) { case 'product': $this->create_product_tutorial(); break; } } /** * Pointers for creating a product. */ public function create_product_tutorial() { if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // These pointers will chain - they will not be shown at once. $pointers = array( 'pointers' => array( 'title' => array( 'target' => '#title', 'next' => 'content', 'next_trigger' => array( 'target' => '#title', 'event' => 'input', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Product name', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Give your new product a name here. This is a required field and will be what your customers will see in your store.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'top', 'align' => 'left', ), ), ), 'content' => array( 'target' => '#wp-content-editor-container', 'next' => 'product-type', 'next_trigger' => array(), 'options' => array( 'content' => '<h3>' . esc_html__( 'Product description', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'This is your products main body of content. Here you should describe your product in detail.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'product-type' => array( 'target' => '#product-type', 'next' => 'virtual', 'next_trigger' => array( 'target' => '#product-type', 'event' => 'change blur click', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Choose product type', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Choose a type for this product. Simple is suitable for most physical goods and services (we recommend setting up a simple product for now).', 'woocommerce' ) . '</p>' . '<p>' . esc_html__( 'Variable is for more complex products such as t-shirts with multiple sizes.', 'woocommerce' ) . '</p>' . '<p>' . esc_html__( 'Grouped products are for grouping several simple products into one.', 'woocommerce' ) . '</p>' . '<p>' . esc_html__( 'Finally, external products are for linking off-site.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'virtual' => array( 'target' => '#_virtual', 'next' => 'downloadable', 'next_trigger' => array( 'target' => '#_virtual', 'event' => 'change', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Virtual products', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Check the "Virtual" box if this is a non-physical item, for example a service, which does not need shipping.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'downloadable' => array( 'target' => '#_downloadable', 'next' => 'regular_price', 'next_trigger' => array( 'target' => '#_downloadable', 'event' => 'change', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Downloadable products', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'If purchasing this product gives a customer access to a downloadable file, e.g. software, check this box.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'regular_price' => array( 'target' => '#_regular_price', 'next' => 'postexcerpt', 'next_trigger' => array( 'target' => '#_regular_price', 'event' => 'input', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Prices', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Next you need to give your product a price.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'postexcerpt' => array( 'target' => '#postexcerpt', 'next' => 'postimagediv', 'next_trigger' => array( 'target' => '#postexcerpt', 'event' => 'input', ), 'options' => array( 'content' => '<h3>' . esc_html__( 'Product short description', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Add a quick summary for your product here. This will appear on the product page under the product name.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'bottom', 'align' => 'middle', ), ), ), 'postimagediv' => array( 'target' => '#postimagediv', 'next' => 'product_tag', 'options' => array( 'content' => '<h3>' . esc_html__( 'Product images', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( "Upload or assign an image to your product here. This image will be shown in your store's catalog.", 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'right', 'align' => 'middle', ), ), ), 'product_tag' => array( 'target' => '#tagsdiv-product_tag', 'next' => 'product_catdiv', 'options' => array( 'content' => '<h3>' . esc_html__( 'Product tags', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'You can optionally "tag" your products here. Tags are a method of labeling your products to make them easier for customers to find.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'right', 'align' => 'middle', ), ), ), 'product_catdiv' => array( 'target' => '#product_catdiv', 'next' => 'submitdiv', 'options' => array( 'content' => '<h3>' . esc_html__( 'Product categories', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'Optionally assign categories to your products to make them easier to browse through and find in your store.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'right', 'align' => 'middle', ), ), ), 'submitdiv' => array( 'target' => '#submitdiv', 'next' => '', 'options' => array( 'content' => '<h3>' . esc_html__( 'Publish your product!', 'woocommerce' ) . '</h3>' . '<p>' . esc_html__( 'When you are finished editing your product, hit the "Publish" button to publish your product to your store.', 'woocommerce' ) . '</p>', 'position' => array( 'edge' => 'right', 'align' => 'middle', ), ), ), ), ); $this->enqueue_pointers( $pointers ); } /** * Enqueue pointers and add script to page. * * @param array $pointers Pointers data. */ public function enqueue_pointers( $pointers ) { $pointers = rawurlencode( wp_json_encode( $pointers ) ); wp_enqueue_style( 'wp-pointer' ); wp_enqueue_script( 'wp-pointer' ); wc_enqueue_js( "jQuery( function( $ ) { var wc_pointers = JSON.parse( decodeURIComponent( '{$pointers}' ) ); setTimeout( init_wc_pointers, 800 ); function init_wc_pointers() { $.each( wc_pointers.pointers, function( i ) { show_wc_pointer( i ); return false; }); } function show_wc_pointer( id ) { var pointer = wc_pointers.pointers[ id ]; var options = $.extend( pointer.options, { pointerClass: 'wp-pointer wc-pointer', close: function() { if ( pointer.next ) { show_wc_pointer( pointer.next ); } }, buttons: function( event, t ) { var close = '" . esc_js( __( 'Dismiss', 'woocommerce' ) ) . "', next = '" . esc_js( __( 'Next', 'woocommerce' ) ) . "', button = $( '<a class=\"close\" href=\"#\">' + close + '</a>' ), button2 = $( '<a class=\"button button-primary\" href=\"#\">' + next + '</a>' ), wrapper = $( '<div class=\"wc-pointer-buttons\" />' ); button.on( 'click.pointer', function(e) { e.preventDefault(); t.element.pointer('destroy'); }); button2.on( 'click.pointer', function(e) { e.preventDefault(); t.element.pointer('close'); }); wrapper.append( button ); wrapper.append( button2 ); return wrapper; }, } ); var this_pointer = $( pointer.target ).pointer( options ); this_pointer.pointer( 'open' ); if ( pointer.next_trigger ) { $( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() { setTimeout( function() { this_pointer.pointer( 'close' ); }, 400 ); }); } } });" ); } } new WC_Admin_Pointers(); includes/admin/class-wc-admin-profile.php 0000644 00000022125 15132754524 0014425 0 ustar 00 <?php /** * Add extra profile fields for users in admin * * @author WooThemes * @category Admin * @package WooCommerce\Admin * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } if ( ! class_exists( 'WC_Admin_Profile', false ) ) : /** * WC_Admin_Profile Class. */ class WC_Admin_Profile { /** * Hook in tabs. */ public function __construct() { add_action( 'show_user_profile', array( $this, 'add_customer_meta_fields' ) ); add_action( 'edit_user_profile', array( $this, 'add_customer_meta_fields' ) ); add_action( 'personal_options_update', array( $this, 'save_customer_meta_fields' ) ); add_action( 'edit_user_profile_update', array( $this, 'save_customer_meta_fields' ) ); } /** * Get Address Fields for the edit user pages. * * @return array Fields to display which are filtered through woocommerce_customer_meta_fields before being returned */ public function get_customer_meta_fields() { $show_fields = apply_filters( 'woocommerce_customer_meta_fields', array( 'billing' => array( 'title' => __( 'Customer billing address', 'woocommerce' ), 'fields' => array( 'billing_first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'description' => '', ), 'billing_last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'description' => '', ), 'billing_company' => array( 'label' => __( 'Company', 'woocommerce' ), 'description' => '', ), 'billing_address_1' => array( 'label' => __( 'Address line 1', 'woocommerce' ), 'description' => '', ), 'billing_address_2' => array( 'label' => __( 'Address line 2', 'woocommerce' ), 'description' => '', ), 'billing_city' => array( 'label' => __( 'City', 'woocommerce' ), 'description' => '', ), 'billing_postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'description' => '', ), 'billing_country' => array( 'label' => __( 'Country / Region', 'woocommerce' ), 'description' => '', 'class' => 'js_field-country', 'type' => 'select', 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), ), 'billing_state' => array( 'label' => __( 'State / County', 'woocommerce' ), 'description' => __( 'State / County or state code', 'woocommerce' ), 'class' => 'js_field-state', ), 'billing_phone' => array( 'label' => __( 'Phone', 'woocommerce' ), 'description' => '', ), 'billing_email' => array( 'label' => __( 'Email address', 'woocommerce' ), 'description' => '', ), ), ), 'shipping' => array( 'title' => __( 'Customer shipping address', 'woocommerce' ), 'fields' => array( 'copy_billing' => array( 'label' => __( 'Copy from billing address', 'woocommerce' ), 'description' => '', 'class' => 'js_copy-billing', 'type' => 'button', 'text' => __( 'Copy', 'woocommerce' ), ), 'shipping_first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'description' => '', ), 'shipping_last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'description' => '', ), 'shipping_company' => array( 'label' => __( 'Company', 'woocommerce' ), 'description' => '', ), 'shipping_address_1' => array( 'label' => __( 'Address line 1', 'woocommerce' ), 'description' => '', ), 'shipping_address_2' => array( 'label' => __( 'Address line 2', 'woocommerce' ), 'description' => '', ), 'shipping_city' => array( 'label' => __( 'City', 'woocommerce' ), 'description' => '', ), 'shipping_postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'description' => '', ), 'shipping_country' => array( 'label' => __( 'Country / Region', 'woocommerce' ), 'description' => '', 'class' => 'js_field-country', 'type' => 'select', 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), ), 'shipping_state' => array( 'label' => __( 'State / County', 'woocommerce' ), 'description' => __( 'State / County or state code', 'woocommerce' ), 'class' => 'js_field-state', ), 'shipping_phone' => array( 'label' => __( 'Phone', 'woocommerce' ), 'description' => '', ), ), ), ) ); return $show_fields; } /** * Show Address Fields on edit user pages. * * @param WP_User $user */ public function add_customer_meta_fields( $user ) { if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user->ID ) ) { return; } $show_fields = $this->get_customer_meta_fields(); foreach ( $show_fields as $fieldset_key => $fieldset ) : ?> <h2><?php echo $fieldset['title']; ?></h2> <table class="form-table" id="<?php echo esc_attr( 'fieldset-' . $fieldset_key ); ?>"> <?php foreach ( $fieldset['fields'] as $key => $field ) : ?> <tr> <th> <label for="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $field['label'] ); ?></label> </th> <td> <?php if ( ! empty( $field['type'] ) && 'select' === $field['type'] ) : ?> <select name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" class="<?php echo esc_attr( $field['class'] ); ?>" style="width: 25em;"> <?php $selected = esc_attr( get_user_meta( $user->ID, $key, true ) ); foreach ( $field['options'] as $option_key => $option_value ) : ?> <option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( $selected, $option_key, true ); ?>><?php echo esc_html( $option_value ); ?></option> <?php endforeach; ?> </select> <?php elseif ( ! empty( $field['type'] ) && 'checkbox' === $field['type'] ) : ?> <input type="checkbox" name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" value="1" class="<?php echo esc_attr( $field['class'] ); ?>" <?php checked( (int) get_user_meta( $user->ID, $key, true ), 1, true ); ?> /> <?php elseif ( ! empty( $field['type'] ) && 'button' === $field['type'] ) : ?> <button type="button" id="<?php echo esc_attr( $key ); ?>" class="button <?php echo esc_attr( $field['class'] ); ?>"><?php echo esc_html( $field['text'] ); ?></button> <?php else : ?> <input type="text" name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $this->get_user_meta( $user->ID, $key ) ); ?>" class="<?php echo ( ! empty( $field['class'] ) ? esc_attr( $field['class'] ) : 'regular-text' ); ?>" /> <?php endif; ?> <p class="description"><?php echo wp_kses_post( $field['description'] ); ?></p> </td> </tr> <?php endforeach; ?> </table> <?php endforeach; } /** * Save Address Fields on edit user pages. * * @param int $user_id User ID of the user being saved */ public function save_customer_meta_fields( $user_id ) { if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user_id ) ) { return; } $save_fields = $this->get_customer_meta_fields(); foreach ( $save_fields as $fieldset ) { foreach ( $fieldset['fields'] as $key => $field ) { if ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) { update_user_meta( $user_id, $key, isset( $_POST[ $key ] ) ); } elseif ( isset( $_POST[ $key ] ) ) { update_user_meta( $user_id, $key, wc_clean( $_POST[ $key ] ) ); } } } } /** * Get user meta for a given key, with fallbacks to core user info for pre-existing fields. * * @since 3.1.0 * @param int $user_id User ID of the user being edited * @param string $key Key for user meta field * @return string */ protected function get_user_meta( $user_id, $key ) { $value = get_user_meta( $user_id, $key, true ); $existing_fields = array( 'billing_first_name', 'billing_last_name' ); if ( ! $value && in_array( $key, $existing_fields ) ) { $value = get_user_meta( $user_id, str_replace( 'billing_', '', $key ), true ); } elseif ( ! $value && ( 'billing_email' === $key ) ) { $user = get_userdata( $user_id ); $value = $user->user_email; } return $value; } } endif; return new WC_Admin_Profile(); includes/admin/helper/class-wc-helper-options.php 0000644 00000002550 15132754524 0016126 0 ustar 00 <?php /** * WooCommerce Admin Helper Options * * @package WooCommerce\Admin\Helper */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper_Options Class * * An interface to the woocommerce_helper_data entry in the wp_options table. */ class WC_Helper_Options { /** * The option name used to store the helper data. * * @var string */ private static $option_name = 'woocommerce_helper_data'; /** * Update an option by key * * All helper options are grouped in a single options entry. This method * is not thread-safe, use with caution. * * @param string $key The key to update. * @param mixed $value The new option value. * * @return bool True if the option has been updated. */ public static function update( $key, $value ) { $options = get_option( self::$option_name, array() ); $options[ $key ] = $value; return update_option( self::$option_name, $options, true ); } /** * Get an option by key * * @see self::update * * @param string $key The key to fetch. * @param mixed $default The default option to return if the key does not exist. * * @return mixed An option or the default. */ public static function get( $key, $default = false ) { $options = get_option( self::$option_name, array() ); if ( array_key_exists( $key, $options ) ) { return $options[ $key ]; } return $default; } } includes/admin/helper/class-wc-helper-api.php 0000644 00000011067 15132754524 0015207 0 ustar 00 <?php /** * WooCommerce Admin Helper API * * @package WooCommerce\Admin\Helper */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper_API Class * * Provides a communication interface with the WooCommerce.com Helper API. */ class WC_Helper_API { /** * Base path for API routes. * * @var $api_base */ public static $api_base; /** * Load * * Allow devs to point the API base to a local API development or staging server. * Note that sslverify will be turned off for the woocommerce.dev + WP_DEBUG combination. * The URL can be changed on plugins_loaded before priority 10. */ public static function load() { self::$api_base = apply_filters( 'woocommerce_helper_api_base', 'https://woocommerce.com/wp-json/helper/1.0' ); } /** * Perform an HTTP request to the Helper API. * * @param string $endpoint The endpoint to request. * @param array $args Additional data for the request. Set authenticated to a truthy value to enable auth. * * @return array|WP_Error The response from wp_safe_remote_request() */ public static function request( $endpoint, $args = array() ) { $url = self::url( $endpoint ); if ( ! empty( $args['authenticated'] ) ) { if ( ! self::_authenticate( $url, $args ) ) { return new WP_Error( 'authentication', 'Authentication failed.' ); } } /** * Allow developers to filter the request args passed to wp_safe_remote_request(). * Useful to remove sslverify when working on a local api dev environment. */ $args = apply_filters( 'woocommerce_helper_api_request_args', $args, $endpoint ); // TODO: Check response signatures on certain endpoints. return wp_safe_remote_request( $url, $args ); } /** * Adds authentication headers to an HTTP request. * * @param string $url The request URI. * @param array $args By-ref, the args that will be passed to wp_remote_request(). * @return bool Were the headers added? */ private static function _authenticate( &$url, &$args ) { $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) { return false; } $request_uri = parse_url( $url, PHP_URL_PATH ); $query_string = parse_url( $url, PHP_URL_QUERY ); if ( is_string( $query_string ) ) { $request_uri .= '?' . $query_string; } $data = array( 'host' => parse_url( $url, PHP_URL_HOST ), 'request_uri' => $request_uri, 'method' => ! empty( $args['method'] ) ? $args['method'] : 'GET', ); if ( ! empty( $args['body'] ) ) { $data['body'] = $args['body']; } $signature = hash_hmac( 'sha256', json_encode( $data ), $auth['access_token_secret'] ); if ( empty( $args['headers'] ) ) { $args['headers'] = array(); } $headers = array( 'Authorization' => 'Bearer ' . $auth['access_token'], 'X-Woo-Signature' => $signature, ); $args['headers'] = wp_parse_args( $headers, $args['headers'] ); $url = add_query_arg( array( 'token' => $auth['access_token'], 'signature' => $signature, ), $url ); return true; } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function get( $endpoint, $args = array() ) { $args['method'] = 'GET'; return self::request( $endpoint, $args ); } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function post( $endpoint, $args = array() ) { $args['method'] = 'POST'; return self::request( $endpoint, $args ); } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function put( $endpoint, $args = array() ) { $args['method'] = 'PUT'; return self::request( $endpoint, $args ); } /** * Using the API base, form a request URL from a given endpoint. * * @param string $endpoint The endpoint to request. * * @return string The absolute endpoint URL. */ public static function url( $endpoint ) { $endpoint = ltrim( $endpoint, '/' ); $endpoint = sprintf( '%s/%s', self::$api_base, $endpoint ); $endpoint = esc_url_raw( $endpoint ); return $endpoint; } } WC_Helper_API::load(); includes/admin/helper/class-wc-helper-compat.php 0000644 00000013277 15132754524 0015726 0 ustar 00 <?php /** * WooCommerce Admin Helper Compat * * @package WooCommerce\Admin\Helper */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper_Compat Class * * Some level of compatibility with the legacy WooCommerce Helper plugin. */ class WC_Helper_Compat { /** * Loads the class, runs on init. */ public static function load() { add_action( 'woocommerce_helper_loaded', array( __CLASS__, 'helper_loaded' ) ); } /** * Runs during woocommerce_helper_loaded */ public static function helper_loaded() { // Stop the nagging about WooThemes Updater remove_action( 'admin_notices', 'woothemes_updater_notice' ); // A placeholder dashboard menu for legacy helper users. add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) ); if ( empty( $GLOBALS['woothemes_updater'] ) ) { return; } self::remove_actions(); self::migrate_connection(); self::deactivate_plugin(); } /** * Remove legacy helper actions (notices, menus, etc.) */ public static function remove_actions() { // Remove WooThemes Updater notices remove_action( 'network_admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) ); remove_action( 'admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) ); remove_action( 'network_admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); remove_action( 'admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); } /** * Attempt to migrate a legacy connection to a new one. */ public static function migrate_connection() { // Don't attempt to migrate if attempted before. if ( WC_Helper_Options::get( 'did-migrate' ) ) { return; } $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth ) ) { return; } WC_Helper::log( 'Attempting oauth/migrate' ); WC_Helper_Options::update( 'did-migrate', true ); $master_key = get_option( 'woothemes_helper_master_key' ); if ( empty( $master_key ) ) { WC_Helper::log( 'Master key not found, aborting' ); return; } $request = WC_Helper_API::post( 'oauth/migrate', array( 'body' => array( 'home_url' => home_url(), 'master_key' => $master_key, ), ) ); if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { WC_Helper::log( 'Call to oauth/migrate returned a non-200 response code' ); return; } $request_token = json_decode( wp_remote_retrieve_body( $request ) ); if ( empty( $request_token ) ) { WC_Helper::log( 'Call to oauth/migrate returned an empty token' ); return; } // Obtain an access token. $request = WC_Helper_API::post( 'oauth/access_token', array( 'body' => array( 'request_token' => $request_token, 'home_url' => home_url(), 'migrate' => true, ), ) ); if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { WC_Helper::log( 'Call to oauth/access_token returned a non-200 response code' ); return; } $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); if ( empty( $access_token ) ) { WC_Helper::log( 'Call to oauth/access_token returned an invalid token' ); return; } WC_Helper_Options::update( 'auth', array( 'access_token' => $access_token['access_token'], 'access_token_secret' => $access_token['access_token_secret'], 'site_id' => $access_token['site_id'], 'user_id' => null, // Set this later 'updated' => time(), ) ); // Obtain the connected user info. if ( ! WC_Helper::_flush_authentication_cache() ) { WC_Helper::log( 'Could not obtain connected user info in migrate_connection' ); WC_Helper_Options::update( 'auth', array() ); return; } } /** * Attempt to deactivate the legacy helper plugin. */ public static function deactivate_plugin() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( ! function_exists( 'deactivate_plugins' ) ) { return; } if ( is_plugin_active( 'woothemes-updater/woothemes-updater.php' ) ) { deactivate_plugins( 'woothemes-updater/woothemes-updater.php' ); // Notify the user when the plugin is deactivated. add_action( 'pre_current_active_plugins', array( __CLASS__, 'plugin_deactivation_notice' ) ); } } /** * Display admin notice directing the user where to go. */ public static function plugin_deactivation_notice() { ?> <div id="message" class="error is-dismissible"> <p><?php printf( __( 'The WooCommerce Helper plugin is no longer needed. <a href="%s">Manage subscriptions</a> from the extensions tab instead.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) ); ?></p> </div> <?php } /** * Register menu item. */ public static function admin_menu() { // No additional menu items for users who did not have a connected helper before. $master_key = get_option( 'woothemes_helper_master_key' ); if ( empty( $master_key ) ) { return; } // Do not show the menu item if user has already seen the new screen. $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['user_id'] ) ) { return; } add_dashboard_page( __( 'WooCommerce Helper', 'woocommerce' ), __( 'WooCommerce Helper', 'woocommerce' ), 'manage_options', 'woothemes-helper', array( __CLASS__, 'render_compat_menu' ) ); } /** * Render the legacy helper compat view. */ public static function render_compat_menu() { $helper_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', ), admin_url( 'admin.php' ) ); include WC_Helper::get_view_filename( 'html-helper-compat.php' ); } } WC_Helper_Compat::load(); includes/admin/helper/class-wc-helper-plugin-info.php 0000644 00000003560 15132754524 0016664 0 ustar 00 <?php /** * WooCommerce Admin Helper Plugin Info * * @package WooCommerce\Admin\Helper */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper_Plugin_Info Class * * Provides the "View Information" core modals with data for WooCommerce.com * hosted extensions. */ class WC_Helper_Plugin_Info { /** * Loads the class, runs on init. */ public static function load() { add_filter( 'plugins_api', array( __CLASS__, 'plugins_api' ), 20, 3 ); } /** * Plugin information callback for Woo extensions. * * @param object $response The response core needs to display the modal. * @param string $action The requested plugins_api() action. * @param object $args Arguments passed to plugins_api(). * * @return object An updated $response. */ public static function plugins_api( $response, $action, $args ) { if ( 'plugin_information' !== $action ) { return $response; } if ( empty( $args->slug ) ) { return $response; } // Only for slugs that start with woo- if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) { return $response; } $clean_slug = str_replace( 'woocommerce-com-', '', $args->slug ); // Look through update data by slug. $update_data = WC_Helper_Updater::get_update_data(); $products = wp_list_filter( $update_data, array( 'slug' => $clean_slug ) ); if ( empty( $products ) ) { return $response; } $product_id = array_keys( $products ); $product_id = array_shift( $product_id ); // Fetch the product information from the Helper API. $request = WC_Helper_API::get( add_query_arg( array( 'product_id' => absint( $product_id ), ), 'info' ), array( 'authenticated' => true ) ); $results = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! empty( $results ) ) { $response = (object) $results; } return $response; } } WC_Helper_Plugin_Info::load(); includes/admin/helper/class-wc-helper.php 0000644 00000146502 15132754524 0014443 0 ustar 00 <?php /** * WooCommerce Admin Helper * * @package WooCommerce\Admin\Helper */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper Class * * The main entry-point for all things related to the Helper. */ class WC_Helper { /** * A log object returned by wc_get_logger(). * * @var $log */ public static $log; /** * Get an absolute path to the requested helper view. * * @param string $view The requested view file. * * @return string The absolute path to the view file. */ public static function get_view_filename( $view ) { return dirname( __FILE__ ) . "/views/$view"; } /** * Loads the helper class, runs on init. */ public static function load() { self::includes(); add_action( 'current_screen', array( __CLASS__, 'current_screen' ) ); add_action( 'woocommerce_helper_output', array( __CLASS__, 'render_helper_output' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) ); add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) ); do_action( 'woocommerce_helper_loaded' ); } /** * Include supporting helper classes. */ protected static function includes() { include_once dirname( __FILE__ ) . '/class-wc-helper-options.php'; include_once dirname( __FILE__ ) . '/class-wc-helper-api.php'; include_once dirname( __FILE__ ) . '/class-wc-helper-updater.php'; include_once dirname( __FILE__ ) . '/class-wc-helper-plugin-info.php'; include_once dirname( __FILE__ ) . '/class-wc-helper-compat.php'; } /** * Render the helper section content based on context. */ public static function render_helper_output() { $auth = WC_Helper_Options::get( 'auth' ); $auth_user_data = WC_Helper_Options::get( 'auth_user_data' ); // Return success/error notices. $notices = self::_get_return_notices(); // No active connection. if ( empty( $auth['access_token'] ) ) { $connect_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-connect' => 1, 'wc-helper-nonce' => wp_create_nonce( 'connect' ), ), admin_url( 'admin.php' ) ); include self::get_view_filename( 'html-oauth-start.php' ); return; } $disconnect_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-disconnect' => 1, 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), ), admin_url( 'admin.php' ) ); $current_filter = self::get_current_filter(); $refresh_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-refresh' => 1, 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), ), admin_url( 'admin.php' ) ); // Installed plugins and themes, with or without an active subscription. $woo_plugins = self::get_local_woo_plugins(); $woo_themes = self::get_local_woo_themes(); $site_id = absint( $auth['site_id'] ); $subscriptions = self::get_subscriptions(); $updates = WC_Helper_Updater::get_update_data(); $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); foreach ( $subscriptions as &$subscription ) { $subscription['active'] = in_array( $site_id, $subscription['connections'] ); $subscription['activate_url'] = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-activate' => 1, 'wc-helper-product-key' => $subscription['product_key'], 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), ), admin_url( 'admin.php' ) ); $subscription['deactivate_url'] = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-deactivate' => 1, 'wc-helper-product-key' => $subscription['product_key'], 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), ), admin_url( 'admin.php' ) ); $subscription['local'] = array( 'installed' => false, 'active' => false, 'version' => null, ); $subscription['update_url'] = admin_url( 'update-core.php' ); $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); if ( ! empty( $local ) ) { $local = array_shift( $local ); $subscription['local']['installed'] = true; $subscription['local']['version'] = $local['Version']; if ( 'plugin' == $local['_type'] ) { if ( is_plugin_active( $local['_filename'] ) ) { $subscription['local']['active'] = true; } elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) { $subscription['local']['active'] = true; } // A magic update_url. $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); } elseif ( 'theme' == $local['_type'] ) { if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) { $subscription['local']['active'] = true; } // Another magic update_url. $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); } } $subscription['has_update'] = false; if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); } $subscription['download_primary'] = true; $subscription['download_url'] = 'https://woocommerce.com/my-account/downloads/'; if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; } $subscription['actions'] = array(); if ( $subscription['has_update'] && ! $subscription['expired'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), 'button_label' => __( 'Update', 'woocommerce' ), 'button_url' => $subscription['update_url'], 'status' => 'update-available', 'icon' => 'dashicons-update', ); // Subscription is not active on this site. if ( ! $subscription['active'] ) { $action['message'] .= ' ' . __( 'To enable this update you need to <strong>activate</strong> this subscription.', 'woocommerce' ); $action['button_label'] = null; $action['button_url'] = null; } $subscription['actions'][] = $action; } if ( $subscription['has_update'] && $subscription['expired'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $action['message'] .= ' ' . __( 'To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' ); $action['button_label'] = __( 'Purchase', 'woocommerce' ); $action['button_url'] = $subscription['product_url']; $subscription['actions'][] = $action; } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { $action = array( 'message' => sprintf( __( 'This subscription has expired. Contact the owner to <strong>renew</strong> the subscription to receive updates and support.', 'woocommerce' ) ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['actions'][] = $action; } elseif ( $subscription['expired'] ) { $action = array( 'message' => sprintf( __( 'This subscription has expired. Please <strong>renew</strong> to receive updates and support.', 'woocommerce' ) ), 'button_label' => __( 'Renew', 'woocommerce' ), 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['actions'][] = $action; } if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { $action = array( 'message' => __( 'Subscription is <strong>expiring</strong> soon.', 'woocommerce' ), 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['download_primary'] = false; $subscription['actions'][] = $action; } elseif ( $subscription['expiring'] ) { $action = array( 'message' => sprintf( __( 'This subscription is expiring soon. Please <strong>renew</strong> to continue receiving updates and support.', 'woocommerce' ) ), 'button_label' => __( 'Renew', 'woocommerce' ), 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['download_primary'] = false; $subscription['actions'][] = $action; } // Mark the first action primary. foreach ( $subscription['actions'] as $key => $action ) { if ( ! empty( $action['button_label'] ) ) { $subscription['actions'][ $key ]['primary'] = true; break; } } } // Break the by-ref. unset( $subscription ); // Installed products without a subscription. $no_subscriptions = array(); foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { continue; } $data['_product_url'] = '#'; $data['_has_update'] = false; if ( ! empty( $updates[ $data['_product_id'] ] ) ) { $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; } elseif ( ! empty( $data['PluginURI'] ) ) { $data['_product_url'] = $data['PluginURI']; } } $data['_actions'] = array(); if ( $data['_has_update'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is <strong>available</strong>. To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), 'button_label' => __( 'Purchase', 'woocommerce' ), 'button_url' => $data['_product_url'], 'status' => 'expired', 'icon' => 'dashicons-info', ); $data['_actions'][] = $action; } else { $action = array( /* translators: 1: subscriptions docs 2: subscriptions docs */ 'message' => sprintf( __( 'To receive updates and support for this extension, you need to <strong>purchase</strong> a new subscription or consolidate your extensions to one connected account by <strong><a href="%1$s" title="Sharing Docs">sharing</a> or <a href="%2$s" title="Transferring Docs">transferring</a></strong> this extension to this connected account.', 'woocommerce' ), 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ), 'button_label' => __( 'Purchase', 'woocommerce' ), 'button_url' => $data['_product_url'], 'status' => 'expired', 'icon' => 'dashicons-info', ); $data['_actions'][] = $action; } $no_subscriptions[ $filename ] = $data; } // Update the user id if it came from a migrated connection. if ( empty( $auth['user_id'] ) ) { $auth['user_id'] = get_current_user_id(); WC_Helper_Options::update( 'auth', $auth ); } // Sort alphabetically. uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); // Filters. self::get_filters_counts( $subscriptions ); // Warm it up. self::_filter( $subscriptions, self::get_current_filter() ); // We have an active connection. include self::get_view_filename( 'html-main.php' ); return; } /** * Get available subscriptions filters. * * @return array An array of filter keys and labels. */ public static function get_filters() { $filters = array( 'all' => __( 'All', 'woocommerce' ), 'active' => __( 'Active', 'woocommerce' ), 'inactive' => __( 'Inactive', 'woocommerce' ), 'installed' => __( 'Installed', 'woocommerce' ), 'update-available' => __( 'Update Available', 'woocommerce' ), 'expiring' => __( 'Expiring Soon', 'woocommerce' ), 'expired' => __( 'Expired', 'woocommerce' ), 'download' => __( 'Download', 'woocommerce' ), ); return $filters; } /** * Get counts data for the filters array. * * @param array $subscriptions The array of all available subscriptions. * * @return array Filter counts (filter => count). */ public static function get_filters_counts( $subscriptions = null ) { static $filters; if ( isset( $filters ) ) { return $filters; } $filters = array_fill_keys( array_keys( self::get_filters() ), 0 ); if ( empty( $subscriptions ) ) { return array(); } foreach ( $filters as $key => $count ) { $_subs = $subscriptions; self::_filter( $_subs, $key ); $filters[ $key ] = count( $_subs ); } return $filters; } /** * Get current filter. * * @return string The current filter. */ public static function get_current_filter() { $current_filter = 'all'; $valid_filters = array_keys( self::get_filters() ); if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) { $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) ); } return $current_filter; } /** * Filter an array of subscriptions by $filter. * * @param array $subscriptions The subscriptions array, passed by ref. * @param string $filter The filter. */ private static function _filter( &$subscriptions, $filter ) { switch ( $filter ) { case 'active': $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) ); break; case 'inactive': $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) ); break; case 'installed': foreach ( $subscriptions as $key => $subscription ) { if ( empty( $subscription['local']['installed'] ) ) { unset( $subscriptions[ $key ] ); } } break; case 'update-available': $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) ); break; case 'expiring': $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) ); break; case 'expired': $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) ); break; case 'download': foreach ( $subscriptions as $key => $subscription ) { if ( $subscription['local']['installed'] || $subscription['expired'] ) { unset( $subscriptions[ $key ] ); } } break; } } /** * Enqueue admin scripts and styles. */ public static function admin_enqueue_scripts() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' ); } } /** * Various success/error notices. * * Runs during admin page render, so no headers/redirects here. * * @return array Array pairs of message/type strings with notices. */ private static function _get_return_notices() { $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null; $notices = array(); switch ( $return_status ) { case 'activate-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'updated', 'message' => sprintf( /* translators: %s: product name */ __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>' ), ); break; case 'activate-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %s: product name */ __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>' ), ); break; case 'deactivate-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $local = self::_get_local_from_product_id( $product_id ); $message = sprintf( /* translators: %s: product name */ __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>' ); if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { $deactivate_plugin_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-deactivate-plugin' => 1, 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), ), admin_url( 'admin.php' ) ); $message = sprintf( /* translators: %1$s: product name, %2$s: deactivate url */ __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. <a href="%2$s">Click here</a> if you wish to deactivate the plugin as well.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>', esc_url( $deactivate_plugin_url ) ); } $notices[] = array( 'message' => $message, 'type' => 'updated', ); break; case 'deactivate-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %s: product name */ __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>' ), ); break; case 'deactivate-plugin-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'updated', 'message' => sprintf( /* translators: %s: product name */ __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>' ), ); break; case 'deactivate-plugin-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %1$s: product name, %2$s: plugins screen url */ __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the <a href="%2$s">Plugins screen</a> to deactivate it manually.', 'woocommerce' ), '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>', admin_url( 'plugins.php' ) ), ); break; case 'helper-connected': $notices[] = array( 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ), 'type' => 'updated', ); break; case 'helper-disconnected': $notices[] = array( 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ), 'type' => 'updated', ); break; case 'helper-refreshed': $notices[] = array( 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), 'type' => 'updated', ); break; } return $notices; } /** * Various early-phase actions with possible redirects. * * @param object $screen WP screen object. */ public static function current_screen( $screen ) { $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { return; } if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { return; } if ( ! empty( $_GET['wc-helper-connect'] ) ) { return self::_helper_auth_connect(); } if ( ! empty( $_GET['wc-helper-return'] ) ) { return self::_helper_auth_return(); } if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { return self::_helper_auth_disconnect(); } if ( ! empty( $_GET['wc-helper-refresh'] ) ) { return self::_helper_auth_refresh(); } if ( ! empty( $_GET['wc-helper-activate'] ) ) { return self::_helper_subscription_activate(); } if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { return self::_helper_subscription_deactivate(); } if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { return self::_helper_plugin_deactivate(); } } /** * Initiate a new OAuth connection. */ private static function _helper_auth_connect() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_connect' ); wp_die( 'Could not verify nonce' ); } $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-return' => 1, 'wc-helper-nonce' => wp_create_nonce( 'connect' ), ), admin_url( 'admin.php' ) ); $request = WC_Helper_API::post( 'oauth/request_token', array( 'body' => array( 'home_url' => home_url(), 'redirect_uri' => $redirect_uri, ), ) ); $code = wp_remote_retrieve_response_code( $request ); if ( 200 !== $code ) { self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); wp_die( 'Something went wrong' ); } $secret = json_decode( wp_remote_retrieve_body( $request ) ); if ( empty( $secret ) ) { self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); wp_die( 'Something went wrong' ); } /** * Fires when the Helper connection process is initiated. */ do_action( 'woocommerce_helper_connect_start' ); $connect_url = add_query_arg( array( 'home_url' => rawurlencode( home_url() ), 'redirect_uri' => rawurlencode( $redirect_uri ), 'secret' => rawurlencode( $secret ), ), WC_Helper_API::url( 'oauth/authorize' ) ); wp_redirect( esc_url_raw( $connect_url ) ); die(); } /** * Return from WooCommerce.com OAuth flow. */ private static function _helper_auth_return() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_return' ); wp_die( 'Something went wrong' ); } // Bail if the user clicked deny. if ( ! empty( $_GET['deny'] ) ) { /** * Fires when the Helper connection process is denied/cancelled. */ do_action( 'woocommerce_helper_denied' ); wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); die(); } // We do need a request token... if ( empty( $_GET['request_token'] ) ) { self::log( 'Request token not found in _helper_auth_return' ); wp_die( 'Something went wrong' ); } // Obtain an access token. $request = WC_Helper_API::post( 'oauth/access_token', array( 'body' => array( 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 'home_url' => home_url(), ), ) ); $code = wp_remote_retrieve_response_code( $request ); if ( 200 !== $code ) { self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); wp_die( 'Something went wrong' ); } $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! $access_token ) { self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); wp_die( 'Something went wrong' ); } WC_Helper_Options::update( 'auth', array( 'access_token' => $access_token['access_token'], 'access_token_secret' => $access_token['access_token_secret'], 'site_id' => $access_token['site_id'], 'user_id' => get_current_user_id(), 'updated' => time(), ) ); // Obtain the connected user info. if ( ! self::_flush_authentication_cache() ) { self::log( 'Could not obtain connected user info in _helper_auth_return' ); WC_Helper_Options::update( 'auth', array() ); wp_die( 'Something went wrong.' ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); /** * Fires when the Helper connection process has completed successfully. */ do_action( 'woocommerce_helper_connected' ); // Enable tracking when connected. if ( class_exists( 'WC_Tracker' ) ) { update_option( 'woocommerce_allow_tracking', 'yes' ); WC_Tracker::send_tracking_data( true ); } // If connecting through in-app purchase, redirects back to WooCommerce.com // for product installation. if ( ! empty( $_GET['wccom-install-url'] ) ) { wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) ); exit; } wp_safe_redirect( add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-status' => 'helper-connected', ), admin_url( 'admin.php' ) ) ); die(); } /** * Disconnect from WooCommerce.com, clear OAuth tokens. */ private static function _helper_auth_disconnect() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_disconnect' ); wp_die( 'Could not verify nonce' ); } /** * Fires when the Helper has been disconnected. */ do_action( 'woocommerce_helper_disconnected' ); $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-status' => 'helper-disconnected', ), admin_url( 'admin.php' ) ); WC_Helper_API::post( 'oauth/invalidate_token', array( 'authenticated' => true, ) ); WC_Helper_Options::update( 'auth', array() ); WC_Helper_Options::update( 'auth_user_data', array() ); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); wp_safe_redirect( $redirect_uri ); die(); } /** * User hit the Refresh button, clear all caches. */ private static function _helper_auth_refresh() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_refresh' ); wp_die( 'Could not verify nonce' ); } /** * Fires when Helper subscriptions are refreshed. */ do_action( 'woocommerce_helper_subscriptions_refresh' ); $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => 'helper-refreshed', ), admin_url( 'admin.php' ) ); self::_flush_authentication_cache(); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); wp_safe_redirect( $redirect_uri ); die(); } /** * Active a product subscription. */ private static function _helper_subscription_activate() { $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_subscription_activate' ); wp_die( 'Could not verify nonce' ); } // Activate subscription. $activation_response = WC_Helper_API::post( 'activate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { $activated = true; } if ( $activated ) { /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); } else { /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); } // Attempt to activate this plugin. $local = self::_get_local_from_product_id( $product_id ); if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { activate_plugin( $local['_filename'] ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Deactivate a product subscription. */ private static function _helper_subscription_deactivate() { $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_subscription_deactivate' ); wp_die( 'Could not verify nonce' ); } $deactivation_response = WC_Helper_API::post( 'deactivate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $code = wp_remote_retrieve_response_code( $deactivation_response ); $deactivated = 200 === $code; if ( $deactivated ) { /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); } else { self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); } self::_flush_subscriptions_cache(); $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Deactivate a plugin. */ private static function _helper_plugin_deactivate() { $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $deactivated = false; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); wp_die( 'Could not verify nonce' ); } if ( ! current_user_can( 'activate_plugins' ) ) { wp_die( 'You are not allowed to manage plugins on this site.' ); } $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); // Attempt to deactivate this plugin or theme. if ( ! empty( $local ) ) { $local = array_shift( $local ); if ( is_plugin_active( $local['_filename'] ) ) { deactivate_plugins( $local['_filename'] ); } $deactivated = ! is_plugin_active( $local['_filename'] ); } $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Get a local plugin/theme entry from product_id. * * @param int $product_id The product id. * * @return array|bool The array containing the local plugin/theme data or false. */ private static function _get_local_from_product_id( $product_id ) { $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); if ( ! empty( $local ) ) { return array_shift( $local ); } return false; } /** * Checks whether current site has product subscription of a given ID. * * @since 3.7.0 * * @param int $product_id The product id. * * @return bool Returns true if product subscription exists, false otherwise. */ public static function has_product_subscription( $product_id ) { $subscription = self::_get_subscriptions_from_product_id( $product_id, true ); return ! empty( $subscription ); } /** * Get a subscription entry from product_id. If multiple subscriptions are * found with the same product id and $single is set to true, will return the * first one in the list, so you can use this method to get things like extension * name, version, etc. * * @param int $product_id The product id. * @param bool $single Whether to return a single subscription or all matching a product id. * * @return array|bool The array containing sub data or false. */ private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); if ( ! empty( $subscriptions ) ) { return $single ? array_shift( $subscriptions ) : $subscriptions; } return false; } /** * Obtain a list of data about locally installed Woo extensions. */ public static function get_local_woo_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); /** * Check if plugins have WC headers, if not then clear cache and fetch again. * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated. * Also, get_plugins call is expensive so we should clear this cache very conservatively. */ if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) { wp_clean_plugins_cache( false ); $plugins = get_plugins(); } $woo_plugins = array(); // Backwards compatibility for woothemes_queue_update(). $_compat = array(); if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { $_compat[ $_compat_plugin->file ] = array( 'product_id' => $_compat_plugin->product_id, 'file_id' => $_compat_plugin->file_id, ); } } foreach ( $plugins as $filename => $data ) { if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); } if ( empty( $data['Woo'] ) ) { continue; } list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); if ( empty( $product_id ) || empty( $file_id ) ) { continue; } $data['_filename'] = $filename; $data['_product_id'] = absint( $product_id ); $data['_file_id'] = $file_id; $data['_type'] = 'plugin'; $data['slug'] = dirname( $filename ); $woo_plugins[ $filename ] = $data; } return $woo_plugins; } /** * Get locally installed Woo themes. */ public static function get_local_woo_themes() { $themes = wp_get_themes(); $woo_themes = array(); foreach ( $themes as $theme ) { $header = $theme->get( 'Woo' ); // Backwards compatibility for theme_info.txt. if ( ! $header ) { $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; if ( is_readable( $txt ) ) { $txt = file_get_contents( $txt ); $txt = preg_split( '#\s#', $txt ); if ( count( $txt ) >= 2 ) { $header = sprintf( '%d:%s', $txt[0], $txt[1] ); } } } if ( empty( $header ) ) { continue; } list( $product_id, $file_id ) = explode( ':', $header ); if ( empty( $product_id ) || empty( $file_id ) ) { continue; } $data = array( 'Name' => $theme->get( 'Name' ), 'Version' => $theme->get( 'Version' ), 'Woo' => $header, '_filename' => $theme->get_stylesheet() . '/style.css', '_stylesheet' => $theme->get_stylesheet(), '_product_id' => absint( $product_id ), '_file_id' => $file_id, '_type' => 'theme', ); $woo_themes[ $data['_filename'] ] = $data; } return $woo_themes; } /** * Get the connected user's subscriptions. * * @return array */ public static function get_subscriptions() { $cache_key = '_woocommerce_helper_subscriptions'; $data = get_transient( $cache_key ); if ( false !== $data ) { return $data; } // Obtain the connected user info. $request = WC_Helper_API::get( 'subscriptions', array( 'authenticated' => true, ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); return array(); } $data = json_decode( wp_remote_retrieve_body( $request ), true ); if ( empty( $data ) || ! is_array( $data ) ) { $data = array(); } set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); return $data; } /** * Runs when any plugin is activated. * * Depending on the activated plugin attempts to look through available * subscriptions and auto-activate one if possible, so the user does not * need to visit the Helper UI at all after installing a new extension. * * @param string $filename The filename of the activated plugin. */ public static function activated_plugin( $filename ) { $plugins = self::get_local_woo_plugins(); // Not a local woo plugin. if ( empty( $plugins[ $filename ] ) ) { return; } // Make sure we have a connection. $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth ) ) { return; } $plugin = $plugins[ $filename ]; $product_id = $plugin['_product_id']; $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); // No valid subscriptions for this product. if ( empty( $subscriptions ) ) { return; } $subscription = null; foreach ( $subscriptions as $_sub ) { // Don't attempt to activate expired subscriptions. if ( $_sub['expired'] ) { continue; } // No more sites available in this subscription. if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) { continue; } // Looks good. $subscription = $_sub; break; } // No valid subscription found. if ( ! $subscription ) { return; } $product_key = $subscription['product_key']; $activation_response = WC_Helper_API::post( 'activate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { $activated = true; } if ( $activated ) { self::log( 'Auto-activated a subscription for ' . $filename ); /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); } else { self::log( 'Could not activate a subscription upon plugin activation: ' . $filename ); /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } /** * Runs when any plugin is deactivated. * * When a user deactivates a plugin, attempt to deactivate any subscriptions * associated with the extension. * * @param string $filename The filename of the deactivated plugin. */ public static function deactivated_plugin( $filename ) { $plugins = self::get_local_woo_plugins(); // Not a local woo plugin. if ( empty( $plugins[ $filename ] ) ) { return; } // Make sure we have a connection. $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth ) ) { return; } $plugin = $plugins[ $filename ]; $product_id = $plugin['_product_id']; $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); $site_id = absint( $auth['site_id'] ); // No valid subscriptions for this product. if ( empty( $subscriptions ) ) { return; } $deactivated = 0; foreach ( $subscriptions as $subscription ) { // Don't touch subscriptions that aren't activated on this site. if ( ! in_array( $site_id, $subscription['connections'], true ) ) { continue; } $product_key = $subscription['product_key']; $deactivation_response = WC_Helper_API::post( 'deactivate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) { $deactivated++; /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); } else { /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); } } if ( $deactivated ) { self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) ); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } } /** * Various Helper-related admin notices. */ public static function admin_notices() { if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) { return; } $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; if ( 'update-core' !== $screen_id ) { return; } // Don't nag if Woo doesn't have an update available. if ( ! self::_woo_core_update_available() ) { return; } // Add a note about available extension updates if Woo core has an update available. $notice = self::_get_extensions_update_notice(); if ( ! empty( $notice ) ) { echo '<div class="updated woocommerce-message"><p>' . $notice . '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Get an update notice if one or more Woo extensions has an update available. * * @return string|null The update notice or null if everything is up to date. */ private static function _get_extensions_update_notice() { $plugins = self::get_local_woo_plugins(); $updates = WC_Helper_Updater::get_update_data(); $available = 0; foreach ( $plugins as $data ) { if ( empty( $updates[ $data['_product_id'] ] ) ) { continue; } $product_id = $data['_product_id']; if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) { $available++; } } if ( ! $available ) { return; } return sprintf( /* translators: %1$s: helper url, %2$d: number of extensions */ _n( 'Note: You currently have <a href="%1$s">%2$d paid extension</a> which should be updated first before updating WooCommerce.', 'Note: You currently have <a href="%1$s">%2$d paid extensions</a> which should be updated first before updating WooCommerce.', $available, 'woocommerce' ), admin_url( 'admin.php?page=wc-addons§ion=helper' ), $available ); } /** * Whether WooCommerce has an update available. * * @return bool True if a Woo core update is available. */ private static function _woo_core_update_available() { $updates = get_site_transient( 'update_plugins' ); if ( empty( $updates->response ) ) { return false; } if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) { return false; } $data = $updates->response['woocommerce/woocommerce.php']; if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) { return false; } return true; } /** * Flush subscriptions cache. */ public static function _flush_subscriptions_cache() { delete_transient( '_woocommerce_helper_subscriptions' ); } /** * Flush auth cache. */ public static function _flush_authentication_cache() { $request = WC_Helper_API::get( 'oauth/me', array( 'authenticated' => true, ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { return false; } $user_data = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! $user_data ) { return false; } WC_Helper_Options::update( 'auth_user_data', array( 'name' => $user_data['name'], 'email' => $user_data['email'], ) ); return true; } /** * Flush updates cache. */ private static function _flush_updates_cache() { WC_Helper_Updater::flush_updates_cache(); } /** * Sort subscriptions by the product_name. * * @param array $a Subscription array. * @param array $b Subscription array. * * @return int */ public static function _sort_by_product_name( $a, $b ) { return strcmp( $a['product_name'], $b['product_name'] ); } /** * Sort subscriptions by the Name. * * @param array $a Product array. * @param array $b Product array. * * @return int */ public static function _sort_by_name( $a, $b ) { return strcmp( $a['Name'], $b['Name'] ); } /** * Log a helper event. * * @param string $message Log message. * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug. */ public static function log( $message, $level = 'info' ) { if ( ! Constants::is_true( 'WP_DEBUG' ) ) { return; } if ( ! isset( self::$log ) ) { self::$log = wc_get_logger(); } self::$log->log( $level, $message, array( 'source' => 'helper' ) ); } } WC_Helper::load(); includes/admin/helper/class-wc-helper-updater.php 0000644 00000035251 15132754524 0016103 0 ustar 00 <?php /** * The update helper for WooCommerce.com plugins. * * @class WC_Helper_Updater * @package WooCommerce\Admin\Helper */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Helper_Updater Class * * Contains the logic to fetch available updates and hook into Core's update * routines to serve WooCommerce.com-provided packages. */ class WC_Helper_Updater { /** * Loads the class, runs on init. */ public static function load() { add_action( 'pre_set_site_transient_update_plugins', array( __CLASS__, 'transient_update_plugins' ), 21, 1 ); add_action( 'pre_set_site_transient_update_themes', array( __CLASS__, 'transient_update_themes' ), 21, 1 ); add_action( 'upgrader_process_complete', array( __CLASS__, 'upgrader_process_complete' ) ); add_action( 'upgrader_pre_download', array( __CLASS__, 'block_expired_updates' ), 10, 2 ); } /** * Runs in a cron thread, or in a visitor thread if triggered * by _maybe_update_plugins(), or in an auto-update thread. * * @param object $transient The update_plugins transient object. * * @return object The same or a modified version of the transient. */ public static function transient_update_plugins( $transient ) { $update_data = self::get_update_data(); foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { if ( empty( $update_data[ $plugin['_product_id'] ] ) ) { continue; } $data = $update_data[ $plugin['_product_id'] ]; $filename = $plugin['_filename']; $item = array( 'id' => 'woocommerce-com-' . $plugin['_product_id'], 'slug' => 'woocommerce-com-' . $data['slug'], 'plugin' => $filename, 'new_version' => $data['version'], 'url' => $data['url'], 'package' => $data['package'], 'upgrade_notice' => $data['upgrade_notice'], ); if ( isset( $data['requires_php'] ) ) { $item['requires_php'] = $data['requires_php']; } // We don't want to deliver a valid upgrade package when their subscription has expired. // To avoid the generic "no_package" error that empty strings give, we will store an // indication of expiration for the `upgrader_pre_download` filter to error on. if ( ! self::_has_active_subscription( $plugin['_product_id'] ) ) { $item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id']; } if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) { $transient->response[ $filename ] = (object) $item; unset( $transient->no_update[ $filename ] ); } else { $transient->no_update[ $filename ] = (object) $item; unset( $transient->response[ $filename ] ); } } $translations = self::get_translations_update_data(); $transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations ); return $transient; } /** * Runs on pre_set_site_transient_update_themes, provides custom * packages for WooCommerce.com-hosted extensions. * * @param object $transient The update_themes transient object. * * @return object The same or a modified version of the transient. */ public static function transient_update_themes( $transient ) { $update_data = self::get_update_data(); foreach ( WC_Helper::get_local_woo_themes() as $theme ) { if ( empty( $update_data[ $theme['_product_id'] ] ) ) { continue; } $data = $update_data[ $theme['_product_id'] ]; $slug = $theme['_stylesheet']; $item = array( 'theme' => $slug, 'new_version' => $data['version'], 'url' => $data['url'], 'package' => '', ); if ( self::_has_active_subscription( $theme['_product_id'] ) ) { $item['package'] = $data['package']; } if ( version_compare( $theme['Version'], $data['version'], '<' ) ) { $transient->response[ $slug ] = $item; } else { unset( $transient->response[ $slug ] ); $transient->checked[ $slug ] = $data['version']; } } return $transient; } /** * Get update data for all extensions. * * Scans through all subscriptions for the connected user, as well * as all Woo extensions without a subscription, and obtains update * data for each product. * * @return array Update data {product_id => data} */ public static function get_update_data() { $payload = array(); // Scan subscriptions. foreach ( WC_Helper::get_subscriptions() as $subscription ) { $payload[ $subscription['product_id'] ] = array( 'product_id' => $subscription['product_id'], 'file_id' => '', ); } // Scan local plugins which may or may not have a subscription. foreach ( WC_Helper::get_local_woo_plugins() as $data ) { if ( ! isset( $payload[ $data['_product_id'] ] ) ) { $payload[ $data['_product_id'] ] = array( 'product_id' => $data['_product_id'], ); } $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; } // Scan local themes. foreach ( WC_Helper::get_local_woo_themes() as $data ) { if ( ! isset( $payload[ $data['_product_id'] ] ) ) { $payload[ $data['_product_id'] ] = array( 'product_id' => $data['_product_id'], ); } $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; } return self::_update_check( $payload ); } /** * Get translations updates informations. * * Scans through all subscriptions for the connected user, as well * as all Woo extensions without a subscription, and obtains update * data for each product. * * @return array Update data {product_id => data} */ public static function get_translations_update_data() { $payload = array(); $installed_translations = wp_get_installed_translations( 'plugins' ); $locales = array_values( get_available_languages() ); /** * Filters the locales requested for plugin translations. * * @since 3.7.0 * @since 4.5.0 The default value of the `$locales` parameter changed to include all locales. * * @param array $locales Plugin locales. Default is all available locales of the site. */ $locales = apply_filters( 'plugins_update_check_locales', $locales ); $locales = array_unique( $locales ); // No locales, the respone will be empty, we can return now. if ( empty( $locales ) ) { return array(); } // Scan local plugins which may or may not have a subscription. $plugins = WC_Helper::get_local_woo_plugins(); $active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) ); /* * Use only plugins that are subscribed to the automatic translations updates. */ $active_for_translations = array_filter( $active_woo_plugins, function( $plugin ) use ( $plugins ) { return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false ); } ); // Nothing to check for, exit. if ( empty( $active_for_translations ) ) { return array(); } if ( wp_doing_cron() ) { $timeout = 30; } else { // Three seconds, plus one extra second for every 10 plugins. $timeout = 3 + (int) ( count( $active_for_translations ) / 10 ); } $request_body = array( 'locales' => $locales, 'plugins' => array(), ); foreach ( $active_for_translations as $active_plugin ) { $plugin = $plugins[ $active_plugin ]; $request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] ); } $raw_response = wp_remote_post( 'https://translate.wordpress.com/api/translations-updates/woocommerce', array( 'body' => json_encode( $request_body ), 'headers' => array( 'Content-Type: application/json' ), 'timeout' => $timeout, ) ); // Something wrong happened on the translate server side. $response_code = wp_remote_retrieve_response_code( $raw_response ); if ( 200 !== $response_code ) { return array(); } $response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); // API error, api returned but something was wrong. if ( array_key_exists( 'success', $response ) && false === $response['success'] ) { return array(); } $translations = array(); foreach ( $response['data'] as $plugin_name => $language_packs ) { foreach ( $language_packs as $language_pack ) { // Maybe we have this language pack already installed so lets check revision date. if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) { $installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] ); $new_translation_revision_time = new DateTime( $language_pack['last_modified'] ); // Skip if translation language pack is not newer than what is installed already. if ( $new_translation_revision_time <= $installed_translation_revision_time ) { continue; } } $translations[] = array( 'type' => 'plugin', 'slug' => $plugin_name, 'language' => $language_pack['wp_locale'], 'version' => $language_pack['version'], 'updated' => $language_pack['last_modified'], 'package' => $language_pack['package'], 'autoupdate' => true, ); } } return $translations; } /** * Run an update check API call. * * The call is cached based on the payload (product ids, file ids). If * the payload changes, the cache is going to miss. * * @param array $payload Information about the plugin to update. * @return array Update data for each requested product. */ private static function _update_check( $payload ) { ksort( $payload ); $hash = md5( wp_json_encode( $payload ) ); $cache_key = '_woocommerce_helper_updates'; $data = get_transient( $cache_key ); if ( false !== $data ) { if ( hash_equals( $hash, $data['hash'] ) ) { return $data['products']; } } $data = array( 'hash' => $hash, 'updated' => time(), 'products' => array(), 'errors' => array(), ); $request = WC_Helper_API::post( 'update-check', array( 'body' => wp_json_encode( array( 'products' => $payload ) ), 'authenticated' => true, ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { $data['errors'][] = 'http-error'; } else { $data['products'] = json_decode( wp_remote_retrieve_body( $request ), true ); } set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS ); return $data['products']; } /** * Check for an active subscription. * * Checks a given product id against all subscriptions on * the current site. Returns true if at least one active * subscription is found. * * @param int $product_id The product id to look for. * * @return bool True if active subscription found. */ private static function _has_active_subscription( $product_id ) { if ( ! isset( $auth ) ) { $auth = WC_Helper_Options::get( 'auth' ); } if ( ! isset( $subscriptions ) ) { $subscriptions = WC_Helper::get_subscriptions(); } if ( empty( $auth['site_id'] ) || empty( $subscriptions ) ) { return false; } // Check for an active subscription. foreach ( $subscriptions as $subscription ) { if ( $subscription['product_id'] != $product_id ) { continue; } if ( in_array( absint( $auth['site_id'] ), $subscription['connections'] ) ) { return true; } } return false; } /** * Get the number of products that have updates. * * @return int The number of products with updates. */ public static function get_updates_count() { $cache_key = '_woocommerce_helper_updates_count'; $count = get_transient( $cache_key ); if ( false !== $count ) { return $count; } // Don't fetch any new data since this function in high-frequency. if ( ! get_transient( '_woocommerce_helper_subscriptions' ) ) { return 0; } if ( ! get_transient( '_woocommerce_helper_updates' ) ) { return 0; } $count = 0; $update_data = self::get_update_data(); if ( empty( $update_data ) ) { set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); return $count; } // Scan local plugins. foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { if ( empty( $update_data[ $plugin['_product_id'] ] ) ) { continue; } if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) { $count++; } } // Scan local themes. foreach ( WC_Helper::get_local_woo_themes() as $theme ) { if ( empty( $update_data[ $theme['_product_id'] ] ) ) { continue; } if ( version_compare( $theme['Version'], $update_data[ $theme['_product_id'] ]['version'], '<' ) ) { $count++; } } set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); return $count; } /** * Return the updates count markup. * * @return string Updates count markup, empty string if no updates avairable. */ public static function get_updates_count_html() { $count = self::get_updates_count(); if ( ! $count ) { return ''; } $count_html = sprintf( '<span class="update-plugins count-%d"><span class="update-count">%d</span></span>', $count, number_format_i18n( $count ) ); return $count_html; } /** * Flushes cached update data. */ public static function flush_updates_cache() { delete_transient( '_woocommerce_helper_updates' ); delete_transient( '_woocommerce_helper_updates_count' ); delete_site_transient( 'update_plugins' ); delete_site_transient( 'update_themes' ); } /** * Fires when a user successfully updated a theme or a plugin. */ public static function upgrader_process_complete() { delete_transient( '_woocommerce_helper_updates_count' ); } /** * Hooked into the upgrader_pre_download filter in order to better handle error messaging around expired * plugin updates. Initially we were using an empty string, but the error message that no_package * results in does not fit the cause. * * @since 4.1.0 * @param bool $reply Holds the current filtered response. * @param string $package The path to the package file for the update. * @return false|WP_Error False to proceed with the update as normal, anything else to be returned instead of updating. */ public static function block_expired_updates( $reply, $package ) { // Don't override a reply that was set already. if ( false !== $reply ) { return $reply; } // Only for packages with expired subscriptions. if ( 0 !== strpos( $package, 'woocommerce-com-expired-' ) ) { return false; } return new WP_Error( 'woocommerce_subscription_expired', sprintf( // translators: %s: URL of WooCommerce.com subscriptions tab. __( 'Please visit the <a href="%s" target="_blank">subscriptions page</a> and renew to continue receiving updates.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) ) ); } } WC_Helper_Updater::load(); includes/admin/helper/views/html-section-account.php 0000644 00000001667 15132754524 0016651 0 ustar 00 <?php defined( 'ABSPATH' ) or exit(); ?> <a class="button button-update" href="<?php echo esc_url( $refresh_url ); ?>"><span class="dashicons dashicons-image-rotate"></span> <?php _e( 'Update', 'woocommerce' ); ?></a> <div class="user-info"> <header> <p><?php printf( __( 'Connected to WooCommerce.com', 'woocommerce' ) ); ?> <span class="chevron dashicons dashicons-arrow-down-alt2"></span></p> </header> <section> <p><?php echo get_avatar( $auth_user_data['email'], 48 ); ?> <?php echo esc_html( $auth_user_data['email'] ); ?></p> <div class="actions"> <a class="" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><span class="dashicons dashicons-admin-generic"></span> <?php _e( 'My Subscriptions', 'woocommerce' ); ?></a> <a class="" href="<?php echo esc_url( $disconnect_url ); ?>"><span class="dashicons dashicons-no"></span> <?php _e( 'Disconnect', 'woocommerce' ); ?></a> </div> </section> </div> includes/admin/helper/views/html-oauth-start.php 0000644 00000002660 15132754524 0016020 0 ustar 00 <?php /** * Admin -> WooCommerce -> Extensions -> WooCommerce.com Subscriptions main page. * * @package WooCommerce\Views */ defined( 'ABSPATH' ) || exit(); ?> <div class="wrap woocommerce wc-addons-wrap wc-helper"> <h1 class="screen-reader-text"><?php esc_html_e( 'WooCommerce Extensions', 'woocommerce' ); ?></h1> <?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?> <div class="start-container"> <div class="text"> <img src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/woocommerce_logo.png' ); ?>" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" style="width:180px;"> <?php if ( ! empty( $_GET['wc-helper-status'] ) && 'helper-disconnected' === $_GET['wc-helper-status'] ) : ?> <p><strong><?php esc_html_e( 'Sorry to see you go.', 'woocommerce' ); ?></strong> <?php esc_html_e( 'Feel free to reconnect again using the button below.', 'woocommerce' ); ?></p> <?php endif; ?> <h2><?php esc_html_e( 'Manage your subscriptions, get important product notifications, and updates, all from the convenience of your WooCommerce dashboard', 'woocommerce' ); ?></h2> <p><?php esc_html_e( 'Once connected, your WooCommerce.com purchases will be listed here.', 'woocommerce' ); ?></p> <p><a class="button button-primary button-helper-connect" href="<?php echo esc_url( $connect_url ); ?>"><?php esc_html_e( 'Connect', 'woocommerce' ); ?></a></p> </div> </div> </div> includes/admin/helper/views/html-main.php 0000644 00000025727 15132754524 0014502 0 ustar 00 <?php /** * Helper main view * * @package WooCommerce\Helper */ ?> <?php defined( 'ABSPATH' ) || exit(); ?> <div class="wrap woocommerce wc-subscriptions-wrap wc-helper"> <h1 class="screen-reader-text"><?php esc_html_e( 'My Subscriptions', 'woocommerce' ); ?></h1> <?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?> <div class="subscriptions-header"> <h2><?php esc_html_e( 'Subscriptions', 'woocommerce' ); ?></h2> <?php require WC_Helper::get_view_filename( 'html-section-account.php' ); ?> <p> <?php printf( wp_kses( /* translators: Introduction to list of WooCommerce.com extensions the merchant has subscriptions for. */ __( 'Below is a list of extensions available on your WooCommerce.com account. To receive extension updates please make sure the extension is installed, and its subscription activated and connected to your WooCommerce.com account. Extensions can be activated from the <a href="%s">Plugins</a> screen.', 'woocommerce' ), array( 'a' => array( 'href' => array(), ), ) ), esc_url( admin_url( 'plugins.php' ) ) ); ?> </p> </div> <ul class="subscription-filter"> <label><?php esc_html_e( 'Sort by:', 'woocommerce' ); ?> <span class="chevron dashicons dashicons-arrow-up-alt2"></span></label> <?php $filters = array_keys( WC_Helper::get_filters() ); $last_filter = array_pop( $filters ); $current_filter = WC_Helper::get_current_filter(); $counts = WC_Helper::get_filters_counts(); ?> <?php foreach ( WC_Helper::get_filters() as $key => $label ) : // Don't show empty filters. if ( empty( $counts[ $key ] ) ) { continue; } $url = admin_url( 'admin.php?page=wc-addons§ion=helper&filter=' . $key ); $class_html = $current_filter === $key ? 'class="current"' : ''; ?> <li> <a <?php echo esc_html( $class_html ); ?> href="<?php echo esc_url( $url ); ?>"> <?php echo esc_html( $label ); ?> <span class="count">(<?php echo absint( $counts[ $key ] ); ?>)</span> </a> </li> <?php endforeach; ?> </ul> <table class="wp-list-table widefat fixed striped"> <?php if ( ! empty( $subscriptions ) ) : ?> <?php foreach ( $subscriptions as $subscription ) : ?> <tbody> <tr class="wp-list-table__row is-ext-header"> <td class="wp-list-table__ext-details"> <div class="wp-list-table__ext-title"> <a href="<?php echo esc_url( $subscription['product_url'] ); ?>" target="_blank"> <?php echo esc_html( $subscription['product_name'] ); ?> </a> </div> <div class="wp-list-table__ext-description"> <?php if ( $subscription['lifetime'] ) : ?> <span class="renews"> <?php esc_html_e( 'Lifetime Subscription', 'woocommerce' ); ?> </span> <?php elseif ( $subscription['expired'] ) : ?> <span class="renews"> <strong><?php esc_html_e( 'Expired :(', 'woocommerce' ); ?></strong> <?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?> </span> <?php elseif ( $subscription['autorenew'] ) : ?> <span class="renews"> <?php esc_html_e( 'Auto renews on:', 'woocommerce' ); ?> <?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?> </span> <?php elseif ( $subscription['expiring'] ) : ?> <span class="renews"> <strong><?php esc_html_e( 'Expiring soon!', 'woocommerce' ); ?></strong> <?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?> </span> <?php else : ?> <span class="renews"> <?php esc_html_e( 'Expires on:', 'woocommerce' ); ?> <?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?> </span> <?php endif; ?> <br/> <span class="subscription"> <?php if ( ! $subscription['active'] && $subscription['maxed'] ) { /* translators: %1$d: sites active, %2$d max sites active */ printf( esc_html__( 'Subscription: Not available - %1$d of %2$d already in use', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) ); } elseif ( $subscription['sites_max'] > 0 ) { /* translators: %1$d: sites active, %2$d max sites active */ printf( esc_html__( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) ); } else { esc_html_e( 'Subscription: Unlimited', 'woocommerce' ); } // Check shared. if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) { /* translators: Email address of person who shared the subscription. */ printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) ); } elseif ( isset( $subscription['master_user_email'] ) ) { /* translators: Email address of person who shared the subscription. */ printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) ); } ?> </span> </div> </td> <td class="wp-list-table__ext-actions"> <?php if ( ! $subscription['active'] && $subscription['maxed'] ) : ?> <a class="button" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><?php esc_html_e( 'Upgrade', 'woocommerce' ); ?></a> <?php elseif ( ! $subscription['local']['installed'] && ! $subscription['expired'] ) : ?> <a class="button <?php echo empty( $subscription['download_primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription['download_url'] ); ?>" target="_blank"><?php esc_html_e( 'Download', 'woocommerce' ); ?></a> <?php elseif ( $subscription['active'] ) : ?> <span class="form-toggle__wrapper"> <a href="<?php echo esc_url( $subscription['deactivate_url'] ); ?>" class="form-toggle active is-compact" role="link" aria-checked="true"><?php esc_html_e( 'Active', 'woocommerce' ); ?></a> <label class="form-toggle__label" for="activate-extension"> <span class="form-toggle__label-content"> <label for="activate-extension"><?php esc_html_e( 'Active', 'woocommerce' ); ?></label> </span> <span class="form-toggle__switch"></span> </label> </span> <?php elseif ( ! $subscription['expired'] ) : ?> <span class="form-toggle__wrapper"> <a href="<?php echo esc_url( $subscription['activate_url'] ); ?>" class="form-toggle is-compact" role="link" aria-checked="false"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></a> <label class="form-toggle__label" for="activate-extension"> <span class="form-toggle__label-content"> <label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label> </span> <span class="form-toggle__switch"></span> </label> </span> <?php else : ?> <span class="form-toggle__wrapper"> <span class="form-toggle disabled is-compact"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span> <label class="form-toggle__label" for="activate-extension"> <span class="form-toggle__label-content"> <label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label> </span> </label> </span> <?php endif; ?> </td> </tr> <?php foreach ( $subscription['actions'] as $subscription_action ) : ?> <tr class="wp-list-table__row wp-list-table__ext-updates"> <td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>"> <p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span> <?php echo wp_kses_post( $subscription_action['message'] ); ?> </p> </td> <td class="wp-list-table__ext-actions"> <?php if ( ! empty( $subscription_action['button_label'] ) && ! empty( $subscription_action['button_url'] ) ) : ?> <a class="button <?php echo empty( $subscription_action['primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>"><?php echo esc_html( $subscription_action['button_label'] ); ?></a> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> <?php endforeach; ?> <?php else : ?> <tr> <td colspan="3"><em><?php esc_html_e( 'Could not find any subscriptions on your WooCommerce.com account', 'woocommerce' ); ?></td> </tr> <?php endif; ?> </tbody> </table> <?php if ( ! empty( $no_subscriptions ) ) : ?> <h2><?php esc_html_e( 'Installed Extensions without a Subscription', 'woocommerce' ); ?></h2> <p>Below is a list of WooCommerce.com products available on your site - but are either out-dated or do not have a valid subscription.</p> <table class="wp-list-table widefat fixed striped"> <?php /* Extensions without a subscription. */ ?> <?php foreach ( $no_subscriptions as $filename => $data ) : ?> <tbody> <tr class="wp-list-table__row is-ext-header"> <td class="wp-list-table__ext-details color-bar autorenews"> <div class="wp-list-table__ext-title"> <a href="<?php echo esc_url( $data['_product_url'] ); ?>" target="_blank"><?php echo esc_html( $data['Name'] ); ?></a> </div> <div class="wp-list-table__ext-description"> </div> </td> <td class="wp-list-table__ext-actions"> <span class="form-toggle__wrapper"> <span class="form-toggle disabled is-compact" ><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span> <label class="form-toggle__label" for="activate-extension"> <span class="form-toggle__label-content"> <label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label> </span> </label> </span> </td> </tr> <?php foreach ( $data['_actions'] as $subscription_action ) : ?> <tr class="wp-list-table__row wp-list-table__ext-updates"> <td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>"> <p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span> <?php echo wp_kses( $subscription_action['message'], array( 'a' => array( 'href' => array(), 'title' => array(), ), 'br' => array(), 'em' => array(), 'strong' => array(), ) ); ?> </p> </td> <td class="wp-list-table__ext-actions"> <a class="button" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>" target="_blank"><?php echo esc_html( $subscription_action['button_label'] ); ?></a> </td> </tr> <?php endforeach; ?> </tbody> <?php endforeach; ?> </table> <?php endif; ?> </div> includes/admin/helper/views/html-section-nav.php 0000644 00000001371 15132754524 0015771 0 ustar 00 <?php /** * Helper admin navigation. * * @package WooCommerce\Helper * * @deprecated 5.7.0 */ defined( 'ABSPATH' ) || exit(); ?> <nav class="nav-tab-wrapper woo-nav-tab-wrapper"> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons' ) ); ?>" class="nav-tab"><?php esc_html_e( 'Browse Extensions', 'woocommerce' ); ?></a> <?php $count_html = WC_Helper_Updater::get_updates_count_html(); /* translators: %s: WooCommerce.com Subscriptions tab count HTML. */ $menu_title = sprintf( __( 'WooCommerce.com Subscriptions %s', 'woocommerce' ), $count_html ); ?> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); ?>" class="nav-tab nav-tab-active"><?php echo wp_kses_post( $menu_title ); ?></a> </nav> includes/admin/helper/views/html-helper-compat.php 0000644 00000000726 15132754524 0016306 0 ustar 00 <?php defined( 'ABSPATH' ) or exit(); ?> <div class="wrap"> <h1><?php _e( 'Looking for the WooCommerce Helper?', 'woocommerce' ); ?></h1> <p><?php printf( __( 'We\'ve made things simpler and easier to manage moving forward. From now on you can manage all your WooCommerce purchases directly from the Extensions menu within the WooCommerce plugin itself. <a href="%s">View and manage</a> your extensions now.', 'woocommerce' ), esc_url( $helper_url ) ); ?></p> </div> includes/admin/helper/views/html-section-notices.php 0000644 00000000355 15132754524 0016652 0 ustar 00 <?php defined( 'ABSPATH' ) or exit(); ?> <?php foreach ( $notices as $notice ) : ?> <div class="notice <?php echo sanitize_html_class( $notice['type'] ); ?>"> <?php echo wpautop( $notice['message'] ); ?> </div> <?php endforeach; ?> includes/admin/settings/class-wc-settings-page.php 0000644 00000016560 15132754524 0016317 0 ustar 00 <?php /** * WooCommerce Settings Page/Tab * * @package WooCommerce\Admin * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'WC_Settings_Page', false ) ) : /** * WC_Settings_Page. */ abstract class WC_Settings_Page { /** * Setting page id. * * @var string */ protected $id = ''; /** * Setting page label. * * @var string */ protected $label = ''; /** * Constructor. */ public function __construct() { add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); } /** * Get settings page ID. * * @since 3.0.0 * @return string */ public function get_id() { return $this->id; } /** * Get settings page label. * * @since 3.0.0 * @return string */ public function get_label() { return $this->label; } /** * Add this page to settings. * * @param array $pages The setings array where we'll add ourselves. * * @return mixed */ public function add_settings_page( $pages ) { $pages[ $this->id ] = $this->label; return $pages; } /** * Get settings array for the default section. * * External settings classes (registered via 'woocommerce_get_settings_pages' filter) * might have redefined this method as "get_settings($section_id='')", thus we need * to use this method internally instead of 'get_settings_for_section' to register settings * and render settings pages. * * *But* we can't just redefine the method as "get_settings($section_id='')" here, since this * will break on PHP 8 if any external setting class have it as 'get_settings()'. * * Thus we leave the method signature as is and use 'func_get_arg' to get the setting id * if it's supplied, and we use this method internally; but it's deprecated and should * otherwise never be used. * * @deprecated 5.4.0 Use 'get_settings_for_section' (passing an empty string for default section) * * @return array Settings array, each item being an associative array representing a setting. */ public function get_settings() { $section_id = 0 === func_num_args() ? '' : func_get_arg( 0 ); return $this->get_settings_for_section( $section_id ); } /** * Get settings array. * * The strategy for getting the settings is as follows: * * - If a method named 'get_settings_for_{section_id}_section' exists in the class * it will be invoked (for the default '' section, the method name is 'get_settings_for_default_section'). * Derived classes can implement these methods as required. * * - Otherwise, 'get_settings_for_section_core' will be invoked. Derived classes can override it * as an alternative to implementing 'get_settings_for_{section_id}_section' methods. * * @param string $section_id The id of the section to return settings for, an empty string for the default section. * * @return array Settings array, each item being an associative array representing a setting. */ final public function get_settings_for_section( $section_id ) { if ( '' === $section_id ) { $method_name = 'get_settings_for_default_section'; } else { $method_name = "get_settings_for_{$section_id}_section"; } if ( method_exists( $this, $method_name ) ) { $settings = $this->$method_name(); } else { $settings = $this->get_settings_for_section_core( $section_id ); } return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $section_id ); } /** * Get the settings for a given section. * This method is invoked from 'get_settings_for_section' when no 'get_settings_for_{current_section}_section' * method exists in the class. * * When overriding, note that the 'woocommerce_get_settings_' filter must NOT be triggered, * as this is already done by 'get_settings_for_section'. * * @param string $section_id The section name to get the settings for. * * @return array Settings array, each item being an associative array representing a setting. */ protected function get_settings_for_section_core( $section_id ) { return array(); } /** * Get all sections for this page, both the own ones and the ones defined via filters. * * @return array */ public function get_sections() { $sections = $this->get_own_sections(); return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** * Get own sections for this page. * Derived classes should override this method if they define sections. * There should always be one default section with an empty string as identifier. * * Example: * return array( * '' => __( 'General', 'woocommerce' ), * 'foobars' => __( 'Foos & Bars', 'woocommerce' ), * ); * * @return array An associative array where keys are section identifiers and the values are translated section names. */ protected function get_own_sections() { return array( '' => __( 'General', 'woocommerce' ) ); } /** * Output sections. */ public function output_sections() { global $current_section; $sections = $this->get_sections(); if ( empty( $sections ) || 1 === count( $sections ) ) { return; } echo '<ul class="subsubsub">'; $array_keys = array_keys( $sections ); foreach ( $sections as $id => $label ) { $url = admin_url( 'admin.php?page=wc-settings&tab=' . $this->id . '§ion=' . sanitize_title( $id ) ); $class = ( $current_section === $id ? 'current' : '' ); $separator = ( end( $array_keys ) === $id ? '' : '|' ); $text = esc_html( $label ); echo "<li><a href='$url' class='$class'>$text</a> $separator </li>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo '</ul><br class="clear" />'; } /** * Output the HTML for the settings. */ public function output() { global $current_section; // We can't use "get_settings_for_section" here // for compatibility with derived classes overriding "get_settings". $settings = $this->get_settings( $current_section ); WC_Admin_Settings::output_fields( $settings ); } /** * Save settings and trigger the 'woocommerce_update_options_'.id action. */ public function save() { $this->save_settings_for_current_section(); $this->do_update_options_action(); } /** * Save settings for current section. */ protected function save_settings_for_current_section() { global $current_section; // We can't use "get_settings_for_section" here // for compatibility with derived classes overriding "get_settings". $settings = $this->get_settings( $current_section ); WC_Admin_Settings::save_fields( $settings ); } /** * Trigger the 'woocommerce_update_options_'.id action. * * @param string $section_id Section to trigger the action for, or null for current section. */ protected function do_update_options_action( $section_id = null ) { global $current_section; if ( is_null( $section_id ) ) { $section_id = $current_section; } if ( $section_id ) { do_action( 'woocommerce_update_options_' . $this->id . '_' . $section_id ); } } } endif; includes/admin/settings/class-wc-settings-products.php 0000644 00000037371 15132754524 0017251 0 ustar 00 <?php /** * WooCommerce Product Settings * * @package WooCommerce\Admin * @version 2.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Settings_Products', false ) ) { return new WC_Settings_Products(); } /** * WC_Settings_Products. */ class WC_Settings_Products extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'products'; $this->label = __( 'Products', 'woocommerce' ); parent::__construct(); } /** * Get own sections. * * @return array */ protected function get_own_sections() { return array( '' => __( 'General', 'woocommerce' ), 'inventory' => __( 'Inventory', 'woocommerce' ), 'downloadable' => __( 'Downloadable products', 'woocommerce' ), ); } /** * Get settings for the detault section. * * @return array */ protected function get_settings_for_default_section() { $settings = array( array( 'title' => __( 'Shop pages', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'catalog_options', ), array( 'title' => __( 'Shop page', 'woocommerce' ), /* translators: %s: URL to settings. */ 'desc' => sprintf( __( 'The base page can also be used in your <a href="%s">product permalinks</a>.', 'woocommerce' ), admin_url( 'options-permalink.php' ) ), 'id' => 'woocommerce_shop_page_id', 'type' => 'single_select_page', 'default' => '', 'class' => 'wc-enhanced-select-nostd', 'css' => 'min-width:300px;', 'desc_tip' => __( 'This sets the base page of your shop - this is where your product archive will be.', 'woocommerce' ), ), array( 'title' => __( 'Add to cart behaviour', 'woocommerce' ), 'desc' => __( 'Redirect to the cart page after successful addition', 'woocommerce' ), 'id' => 'woocommerce_cart_redirect_after_add', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', ), array( 'desc' => __( 'Enable AJAX add to cart buttons on archives', 'woocommerce' ), 'id' => 'woocommerce_enable_ajax_add_to_cart', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'end', ), array( 'title' => __( 'Placeholder image', 'woocommerce' ), 'id' => 'woocommerce_placeholder_image', 'type' => 'text', 'default' => '', 'class' => '', 'css' => '', 'placeholder' => __( 'Enter attachment ID or URL to an image', 'woocommerce' ), 'desc_tip' => __( 'This is the attachment ID, or image URL, used for placeholder images in the product catalog. Products with no image will use this.', 'woocommerce' ), ), array( 'type' => 'sectionend', 'id' => 'catalog_options', ), array( 'title' => __( 'Measurements', 'woocommerce' ), 'type' => 'title', 'id' => 'product_measurement_options', ), array( 'title' => __( 'Weight unit', 'woocommerce' ), 'desc' => __( 'This controls what unit you will define weights in.', 'woocommerce' ), 'id' => 'woocommerce_weight_unit', 'class' => 'wc-enhanced-select', 'css' => 'min-width:300px;', 'default' => 'kg', 'type' => 'select', 'options' => array( 'kg' => __( 'kg', 'woocommerce' ), 'g' => __( 'g', 'woocommerce' ), 'lbs' => __( 'lbs', 'woocommerce' ), 'oz' => __( 'oz', 'woocommerce' ), ), 'desc_tip' => true, ), array( 'title' => __( 'Dimensions unit', 'woocommerce' ), 'desc' => __( 'This controls what unit you will define lengths in.', 'woocommerce' ), 'id' => 'woocommerce_dimension_unit', 'class' => 'wc-enhanced-select', 'css' => 'min-width:300px;', 'default' => 'cm', 'type' => 'select', 'options' => array( 'm' => __( 'm', 'woocommerce' ), 'cm' => __( 'cm', 'woocommerce' ), 'mm' => __( 'mm', 'woocommerce' ), 'in' => __( 'in', 'woocommerce' ), 'yd' => __( 'yd', 'woocommerce' ), ), 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'product_measurement_options', ), array( 'title' => __( 'Reviews', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'product_rating_options', ), array( 'title' => __( 'Enable reviews', 'woocommerce' ), 'desc' => __( 'Enable product reviews', 'woocommerce' ), 'id' => 'woocommerce_enable_reviews', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'show_if_checked' => 'option', ), array( 'desc' => __( 'Show "verified owner" label on customer reviews', 'woocommerce' ), 'id' => 'woocommerce_review_rating_verification_label', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => '', 'show_if_checked' => 'yes', 'autoload' => false, ), array( 'desc' => __( 'Reviews can only be left by "verified owners"', 'woocommerce' ), 'id' => 'woocommerce_review_rating_verification_required', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'show_if_checked' => 'yes', 'autoload' => false, ), array( 'title' => __( 'Product ratings', 'woocommerce' ), 'desc' => __( 'Enable star rating on reviews', 'woocommerce' ), 'id' => 'woocommerce_enable_review_rating', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'show_if_checked' => 'option', ), array( 'desc' => __( 'Star ratings should be required, not optional', 'woocommerce' ), 'id' => 'woocommerce_review_rating_required', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'show_if_checked' => 'yes', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'product_rating_options', ), ); $settings = apply_filters( 'woocommerce_products_general_settings', $settings ); return apply_filters( 'woocommerce_product_settings', $settings ); } /** * Get settings for the inventory section. * * @return array */ protected function get_settings_for_inventory_section() { $settings = array( array( 'title' => __( 'Inventory', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'product_inventory_options', ), array( 'title' => __( 'Manage stock', 'woocommerce' ), 'desc' => __( 'Enable stock management', 'woocommerce' ), 'id' => 'woocommerce_manage_stock', 'default' => 'yes', 'type' => 'checkbox', ), array( 'title' => __( 'Hold stock (minutes)', 'woocommerce' ), 'desc' => __( 'Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.', 'woocommerce' ), 'id' => 'woocommerce_hold_stock_minutes', 'type' => 'number', 'custom_attributes' => array( 'min' => 0, 'step' => 1, ), 'css' => 'width: 80px;', 'default' => '60', 'autoload' => false, 'class' => 'manage_stock_field', ), array( 'title' => __( 'Notifications', 'woocommerce' ), 'desc' => __( 'Enable low stock notifications', 'woocommerce' ), 'id' => 'woocommerce_notify_low_stock', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'autoload' => false, 'class' => 'manage_stock_field', ), array( 'desc' => __( 'Enable out of stock notifications', 'woocommerce' ), 'id' => 'woocommerce_notify_no_stock', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'autoload' => false, 'class' => 'manage_stock_field', ), array( 'title' => __( 'Notification recipient(s)', 'woocommerce' ), 'desc' => __( 'Enter recipients (comma separated) that will receive this notification.', 'woocommerce' ), 'id' => 'woocommerce_stock_email_recipient', 'type' => 'text', 'default' => get_option( 'admin_email' ), 'css' => 'width: 250px;', 'autoload' => false, 'desc_tip' => true, 'class' => 'manage_stock_field', ), array( 'title' => __( 'Low stock threshold', 'woocommerce' ), 'desc' => __( 'When product stock reaches this amount you will be notified via email.', 'woocommerce' ), 'id' => 'woocommerce_notify_low_stock_amount', 'css' => 'width:50px;', 'type' => 'number', 'custom_attributes' => array( 'min' => 0, 'step' => 1, ), 'default' => '2', 'autoload' => false, 'desc_tip' => true, 'class' => 'manage_stock_field', ), array( 'title' => __( 'Out of stock threshold', 'woocommerce' ), 'desc' => __( 'When product stock reaches this amount the stock status will change to "out of stock" and you will be notified via email. This setting does not affect existing "in stock" products.', 'woocommerce' ), 'id' => 'woocommerce_notify_no_stock_amount', 'css' => 'width:50px;', 'type' => 'number', 'custom_attributes' => array( 'min' => 0, 'step' => 1, ), 'default' => '0', 'desc_tip' => true, 'class' => 'manage_stock_field', ), array( 'title' => __( 'Out of stock visibility', 'woocommerce' ), 'desc' => __( 'Hide out of stock items from the catalog', 'woocommerce' ), 'id' => 'woocommerce_hide_out_of_stock_items', 'default' => 'no', 'type' => 'checkbox', ), array( 'title' => __( 'Stock display format', 'woocommerce' ), 'desc' => __( 'This controls how stock quantities are displayed on the frontend.', 'woocommerce' ), 'id' => 'woocommerce_stock_format', 'css' => 'min-width:150px;', 'class' => 'wc-enhanced-select', 'default' => '', 'type' => 'select', 'options' => array( '' => __( 'Always show quantity remaining in stock e.g. "12 in stock"', 'woocommerce' ), 'low_amount' => __( 'Only show quantity remaining in stock when low e.g. "Only 2 left in stock"', 'woocommerce' ), 'no_amount' => __( 'Never show quantity remaining in stock', 'woocommerce' ), ), 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'product_inventory_options', ), ); return apply_filters( 'woocommerce_inventory_settings', $settings ); } /** * Get settings for the downloadable section. * * @return array */ protected function get_settings_for_downloadable_section() { $settings = array( array( 'title' => __( 'Downloadable products', 'woocommerce' ), 'type' => 'title', 'id' => 'digital_download_options', ), array( 'title' => __( 'File download method', 'woocommerce' ), 'desc_tip' => sprintf( /* translators: 1: X-Accel-Redirect 2: X-Sendfile 3: mod_xsendfile */ __( 'Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, %1$s / %2$s can be used to serve downloads instead (server requires %3$s).', 'woocommerce' ), '<code>X-Accel-Redirect</code>', '<code>X-Sendfile</code>', '<code>mod_xsendfile</code>' ), 'id' => 'woocommerce_file_download_method', 'type' => 'select', 'class' => 'wc-enhanced-select', 'css' => 'min-width:300px;', 'default' => 'force', 'desc' => sprintf( // translators: Link to WooCommerce Docs. __( "If you are using X-Accel-Redirect download method along with NGINX server, make sure that you have applied settings as described in <a href='%s'>Digital/Downloadable Product Handling</a> guide.", 'woocommerce' ), 'https://docs.woocommerce.com/document/digital-downloadable-product-handling#nginx-setting' ), 'options' => array( 'force' => __( 'Force downloads', 'woocommerce' ), 'xsendfile' => __( 'X-Accel-Redirect/X-Sendfile', 'woocommerce' ), 'redirect' => apply_filters( 'woocommerce_redirect_only_method_is_secure', false ) ? __( 'Redirect only', 'woocommerce' ) : __( 'Redirect only (Insecure)', 'woocommerce' ), ), 'autoload' => false, ), array( 'desc' => __( 'Allow using redirect mode (insecure) as a last resort', 'woocommerce' ), 'id' => 'woocommerce_downloads_redirect_fallback_allowed', 'type' => 'checkbox', 'default' => 'no', 'desc_tip' => sprintf( /* translators: %1$s is a link to the WooCommerce documentation. */ __( 'If the "Force Downloads" or "X-Accel-Redirect/X-Sendfile" download method is selected but does not work, the system will use the "Redirect" method as a last resort. <a href="%1$s">See this guide</a> for more details.', 'woocommerce' ), 'https://docs.woocommerce.com/document/digital-downloadable-product-handling/' ), 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'title' => __( 'Access restriction', 'woocommerce' ), 'desc' => __( 'Downloads require login', 'woocommerce' ), 'id' => 'woocommerce_downloads_require_login', 'type' => 'checkbox', 'default' => 'no', 'desc_tip' => __( 'This setting does not apply to guest purchases.', 'woocommerce' ), 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'desc' => __( 'Grant access to downloadable products after payment', 'woocommerce' ), 'id' => 'woocommerce_downloads_grant_access_after_payment', 'type' => 'checkbox', 'default' => 'yes', 'desc_tip' => __( 'Enable this option to grant access to downloads when orders are "processing", rather than "completed".', 'woocommerce' ), 'checkboxgroup' => 'end', 'autoload' => false, ), array( 'title' => __( 'Filename', 'woocommerce' ), 'desc' => __( 'Append a unique string to filename for security', 'woocommerce' ), 'id' => 'woocommerce_downloads_add_hash_to_filename', 'type' => 'checkbox', 'default' => 'yes', 'desc_tip' => sprintf( // translators: Link to WooCommerce Docs. __( "Not required if your download directory is protected. <a href='%s'>See this guide</a> for more details. Files already uploaded will not be affected.", 'woocommerce' ), 'https://docs.woocommerce.com/document/digital-downloadable-product-handling#unique-string' ), ), array( 'type' => 'sectionend', 'id' => 'digital_download_options', ), ); return apply_filters( 'woocommerce_downloadable_products_settings', $settings ); } /** * Save settings and trigger the 'woocommerce_update_options_'.id action. */ public function save() { $this->save_settings_for_current_section(); /* * Product->Inventory has a setting `Out of stock visibility`. * Because of this, we need to recount the terms to keep them in-sync. */ WC()->call_function( 'wc_recount_all_terms' ); $this->do_update_options_action(); } } return new WC_Settings_Products(); includes/admin/settings/class-wc-settings-tax.php 0000644 00000024222 15132754524 0016171 0 ustar 00 <?php /** * WooCommerce Tax Settings * * @package WooCommerce\Admin * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Settings_Tax', false ) ) { return new WC_Settings_Tax(); } /** * WC_Settings_Tax. */ class WC_Settings_Tax extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'tax'; $this->label = __( 'Tax', 'woocommerce' ); add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); if ( wc_tax_enabled() ) { add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); } } /** * Add this page to settings. * * @param array $pages Existing pages. * @return array|mixed */ public function add_settings_page( $pages ) { if ( wc_tax_enabled() ) { return parent::add_settings_page( $pages ); } else { return $pages; } } /** * Get own sections. * * @return array */ protected function get_own_sections() { $sections = array( '' => __( 'Tax options', 'woocommerce' ), 'standard' => __( 'Standard rates', 'woocommerce' ), ); // Get tax classes and display as links. $tax_classes = WC_Tax::get_tax_classes(); foreach ( $tax_classes as $class ) { /* translators: $s tax rate section name */ $sections[ sanitize_title( $class ) ] = sprintf( __( '%s rates', 'woocommerce' ), $class ); } return $sections; } /** * Get settings array. * * @return array */ public function get_settings_for_default_section() { return include __DIR__ . '/views/settings-tax.php'; } /** * Output the settings. */ public function output() { global $current_section; $tax_classes = WC_Tax::get_tax_class_slugs(); if ( 'standard' === $current_section || in_array( $current_section, array_filter( $tax_classes ), true ) ) { $this->output_tax_rates(); } else { parent::output(); } } /** * Save settings. */ public function save() { // phpcs:disable WordPress.Security.NonceVerification.Missing global $current_section; if ( ! $current_section ) { $this->save_settings_for_current_section(); if ( isset( $_POST['woocommerce_tax_classes'] ) ) { $this->save_tax_classes( wp_unslash( $_POST['woocommerce_tax_classes'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } } elseif ( ! empty( $_POST['tax_rate_country'] ) ) { $this->save_tax_rates(); } else { $this->save_settings_for_current_section(); } $this->do_update_options_action(); // Invalidate caches. WC_Cache_Helper::invalidate_cache_group( 'taxes' ); WC_Cache_Helper::get_transient_version( 'shipping', true ); // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Saves tax classes defined in the textarea to the tax class table instead of an option. * * @param string $raw_tax_classes Posted value. * @return null */ public function save_tax_classes( $raw_tax_classes ) { $tax_classes = array_filter( array_map( 'trim', explode( "\n", $raw_tax_classes ) ) ); $existing_tax_classes = WC_Tax::get_tax_classes(); $removed = array_diff( $existing_tax_classes, $tax_classes ); $added = array_diff( $tax_classes, $existing_tax_classes ); foreach ( $removed as $name ) { WC_Tax::delete_tax_class_by( 'name', $name ); } foreach ( $added as $name ) { $tax_class = WC_Tax::create_tax_class( $name ); // Display any error that could be triggered while creating tax classes. if ( is_wp_error( $tax_class ) ) { WC_Admin_Settings::add_error( sprintf( /* translators: 1: tax class name 2: error message */ esc_html__( 'Additional tax class "%1$s" couldn\'t be saved. %2$s.', 'woocommerce' ), esc_html( $name ), $tax_class->get_error_message() ) ); } } return null; } /** * Output tax rate tables. */ public function output_tax_rates() { global $current_section; $current_class = $this->get_current_tax_class(); $countries = array(); foreach ( WC()->countries->get_allowed_countries() as $value => $label ) { $countries[] = array( 'value' => $value, 'label' => esc_js( html_entity_decode( $label ) ), ); } $states = array(); foreach ( WC()->countries->get_allowed_country_states() as $label ) { foreach ( $label as $code => $state ) { $states[] = array( 'value' => $code, 'label' => esc_js( html_entity_decode( $state ) ), ); } } $base_url = admin_url( add_query_arg( array( 'page' => 'wc-settings', 'tab' => 'tax', 'section' => $current_section, ), 'admin.php' ) ); // Localize and enqueue our js. wp_localize_script( 'wc-settings-tax', 'htmlSettingsTaxLocalizeScript', array( 'current_class' => $current_class, 'wc_tax_nonce' => wp_create_nonce( 'wc_tax_nonce-class:' . $current_class ), 'base_url' => $base_url, 'rates' => array_values( WC_Tax::get_rates_for_tax_class( $current_class ) ), 'page' => ! empty( $_GET['p'] ) ? absint( $_GET['p'] ) : 1, // phpcs:ignore WordPress.Security.NonceVerification.Recommended 'limit' => 100, 'countries' => $countries, 'states' => $states, 'default_rate' => array( 'tax_rate_id' => 0, 'tax_rate_country' => '', 'tax_rate_state' => '', 'tax_rate' => '', 'tax_rate_name' => '', 'tax_rate_priority' => 1, 'tax_rate_compound' => 0, 'tax_rate_shipping' => 1, 'tax_rate_order' => null, 'tax_rate_class' => $current_class, ), 'strings' => array( 'no_rows_selected' => __( 'No row(s) selected', 'woocommerce' ), 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), 'csv_data_cols' => array( __( 'Country code', 'woocommerce' ), __( 'State code', 'woocommerce' ), __( 'Postcode / ZIP', 'woocommerce' ), __( 'City', 'woocommerce' ), __( 'Rate %', 'woocommerce' ), __( 'Tax name', 'woocommerce' ), __( 'Priority', 'woocommerce' ), __( 'Compound', 'woocommerce' ), __( 'Shipping', 'woocommerce' ), __( 'Tax class', 'woocommerce' ), ), ), ) ); wp_enqueue_script( 'wc-settings-tax' ); include __DIR__ . '/views/html-settings-tax.php'; } /** * Get tax class being edited. * * @return string */ private static function get_current_tax_class() { global $current_section; $tax_classes = WC_Tax::get_tax_classes(); $current_class = ''; foreach ( $tax_classes as $class ) { if ( sanitize_title( $class ) === $current_section ) { $current_class = $class; } } return $current_class; } /** * Get a posted tax rate. * * @param string $key Key of tax rate in the post data array. * @param int $order Position/order of rate. * @param string $class Tax class for rate. * @return array */ private function get_posted_tax_rate( $key, $order, $class ) { // phpcs:disable WordPress.Security.NonceVerification.Missing -- this is called from 'save_tax_rates' only, where nonce is already verified. $tax_rate = array(); $tax_rate_keys = array( 'tax_rate_country', 'tax_rate_state', 'tax_rate', 'tax_rate_name', 'tax_rate_priority', ); // phpcs:disable WordPress.Security.NonceVerification.Missing foreach ( $tax_rate_keys as $tax_rate_key ) { if ( isset( $_POST[ $tax_rate_key ], $_POST[ $tax_rate_key ][ $key ] ) ) { $tax_rate[ $tax_rate_key ] = wc_clean( wp_unslash( $_POST[ $tax_rate_key ][ $key ] ) ); } } $tax_rate['tax_rate_compound'] = isset( $_POST['tax_rate_compound'][ $key ] ) ? 1 : 0; $tax_rate['tax_rate_shipping'] = isset( $_POST['tax_rate_shipping'][ $key ] ) ? 1 : 0; $tax_rate['tax_rate_order'] = $order; $tax_rate['tax_rate_class'] = $class; // phpcs:enable WordPress.Security.NonceVerification.Missing return $tax_rate; // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Save tax rates. */ public function save_tax_rates() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- this is called via "do_action('woocommerce_settings_save_'...") in base class, where nonce is verified first. global $wpdb; $current_class = sanitize_title( $this->get_current_tax_class() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing $posted_countries = wc_clean( wp_unslash( $_POST['tax_rate_country'] ) ); // get the tax rate id of the first submited row. $first_tax_rate_id = key( $posted_countries ); // get the order position of the first tax rate id. $tax_rate_order = absint( $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_order FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $first_tax_rate_id ) ) ); $index = isset( $tax_rate_order ) ? $tax_rate_order : 0; // Loop posted fields. // phpcs:disable WordPress.Security.NonceVerification.Missing foreach ( $posted_countries as $key => $value ) { $mode = ( 0 === strpos( $key, 'new-' ) ) ? 'insert' : 'update'; $tax_rate = $this->get_posted_tax_rate( $key, $index ++, $current_class ); if ( 'insert' === $mode ) { $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); } elseif ( isset( $_POST['remove_tax_rate'][ $key ] ) && 1 === absint( $_POST['remove_tax_rate'][ $key ] ) ) { $tax_rate_id = absint( $key ); WC_Tax::_delete_tax_rate( $tax_rate_id ); continue; } else { $tax_rate_id = absint( $key ); WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate ); } if ( isset( $_POST['tax_rate_postcode'][ $key ] ) ) { WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( wp_unslash( $_POST['tax_rate_postcode'][ $key ] ) ) ); } if ( isset( $_POST['tax_rate_city'][ $key ] ) ) { WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( wp_unslash( $_POST['tax_rate_city'][ $key ] ) ) ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing } } return new WC_Settings_Tax(); includes/admin/settings/class-wc-settings-integrations.php 0000644 00000004170 15132754524 0020103 0 ustar 00 <?php /** * WooCommerce Integration Settings * * @package WooCommerce\Admin * @version 2.1.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Settings_Integrations', false ) ) : /** * WC_Settings_Integrations. */ class WC_Settings_Integrations extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'integration'; $this->label = __( 'Integration', 'woocommerce' ); if ( isset( WC()->integrations ) && WC()->integrations->get_integrations() ) { parent::__construct(); } } /** * Get own sections. * * @return array */ protected function get_own_sections() { global $current_section; $sections = array(); if ( ! $this->wc_is_installing() ) { $integrations = $this->get_integrations(); if ( ! $current_section && ! empty( $integrations ) ) { $current_section = current( $integrations )->id; } if ( count( $integrations ) > 1 ) { foreach ( $integrations as $integration ) { $title = empty( $integration->method_title ) ? ucfirst( $integration->id ) : $integration->method_title; $sections[ strtolower( $integration->id ) ] = esc_html( $title ); } } } return $sections; } /** * Is WC_INSTALLING constant defined? * This method exists to ease unit testing. * * @return bool True is the WC_INSTALLING constant is defined. */ protected function wc_is_installing() { return Constants::is_defined( 'WC_INSTALLING' ); } /** * Get the currently available integrations. * This method exists to ease unit testing. * * @return array Currently available integrations. */ protected function get_integrations() { return WC()->integrations->get_integrations(); } /** * Output the settings. */ public function output() { global $current_section; $integrations = $this->get_integrations(); if ( isset( $integrations[ $current_section ] ) ) { $integrations[ $current_section ]->admin_options(); } } } endif; return new WC_Settings_Integrations(); includes/admin/settings/class-wc-settings-emails.php 0000644 00000027670 15132754524 0016661 0 ustar 00 <?php /** * WooCommerce Email Settings * * @package WooCommerce\Admin * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Emails', false ) ) { return new WC_Settings_Emails(); } /** * WC_Settings_Emails. */ class WC_Settings_Emails extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'email'; $this->label = __( 'Emails', 'woocommerce' ); add_action( 'woocommerce_admin_field_email_notification', array( $this, 'email_notification_setting' ) ); parent::__construct(); } /** * Get own sections. * * @return array */ protected function get_own_sections() { return array( '' => __( 'Email options', 'woocommerce' ), ); } /** * Get settings array. * * @return array */ protected function get_settings_for_default_section() { $desc_help_text = sprintf( /* translators: %1$s: Link to WP Mail Logging plugin, %2$s: Link to Email FAQ support page. */ __( 'To ensure your store’s notifications arrive in your and your customers’ inboxes, we recommend connecting your email address to your domain and setting up a dedicated SMTP server. If something doesn’t seem to be sending correctly, install the <a href="%1$s">WP Mail Logging Plugin</a> or check the <a href="%2$s">Email FAQ page</a>.', 'woocommerce' ), 'https://wordpress.org/plugins/wp-mail-logging/', 'https://docs.woocommerce.com/document/email-faq' ); $settings = array( array( 'title' => __( 'Email notifications', 'woocommerce' ), /* translators: %s: help description with link to WP Mail logging and support page. */ 'desc' => sprintf( __( 'Email notifications sent from WooCommerce are listed below. Click on an email to configure it.<br>%s', 'woocommerce' ), $desc_help_text ), 'type' => 'title', 'id' => 'email_notification_settings', ), array( 'type' => 'email_notification' ), array( 'type' => 'sectionend', 'id' => 'email_notification_settings', ), array( 'type' => 'sectionend', 'id' => 'email_recipient_options', ), array( 'title' => __( 'Email sender options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'email_options', ), array( 'title' => __( '"From" name', 'woocommerce' ), 'desc' => __( 'How the sender name appears in outgoing WooCommerce emails.', 'woocommerce' ), 'id' => 'woocommerce_email_from_name', 'type' => 'text', 'css' => 'min-width:400px;', 'default' => esc_attr( get_bloginfo( 'name', 'display' ) ), 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( '"From" address', 'woocommerce' ), 'desc' => __( 'How the sender email appears in outgoing WooCommerce emails.', 'woocommerce' ), 'id' => 'woocommerce_email_from_address', 'type' => 'email', 'custom_attributes' => array( 'multiple' => 'multiple', ), 'css' => 'min-width:400px;', 'default' => get_option( 'admin_email' ), 'autoload' => false, 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'email_options', ), array( 'title' => __( 'Email template', 'woocommerce' ), 'type' => 'title', /* translators: %s: Nonced email preview link */ 'desc' => sprintf( __( 'This section lets you customize the WooCommerce emails. <a href="%s" target="_blank">Click here to preview your email template</a>.', 'woocommerce' ), wp_nonce_url( admin_url( '?preview_woocommerce_mail=true' ), 'preview-mail' ) ), 'id' => 'email_template_options', ), array( 'title' => __( 'Header image', 'woocommerce' ), 'desc' => __( 'URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).', 'woocommerce' ), 'id' => 'woocommerce_email_header_image', 'type' => 'text', 'css' => 'min-width:400px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => '', 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( 'Footer text', 'woocommerce' ), /* translators: %s: Available placeholders for use */ 'desc' => __( 'The text to appear in the footer of all WooCommerce emails.', 'woocommerce' ) . ' ' . sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title} {site_url}' ), 'id' => 'woocommerce_email_footer_text', 'css' => 'width:400px; height: 75px;', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'textarea', 'default' => '{site_title} — Built with {WooCommerce}', 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( 'Base color', 'woocommerce' ), /* translators: %s: default color */ 'desc' => sprintf( __( 'The base color for WooCommerce email templates. Default %s.', 'woocommerce' ), '<code>#96588a</code>' ), 'id' => 'woocommerce_email_base_color', 'type' => 'color', 'css' => 'width:6em;', 'default' => '#96588a', 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( 'Background color', 'woocommerce' ), /* translators: %s: default color */ 'desc' => sprintf( __( 'The background color for WooCommerce email templates. Default %s.', 'woocommerce' ), '<code>#f7f7f7</code>' ), 'id' => 'woocommerce_email_background_color', 'type' => 'color', 'css' => 'width:6em;', 'default' => '#f7f7f7', 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( 'Body background color', 'woocommerce' ), /* translators: %s: default color */ 'desc' => sprintf( __( 'The main body background color. Default %s.', 'woocommerce' ), '<code>#ffffff</code>' ), 'id' => 'woocommerce_email_body_background_color', 'type' => 'color', 'css' => 'width:6em;', 'default' => '#ffffff', 'autoload' => false, 'desc_tip' => true, ), array( 'title' => __( 'Body text color', 'woocommerce' ), /* translators: %s: default color */ 'desc' => sprintf( __( 'The main body text color. Default %s.', 'woocommerce' ), '<code>#3c3c3c</code>' ), 'id' => 'woocommerce_email_text_color', 'type' => 'color', 'css' => 'width:6em;', 'default' => '#3c3c3c', 'autoload' => false, 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'email_template_options', ), array( 'title' => __( 'Store management insights', 'woocommerce' ), 'type' => 'title', 'id' => 'email_merchant_notes', ), array( 'title' => __( 'Enable email insights', 'woocommerce' ), 'desc' => __( 'Receive email notifications with additional guidance to complete the basic store setup and helpful insights', 'woocommerce' ), 'id' => 'woocommerce_merchant_email_notifications', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'default' => 'no', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'email_merchant_notes', ), ); return apply_filters( 'woocommerce_email_settings', $settings ); } /** * Output the settings. */ public function output() { global $current_section; // Define emails that can be customised here. $mailer = WC()->mailer(); $email_templates = $mailer->get_emails(); if ( $current_section ) { foreach ( $email_templates as $email_key => $email ) { if ( strtolower( $email_key ) === $current_section ) { $this->run_email_admin_options( $email ); break; } } } parent::output(); } /** * Run the 'admin_options' method on a given email. * This method exists to easy unit testing. * * @param object $email The email object to run the method on. */ protected function run_email_admin_options( $email ) { $email->admin_options(); } /** * Save settings. */ public function save() { global $current_section; if ( ! $current_section ) { $this->save_settings_for_current_section(); $this->do_update_options_action(); } else { $wc_emails = WC_Emails::instance(); if ( in_array( $current_section, array_map( 'sanitize_title', array_keys( $wc_emails->get_emails() ) ), true ) ) { foreach ( $wc_emails->get_emails() as $email_id => $email ) { if ( sanitize_title( $email_id ) === $current_section ) { $this->do_update_options_action( $email->id ); } } } else { $this->save_settings_for_current_section(); $this->do_update_options_action(); } } } /** * Output email notification settings. */ public function email_notification_setting() { // Define emails that can be customised here. $mailer = WC()->mailer(); $email_templates = $mailer->get_emails(); ?> <tr valign="top"> <td class="wc_emails_wrapper" colspan="2"> <table class="wc_emails widefat" cellspacing="0"> <thead> <tr> <?php $columns = apply_filters( 'woocommerce_email_setting_columns', array( 'status' => '', 'name' => __( 'Email', 'woocommerce' ), 'email_type' => __( 'Content type', 'woocommerce' ), 'recipient' => __( 'Recipient(s)', 'woocommerce' ), 'actions' => '', ) ); foreach ( $columns as $key => $column ) { echo '<th class="wc-email-settings-table-' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>'; } ?> </tr> </thead> <tbody> <?php foreach ( $email_templates as $email_key => $email ) { echo '<tr>'; foreach ( $columns as $key => $column ) { switch ( $key ) { case 'name': echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '"> <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=email§ion=' . strtolower( $email_key ) ) ) . '">' . esc_html( $email->get_title() ) . '</a> ' . wc_help_tip( $email->get_description() ) . ' </td>'; break; case 'recipient': echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '"> ' . esc_html( $email->is_customer_email() ? __( 'Customer', 'woocommerce' ) : $email->get_recipient() ) . ' </td>'; break; case 'status': echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">'; if ( $email->is_manual() ) { echo '<span class="status-manual tips" data-tip="' . esc_attr__( 'Manually sent', 'woocommerce' ) . '">' . esc_html__( 'Manual', 'woocommerce' ) . '</span>'; } elseif ( $email->is_enabled() ) { echo '<span class="status-enabled tips" data-tip="' . esc_attr__( 'Enabled', 'woocommerce' ) . '">' . esc_html__( 'Yes', 'woocommerce' ) . '</span>'; } else { echo '<span class="status-disabled tips" data-tip="' . esc_attr__( 'Disabled', 'woocommerce' ) . '">-</span>'; } echo '</td>'; break; case 'email_type': echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '"> ' . esc_html( $email->get_content_type() ) . ' </td>'; break; case 'actions': echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '"> <a class="button alignright" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=email§ion=' . strtolower( $email_key ) ) ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a> </td>'; break; default: do_action( 'woocommerce_email_setting_column_' . $key, $email ); break; } } echo '</tr>'; } ?> </tbody> </table> </td> </tr> <?php } } return new WC_Settings_Emails(); includes/admin/settings/views/html-admin-page-shipping-zones.php 0000644 00000014071 15132754524 0021102 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <h2 class="wc-shipping-zones-heading"> <?php _e( 'Shipping zones', 'woocommerce' ); ?> <a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=new' ); ?>" class="page-title-action"><?php esc_html_e( 'Add shipping zone', 'woocommerce' ); ?></a> </h2> <p><?php echo __( 'A shipping zone is a geographic region where a certain set of shipping methods are offered.', 'woocommerce' ) . ' ' . __( 'WooCommerce will match a customer to a single zone using their shipping address and present the shipping methods within that zone to them.', 'woocommerce' ); ?></p> <table class="wc-shipping-zones widefat"> <thead> <tr> <th class="wc-shipping-zone-sort"><?php echo wc_help_tip( __( 'Drag and drop to re-order your custom zones. This is the order in which they will be matched against the customer address.', 'woocommerce' ) ); ?></th> <th class="wc-shipping-zone-name"><?php esc_html_e( 'Zone name', 'woocommerce' ); ?></th> <th class="wc-shipping-zone-region"><?php esc_html_e( 'Region(s)', 'woocommerce' ); ?></th> <th class="wc-shipping-zone-methods"><?php esc_html_e( 'Shipping method(s)', 'woocommerce' ); ?></th> </tr> </thead> <tbody class="wc-shipping-zone-rows"></tbody> <tbody> <tr data-id="0" class="wc-shipping-zone-worldwide"> <td width="1%" class="wc-shipping-zone-worldwide"></td> <td class="wc-shipping-zone-name"> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=0' ) ); ?>"><?php esc_html_e( 'Locations not covered by your other zones', 'woocommerce' ); ?></a> <div class="row-actions"> <a href="admin.php?page=wc-settings&tab=shipping&zone_id=0"><?php _e( 'Manage shipping methods', 'woocommerce' ); ?></a> </div> </td> <td class="wc-shipping-zone-region"><?php _e( 'This zone is <b>optionally</b> used for regions that are not included in any other shipping zone.', 'woocommerce' ); ?></td> <td class="wc-shipping-zone-methods"> <ul> <?php $worldwide = new WC_Shipping_Zone( 0 ); $methods = $worldwide->get_shipping_methods(); uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' ); if ( ! empty( $methods ) ) { foreach ( $methods as $method ) { $class_name = 'yes' === $method->enabled ? 'method_enabled' : 'method_disabled'; echo '<li class="wc-shipping-zone-method ' . esc_attr( $class_name ) . '">' . esc_html( $method->get_title() ) . '</li>'; } } else { echo '<li class="wc-shipping-zone-method">' . __( 'No shipping methods offered to this zone.', 'woocommerce' ) . '</li>'; } ?> </ul> </td> </tr> </tbody> </table> <script type="text/html" id="tmpl-wc-shipping-zone-row-blank"> <?php if ( 0 === $method_count ) : ?> <tr> <td class="wc-shipping-zones-blank-state" colspan="4"> <p class="main"><?php _e( 'A shipping zone is a geographic region where a certain set of shipping methods and rates apply.', 'woocommerce' ); ?></p> <p><?php _e( 'For example:', 'woocommerce' ); ?></p> <ul> <li><?php _e( 'Local zone = California ZIP 90210 = Local pickup', 'woocommerce' ); ?> <li><?php _e( 'US domestic zone = All US states = Flat rate shipping', 'woocommerce' ); ?> <li><?php _e( 'Europe zone = Any country in Europe = Flat rate shipping', 'woocommerce' ); ?> </ul> <p><?php _e( 'Add as many zones as you need – customers will only see the methods available for their address.', 'woocommerce' ); ?></p> <a class="button button-primary wc-shipping-zone-add" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=new' ) ); ?>"><?php _e( 'Add shipping zone', 'woocommerce' ); ?></a> </td> </tr> <?php endif; ?> </script> <script type="text/html" id="tmpl-wc-shipping-zone-row"> <tr data-id="{{ data.zone_id }}"> <td width="1%" class="wc-shipping-zone-sort"></td> <td class="wc-shipping-zone-name"> <a href="admin.php?page=wc-settings&tab=shipping&zone_id={{ data.zone_id }}">{{ data.zone_name }}</a> <div class="row-actions"> <a href="admin.php?page=wc-settings&tab=shipping&zone_id={{ data.zone_id }}"><?php _e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-zone-delete"><?php _e( 'Delete', 'woocommerce' ); ?></a> </div> </td> <td class="wc-shipping-zone-region"> {{ data.formatted_zone_location }} </td> <td class="wc-shipping-zone-methods"> <div><ul></ul></div> </td> </tr> </script> <script type="text/template" id="tmpl-wc-modal-add-shipping-method"> <div class="wc-backbone-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php _e( 'Add shipping method', 'woocommerce' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php _e( 'Close modal panel', 'woocommerce' ); ?></span> </button> </header> <article> <form action="" method="post"> <div class="wc-shipping-zone-method-selector"> <p><?php esc_html_e( 'Choose the shipping method you wish to add. Only shipping methods which support zones are listed.', 'woocommerce' ); ?></p> <select name="add_method_id"> <?php foreach ( WC()->shipping()->load_shipping_methods() as $method ) { if ( ! $method->supports( 'shipping-zones' ) ) { continue; } echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>'; } ?> </select> <input type="hidden" name="zone_id" value="{{{ data.zone_id }}}" /> </div> </form> </article> <footer> <div class="inner"> <button id="btn-ok" class="button button-primary button-large"><?php _e( 'Add shipping method', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/admin/settings/views/html-admin-page-shipping-zones-instance.php 0000644 00000000746 15132754524 0022710 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <h2> <a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping' ); ?>"><?php _e( 'Shipping zones', 'woocommerce' ); ?></a> > <a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=' . absint( $zone->get_id() ) ); ?>"><?php echo esc_html( $zone->get_zone_name() ); ?></a> > <?php echo esc_html( $shipping_method->get_method_title() ); ?> </h2> <?php $shipping_method->admin_options(); ?> includes/admin/settings/views/html-admin-page-shipping-zone-methods.php 0000644 00000023226 15132754524 0022362 0 ustar 00 <?php /** * Shipping zone admin * * @package WooCommerce\Admin\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <h2> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>"><?php esc_html_e( 'Shipping zones', 'woocommerce' ); ?></a> > <span class="wc-shipping-zone-name"><?php echo esc_html( $zone->get_zone_name() ? $zone->get_zone_name() : __( 'Zone', 'woocommerce' ) ); ?></span> </h2> <?php do_action( 'woocommerce_shipping_zone_before_methods_table', $zone ); ?> <table class="form-table wc-shipping-zone-settings"> <tbody> <?php if ( 0 !== $zone->get_id() ) : ?> <tr valign="top" class=""> <th scope="row" class="titledesc"> <label for="zone_name"> <?php esc_html_e( 'Zone name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the name of the zone for your reference.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?> </label> </th> <td class="forminp"> <input type="text" data-attribute="zone_name" name="zone_name" id="zone_name" value="<?php echo esc_attr( $zone->get_zone_name( 'edit' ) ); ?>" placeholder="<?php esc_attr_e( 'Zone name', 'woocommerce' ); ?>"> </td> </tr> <tr valign="top" class=""> <th scope="row" class="titledesc"> <label for="zone_locations"> <?php esc_html_e( 'Zone regions', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'These are regions inside this zone. Customers will be matched against these regions.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?> </label> </th> <td class="forminp"> <select multiple="multiple" data-attribute="zone_locations" id="zone_locations" name="zone_locations" data-placeholder="<?php esc_attr_e( 'Select regions within this zone', 'woocommerce' ); ?>" class="wc-shipping-zone-region-select chosen_select"> <?php foreach ( $shipping_continents as $continent_code => $continent ) { echo '<option value="continent:' . esc_attr( $continent_code ) . '"' . wc_selected( "continent:$continent_code", $locations ) . '>' . esc_html( $continent['name'] ) . '</option>'; $countries = array_intersect( array_keys( $allowed_countries ), $continent['countries'] ); foreach ( $countries as $country_code ) { echo '<option value="country:' . esc_attr( $country_code ) . '"' . wc_selected( "country:$country_code", $locations ) . '>' . esc_html( ' ' . $allowed_countries[ $country_code ] ) . '</option>'; $states = WC()->countries->get_states( $country_code ); if ( $states ) { foreach ( $states as $state_code => $state_name ) { echo '<option value="state:' . esc_attr( $country_code . ':' . $state_code ) . '"' . wc_selected( "state:$country_code:$state_code", $locations ) . '>' . esc_html( ' ' . $state_name . ', ' . $allowed_countries[ $country_code ] ) . '</option>'; } } } } ?> </select> <?php if ( empty( $postcodes ) ) : ?> <a class="wc-shipping-zone-postcodes-toggle" href="#"><?php esc_html_e( 'Limit to specific ZIP/postcodes', 'woocommerce' ); ?></a> <?php endif; ?> <div class="wc-shipping-zone-postcodes"> <textarea name="zone_postcodes" data-attribute="zone_postcodes" id="zone_postcodes" placeholder="<?php esc_attr_e( 'List 1 postcode per line', 'woocommerce' ); ?>" class="input-text large-text" cols="25" rows="5"><?php echo esc_textarea( implode( "\n", $postcodes ) ); ?></textarea> <?php /* translators: WooCommerce link to setting up shipping zones */ ?> <span class="description"><?php printf( __( 'Postcodes containing wildcards (e.g. CB23*) or fully numeric ranges (e.g. <code>90210...99000</code>) are also supported. Please see the shipping zones <a href="%s" target="_blank">documentation</a> for more information.', 'woocommerce' ), 'https://docs.woocommerce.com/document/setting-up-shipping-zones/#section-3' ); ?></span><?php // @codingStandardsIgnoreLine. ?> </div> </td> <?php endif; ?> </tr> <tr valign="top" class=""> <th scope="row" class="titledesc"> <label> <?php esc_html_e( 'Shipping methods', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'The following shipping methods apply to customers with shipping addresses within this zone.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?> </label> </th> <td class=""> <table class="wc-shipping-zone-methods widefat"> <thead> <tr> <th class="wc-shipping-zone-method-sort"></th> <th class="wc-shipping-zone-method-title"><?php esc_html_e( 'Title', 'woocommerce' ); ?></th> <th class="wc-shipping-zone-method-enabled"><?php esc_html_e( 'Enabled', 'woocommerce' ); ?></th> <th class="wc-shipping-zone-method-description"><?php esc_html_e( 'Description', 'woocommerce' ); ?></th> </tr> </thead> <tfoot> <tr> <td colspan="4"> <button type="submit" class="button wc-shipping-zone-add-method" value="<?php esc_attr_e( 'Add shipping method', 'woocommerce' ); ?>"><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></button> </td> </tr> </tfoot> <tbody class="wc-shipping-zone-method-rows"></tbody> </table> </td> </tr> </tbody> </table> <?php do_action( 'woocommerce_shipping_zone_after_methods_table', $zone ); ?> <p class="submit"> <button type="submit" name="submit" id="submit" class="button button-primary button-large wc-shipping-zone-method-save" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>" disabled><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button> </p> <script type="text/html" id="tmpl-wc-shipping-zone-method-row-blank"> <tr> <td class="wc-shipping-zone-method-blank-state" colspan="4"> <p><?php esc_html_e( 'You can add multiple shipping methods within this zone. Only customers within the zone will see them.', 'woocommerce' ); ?></p> </td> </tr> </script> <script type="text/html" id="tmpl-wc-shipping-zone-method-row"> <tr data-id="{{ data.instance_id }}" data-enabled="{{ data.enabled }}"> <td width="1%" class="wc-shipping-zone-method-sort"></td> <td class="wc-shipping-zone-method-title"> <a class="wc-shipping-zone-method-settings" href="admin.php?page=wc-settings&tab=shipping&instance_id={{ data.instance_id }}">{{{ data.title }}}</a> <div class="row-actions"> <a class="wc-shipping-zone-method-settings" href="admin.php?page=wc-settings&tab=shipping&instance_id={{ data.instance_id }}"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-zone-method-delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a> </div> </td> <td width="1%" class="wc-shipping-zone-method-enabled"><a href="#">{{{ data.enabled_icon }}}</a></td> <td class="wc-shipping-zone-method-description"> <strong class="wc-shipping-zone-method-type">{{ data.method_title }}</strong> {{{ data.method_description }}} </td> </tr> </script> <script type="text/template" id="tmpl-wc-modal-shipping-method-settings"> <div class="wc-backbone-modal wc-backbone-modal-shipping-method-settings"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1> <?php printf( /* translators: %s: shipping method title */ esc_html__( '%s Settings', 'woocommerce' ), '{{{ data.method.method_title }}}' ); ?> </h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span> </button> </header> <article class="wc-modal-shipping-method-settings"> <form action="" method="post"> {{{ data.method.settings_html }}} <input type="hidden" name="instance_id" value="{{{ data.instance_id }}}" /> </form> </article> <footer> <div class="inner"> <button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <script type="text/template" id="tmpl-wc-modal-add-shipping-method"> <div class="wc-backbone-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span> </button> </header> <article> <form action="" method="post"> <div class="wc-shipping-zone-method-selector"> <p><?php esc_html_e( 'Choose the shipping method you wish to add. Only shipping methods which support zones are listed.', 'woocommerce' ); ?></p> <select name="add_method_id"> <?php foreach ( WC()->shipping()->load_shipping_methods() as $method ) { if ( ! $method->supports( 'shipping-zones' ) ) { continue; } echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>'; } ?> </select> </div> </form> </article> <footer> <div class="inner"> <button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/admin/settings/views/html-keys-edit.php 0000644 00000013441 15132754524 0016023 0 ustar 00 <?php /** * Admin view: Edit API keys * * @package WooCommerce\Admin\Settings */ defined( 'ABSPATH' ) || exit; ?> <div id="key-fields" class="settings-panel"> <h2><?php esc_html_e( 'Key details', 'woocommerce' ); ?></h2> <input type="hidden" id="key_id" value="<?php echo esc_attr( $key_id ); ?>" /> <table id="api-keys-options" class="form-table"> <tbody> <tr valign="top"> <th scope="row" class="titledesc"> <label for="key_description"> <?php esc_html_e( 'Description', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Friendly name for identifying this key.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <input id="key_description" type="text" class="input-text regular-input" value="<?php echo esc_attr( $key_data['description'] ); ?>" /> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="key_user"> <?php esc_html_e( 'User', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Owner of these keys.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <?php $current_user_id = get_current_user_id(); $user_id = ! empty( $key_data['user_id'] ) ? absint( $key_data['user_id'] ) : $current_user_id; $user = get_user_by( 'id', $user_id ); $user_string = sprintf( /* translators: 1: user display name 2: user ID 3: user email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), $user->display_name, absint( $user->ID ), $user->user_email ); ?> <select class="wc-customer-search" id="key_user" data-placeholder="<?php esc_attr_e( 'Search for a user…', 'woocommerce' ); ?>" data-allow_clear="true"> <option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option> </select> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="key_permissions"> <?php esc_html_e( 'Permissions', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Select the access type of these keys.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <select id="key_permissions" class="wc-enhanced-select"> <?php $permissions = array( 'read' => __( 'Read', 'woocommerce' ), 'write' => __( 'Write', 'woocommerce' ), 'read_write' => __( 'Read/Write', 'woocommerce' ), ); foreach ( $permissions as $permission_id => $permission_name ) : ?> <option value="<?php echo esc_attr( $permission_id ); ?>" <?php selected( $key_data['permissions'], $permission_id, true ); ?>><?php echo esc_html( $permission_name ); ?></option> <?php endforeach; ?> </select> </td> </tr> <?php if ( 0 !== $key_id ) : ?> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Consumer key ending in', 'woocommerce' ); ?> </th> <td class="forminp"> <code>…<?php echo esc_html( $key_data['truncated_key'] ); ?></code> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Last access', 'woocommerce' ); ?> </th> <td class="forminp"> <span> <?php if ( ! empty( $key_data['last_access'] ) ) { /* translators: 1: last access date 2: last access time */ $date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key_data['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key_data['last_access'] ) ) ); echo esc_html( apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key_data['last_access'] ) ); } else { esc_html_e( 'Unknown', 'woocommerce' ); } ?> </span> </td> </tr> <?php endif ?> </tbody> </table> <?php do_action( 'woocommerce_admin_key_fields', $key_data ); ?> <?php if ( 0 === intval( $key_id ) ) { submit_button( __( 'Generate API key', 'woocommerce' ), 'primary', 'update_api_key' ); } else { ?> <p class="submit"> <?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'update_api_key', false ); ?> <a style="color: #a00; text-decoration: none; margin-left: 10px;" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key_id ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ), 'revoke' ) ); ?>"><?php esc_html_e( 'Revoke key', 'woocommerce' ); ?></a> </p> <?php } ?> </div> <script type="text/template" id="tmpl-api-keys-template"> <p id="copy-error"></p> <table class="form-table"> <tbody> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Consumer key', 'woocommerce' ); ?> </th> <td class="forminp"> <input id="key_consumer_key" type="text" value="{{ data.consumer_key }}" size="55" readonly="readonly"> <button type="button" class="button-secondary copy-key" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy', 'woocommerce' ); ?></button> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Consumer secret', 'woocommerce' ); ?> </th> <td class="forminp"> <input id="key_consumer_secret" type="text" value="{{ data.consumer_secret }}" size="55" readonly="readonly"> <button type="button" class="button-secondary copy-secret" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy', 'woocommerce' ); ?></button> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'QRCode', 'woocommerce' ); ?> </th> <td class="forminp"> <div id="keys-qrcode"></div> </td> </tr> </tbody> </table> </script> includes/admin/settings/views/html-admin-page-shipping-classes.php 0000644 00000007000 15132754524 0021373 0 ustar 00 <?php /** * Shipping classes admin * * @package WooCommerce\Admin\Shipping */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <h2> <?php esc_html_e( 'Shipping classes', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Shipping classes can be used to group products of similar type and can be used by some Shipping Methods (such as "Flat rate shipping") to provide different rates to different classes of product.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?> </h2> <table class="wc-shipping-classes widefat"> <thead> <tr> <?php foreach ( $shipping_class_columns as $class => $heading ) : ?> <th class="<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $heading ); ?></th> <?php endforeach; ?> </tr> </thead> <tfoot> <tr> <td colspan="<?php echo absint( count( $shipping_class_columns ) ); ?>"> <button type="submit" name="save" class="button button-primary wc-shipping-class-save" value="<?php esc_attr_e( 'Save shipping classes', 'woocommerce' ); ?>" disabled><?php esc_html_e( 'Save shipping classes', 'woocommerce' ); ?></button> <a class="button button-secondary wc-shipping-class-add" href="#"><?php esc_html_e( 'Add shipping class', 'woocommerce' ); ?></a> </td> </tr> </tfoot> <tbody class="wc-shipping-class-rows"></tbody> </table> <script type="text/html" id="tmpl-wc-shipping-class-row-blank"> <tr> <td class="wc-shipping-classes-blank-state" colspan="<?php echo absint( count( $shipping_class_columns ) ); ?>"><p><?php esc_html_e( 'No shipping classes have been created.', 'woocommerce' ); ?></p></td> </tr> </script> <script type="text/html" id="tmpl-wc-shipping-class-row"> <tr data-id="{{ data.term_id }}"> <?php foreach ( $shipping_class_columns as $class => $heading ) { echo '<td class="' . esc_attr( $class ) . '">'; switch ( $class ) { case 'wc-shipping-class-name': ?> <div class="view"> {{ data.name }} <div class="row-actions"> <a class="wc-shipping-class-edit" href="#"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-class-delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a> </div> </div> <div class="edit"> <input type="text" name="name[{{ data.term_id }}]" data-attribute="name" value="{{ data.name }}" placeholder="<?php esc_attr_e( 'Shipping class name', 'woocommerce' ); ?>" /> <div class="row-actions"> <a class="wc-shipping-class-cancel-edit" href="#"><?php esc_html_e( 'Cancel changes', 'woocommerce' ); ?></a> </div> </div> <?php break; case 'wc-shipping-class-slug': ?> <div class="view">{{ data.slug }}</div> <div class="edit"><input type="text" name="slug[{{ data.term_id }}]" data-attribute="slug" value="{{ data.slug }}" placeholder="<?php esc_attr_e( 'Slug', 'woocommerce' ); ?>" /></div> <?php break; case 'wc-shipping-class-description': ?> <div class="view">{{ data.description }}</div> <div class="edit"><input type="text" name="description[{{ data.term_id }}]" data-attribute="description" value="{{ data.description }}" placeholder="<?php esc_attr_e( 'Description for your reference', 'woocommerce' ); ?>" /></div> <?php break; case 'wc-shipping-class-count': ?> <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&product_shipping_class=' ) ); ?>{{data.slug}}">{{ data.count }}</a> <?php break; default: do_action( 'woocommerce_shipping_classes_column_' . $class ); break; } echo '</td>'; } ?> </tr> </script> includes/admin/settings/views/settings-tax.php 0000644 00000010456 15132754524 0015620 0 ustar 00 <?php /** * Tax settings. * * @package WooCommerce\Admin\Settings. */ defined( 'ABSPATH' ) || exit; $settings = array( array( 'title' => __( 'Tax options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'tax_options', ), array( 'title' => __( 'Prices entered with tax', 'woocommerce' ), 'id' => 'woocommerce_prices_include_tax', 'default' => 'no', 'type' => 'radio', 'desc_tip' => __( 'This option is important as it will affect how you input prices. Changing it will not update existing products.', 'woocommerce' ), 'options' => array( 'yes' => __( 'Yes, I will enter prices inclusive of tax', 'woocommerce' ), 'no' => __( 'No, I will enter prices exclusive of tax', 'woocommerce' ), ), ), array( 'title' => __( 'Calculate tax based on', 'woocommerce' ), 'id' => 'woocommerce_tax_based_on', 'desc_tip' => __( 'This option determines which address is used to calculate tax.', 'woocommerce' ), 'default' => 'shipping', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( 'shipping' => __( 'Customer shipping address', 'woocommerce' ), 'billing' => __( 'Customer billing address', 'woocommerce' ), 'base' => __( 'Shop base address', 'woocommerce' ), ), ), 'shipping-tax-class' => array( 'title' => __( 'Shipping tax class', 'woocommerce' ), 'desc' => __( 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', 'woocommerce' ), 'id' => 'woocommerce_shipping_tax_class', 'css' => 'min-width:150px;', 'default' => 'inherit', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( 'inherit' => __( 'Shipping tax class based on cart items', 'woocommerce' ) ) + wc_get_product_tax_class_options(), 'desc_tip' => true, ), array( 'title' => __( 'Rounding', 'woocommerce' ), 'desc' => __( 'Round tax at subtotal level, instead of rounding per line', 'woocommerce' ), 'id' => 'woocommerce_tax_round_at_subtotal', 'default' => 'no', 'type' => 'checkbox', ), array( 'title' => __( 'Additional tax classes', 'woocommerce' ), 'desc_tip' => __( 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', 'woocommerce' ), 'id' => 'woocommerce_tax_classes', 'css' => 'height: 65px;', 'type' => 'textarea', 'default' => '', 'is_option' => false, 'value' => implode( "\n", WC_Tax::get_tax_classes() ), ), array( 'title' => __( 'Display prices in the shop', 'woocommerce' ), 'id' => 'woocommerce_tax_display_shop', 'default' => 'excl', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( 'incl' => __( 'Including tax', 'woocommerce' ), 'excl' => __( 'Excluding tax', 'woocommerce' ), ), ), array( 'title' => __( 'Display prices during cart and checkout', 'woocommerce' ), 'id' => 'woocommerce_tax_display_cart', 'default' => 'excl', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( 'incl' => __( 'Including tax', 'woocommerce' ), 'excl' => __( 'Excluding tax', 'woocommerce' ), ), ), array( 'title' => __( 'Price display suffix', 'woocommerce' ), 'id' => 'woocommerce_price_display_suffix', 'default' => '', 'placeholder' => __( 'N/A', 'woocommerce' ), 'type' => 'text', 'desc_tip' => __( 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', 'woocommerce' ), ), array( 'title' => __( 'Display tax totals', 'woocommerce' ), 'id' => 'woocommerce_tax_total_display', 'default' => 'itemized', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( 'single' => __( 'As a single total', 'woocommerce' ), 'itemized' => __( 'Itemized', 'woocommerce' ), ), 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'tax_options', ), ); if ( ! wc_shipping_enabled() ) { unset( $settings['shipping-tax-class'] ); } return apply_filters( 'woocommerce_tax_settings', $settings ); includes/admin/settings/views/html-settings-tax.php 0000644 00000017173 15132754524 0016565 0 ustar 00 <?php /** * Admin view: Settings tax * * @package WooCommerce\Admin\Settings */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wc-tax-rates-search" id="rates-search"> <input type="search" class="wc-tax-rates-search-field" placeholder="<?php esc_attr_e( 'Search…', 'woocommerce' ); ?>" value="<?php echo isset( $_GET['s'] ) ? esc_attr( $_GET['s'] ) : ''; ?>" /> </div> <div id="rates-pagination"></div> <h3> <?php /* translators: %s: tax rate */ printf( __( '"%s" tax rates', 'woocommerce' ), $current_class ? esc_html( $current_class ) : __( 'Standard', 'woocommerce' ) ); ?> </h3> <table class="wc_tax_rates wc_input_table widefat"> <thead> <tr> <th width="8%"><a href="https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes" target="_blank"><?php _e( 'Country code', 'woocommerce' ); ?></a> <?php echo wc_help_tip( __( 'A 2 digit country code, e.g. US. Leave blank to apply to all.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'State code', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'A 2 digit state code, e.g. AL. Leave blank to apply to all.', 'woocommerce' ) ); ?></th> <th><?php _e( 'Postcode / ZIP', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Postcode for this rule. Semi-colon (;) separate multiple values. Leave blank to apply to all areas. Wildcards (*) and ranges for numeric postcodes (e.g. 12345...12350) can also be used.', 'woocommerce' ) ); ?></th> <th><?php _e( 'City', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Cities for this rule. Semi-colon (;) separate multiple values. Leave blank to apply to all cities.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'Rate %', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Enter a tax rate (percentage) to 4 decimal places.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'Tax name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Enter a name for this tax rate.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'Priority', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Choose a priority for this tax rate. Only 1 matching rate per priority will be used. To define multiple tax rates for a single area you need to specify a different priority per rate.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'Compound', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Choose whether or not this is a compound rate. Compound tax rates are applied on top of other tax rates.', 'woocommerce' ) ); ?></th> <th width="8%"><?php _e( 'Shipping', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Choose whether or not this tax rate also gets applied to shipping.', 'woocommerce' ) ); ?></th> </tr> </thead> <tfoot> <tr> <th colspan="9"> <a href="#" class="button plus insert"><?php _e( 'Insert row', 'woocommerce' ); ?></a> <a href="#" class="button minus remove_tax_rates"><?php _e( 'Remove selected row(s)', 'woocommerce' ); ?></a> <a href="#" download="tax_rates.csv" class="button export"><?php _e( 'Export CSV', 'woocommerce' ); ?></a> <a href="<?php echo admin_url( 'admin.php?import=woocommerce_tax_rate_csv' ); ?>" class="button import"><?php _e( 'Import CSV', 'woocommerce' ); ?></a> </th> </tr> </tfoot> <tbody id="rates"> <tr> <th colspan="9" style="text-align: center;"><?php esc_html_e( 'Loading…', 'woocommerce' ); ?></th> </tr> </tbody> </table> <script type="text/html" id="tmpl-wc-tax-table-row"> <tr class="tips" data-tip="<?php printf( esc_attr__( 'Tax rate ID: %s', 'woocommerce' ), '{{ data.tax_rate_id }}' ); ?>" data-id="{{ data.tax_rate_id }}"> <td class="country"> <input type="text" value="{{ data.tax_rate_country }}" placeholder="*" name="tax_rate_country[{{ data.tax_rate_id }}]" class="wc_input_country_iso" data-attribute="tax_rate_country" style="text-transform:uppercase" /> </td> <td class="state"> <input type="text" value="{{ data.tax_rate_state }}" placeholder="*" name="tax_rate_state[{{ data.tax_rate_id }}]" data-attribute="tax_rate_state" /> </td> <td class="postcode"> <input type="text" value="<# if ( data.postcode ) print( _.escape( data.postcode.join( '; ' ) ) ); #>" placeholder="*" data-name="tax_rate_postcode[{{ data.tax_rate_id }}]" data-attribute="postcode" /> </td> <td class="city"> <input type="text" value="<# if ( data.city ) print( _.escape( data.city.join( '; ' ) ) ); #>" placeholder="*" data-name="tax_rate_city[{{ data.tax_rate_id }}]" data-attribute="city" /> </td> <td class="rate"> <input type="text" value="{{ data.tax_rate }}" placeholder="0" name="tax_rate[{{ data.tax_rate_id }}]" data-attribute="tax_rate" /> </td> <td class="name"> <input type="text" value="{{ data.tax_rate_name }}" name="tax_rate_name[{{ data.tax_rate_id }}]" data-attribute="tax_rate_name" /> </td> <td class="priority"> <input type="number" step="1" min="1" value="{{ data.tax_rate_priority }}" name="tax_rate_priority[{{ data.tax_rate_id }}]" data-attribute="tax_rate_priority" /> </td> <td class="compound"> <input type="checkbox" class="checkbox" name="tax_rate_compound[{{ data.tax_rate_id }}]" <# if ( parseInt( data.tax_rate_compound, 10 ) ) { #> checked="checked" <# } #> data-attribute="tax_rate_compound" /> </td> <td class="apply_to_shipping"> <input type="checkbox" class="checkbox" name="tax_rate_shipping[{{ data.tax_rate_id }}]" <# if ( parseInt( data.tax_rate_shipping, 10 ) ) { #> checked="checked" <# } #> data-attribute="tax_rate_shipping" /> </td> </tr> </script> <script type="text/html" id="tmpl-wc-tax-table-row-empty"> <tr> <th colspan="9" style="text-align:center"><?php esc_html_e( 'No matching tax rates found.', 'woocommerce' ); ?></th> </tr> </script> <script type="text/html" id="tmpl-wc-tax-table-pagination"> <div class="tablenav"> <div class="tablenav-pages"> <span class="displaying-num"> <?php /* translators: %s: number */ printf( __( '%s items', 'woocommerce' ), // %s will be a number eventually, but must be a string for now. '{{ data.qty_rates }}' ); ?> </span> <span class="pagination-links"> <a class="tablenav-pages-navspan" data-goto="1"> <span class="screen-reader-text"><?php esc_html_e( 'First page', 'woocommerce' ); ?></span> <span aria-hidden="true">«</span> </a> <a class="tablenav-pages-navspan" data-goto="<# print( Math.max( 1, parseInt( data.current_page, 10 ) - 1 ) ) #>"> <span class="screen-reader-text"><?php esc_html_e( 'Previous page', 'woocommerce' ); ?></span> <span aria-hidden="true">‹</span> </a> <span class="paging-input"> <label for="current-page-selector" class="screen-reader-text"><?php esc_html_e( 'Current page', 'woocommerce' ); ?></label> <?php /* translators: 1: current page 2: total pages */ printf( esc_html_x( '%1$s of %2$s', 'Pagination', 'woocommerce' ), '<input class="current-page" id="current-page-selector" type="text" name="paged" value="{{ data.current_page }}" size="<# print( data.qty_pages.toString().length ) #>" aria-describedby="table-paging">', '<span class="total-pages">{{ data.qty_pages }}</span>' ); ?> </span> <a class="tablenav-pages-navspan" data-goto="<# print( Math.min( data.qty_pages, parseInt( data.current_page, 10 ) + 1 ) ) #>"> <span class="screen-reader-text"><?php esc_html_e( 'Next page', 'woocommerce' ); ?></span> <span aria-hidden="true">›</span> </a> <a class="tablenav-pages-navspan" data-goto="{{ data.qty_pages }}"> <span class="screen-reader-text"><?php esc_html_e( 'Last page', 'woocommerce' ); ?></span> <span aria-hidden="true">»</span> </a> </span> </div> </div> </script> includes/admin/settings/views/html-webhooks-edit.php 0000644 00000022546 15132754524 0016677 0 ustar 00 <?php /** * Admin View: Edit Webhooks * * @package WooCommerce\Admin\Webhooks\Views */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <input type="hidden" name="webhook_id" value="<?php echo esc_attr( $webhook->get_id() ); ?>" /> <div id="webhook-options" class="settings-panel"> <h2><?php esc_html_e( 'Webhook data', 'woocommerce' ); ?></h2> <table class="form-table"> <tbody> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_name"> <?php esc_html_e( 'Name', 'woocommerce' ); ?> <?php /* translators: %s: date */ echo wc_help_tip( sprintf( __( 'Friendly name for identifying this webhook, defaults to Webhook created on %s.', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ) ); // @codingStandardsIgnoreLine ?> </label> </th> <td class="forminp"> <input name="webhook_name" id="webhook_name" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_name() ); ?>" /> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_status"> <?php esc_html_e( 'Status', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'The options are "Active" (delivers payload), "Paused" (does not deliver), or "Disabled" (does not deliver due delivery failures).', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <select name="webhook_status" id="webhook_status" class="wc-enhanced-select"> <?php $statuses = wc_get_webhook_statuses(); $current_status = $webhook->get_status(); foreach ( $statuses as $status_slug => $status_name ) : ?> <option value="<?php echo esc_attr( $status_slug ); ?>" <?php selected( $current_status, $status_slug, true ); ?>><?php echo esc_html( $status_name ); ?></option> <?php endforeach; ?> </select> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_topic"> <?php esc_html_e( 'Topic', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Select when the webhook will fire.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <select name="webhook_topic" id="webhook_topic" class="wc-enhanced-select"> <?php $topic_data = WC_Admin_Webhooks::get_topic_data( $webhook ); $topics = apply_filters( 'woocommerce_webhook_topics', array( '' => __( 'Select an option…', 'woocommerce' ), 'coupon.created' => __( 'Coupon created', 'woocommerce' ), 'coupon.updated' => __( 'Coupon updated', 'woocommerce' ), 'coupon.deleted' => __( 'Coupon deleted', 'woocommerce' ), 'coupon.restored' => __( 'Coupon restored', 'woocommerce' ), 'customer.created' => __( 'Customer created', 'woocommerce' ), 'customer.updated' => __( 'Customer updated', 'woocommerce' ), 'customer.deleted' => __( 'Customer deleted', 'woocommerce' ), 'order.created' => __( 'Order created', 'woocommerce' ), 'order.updated' => __( 'Order updated', 'woocommerce' ), 'order.deleted' => __( 'Order deleted', 'woocommerce' ), 'order.restored' => __( 'Order restored', 'woocommerce' ), 'product.created' => __( 'Product created', 'woocommerce' ), 'product.updated' => __( 'Product updated', 'woocommerce' ), 'product.deleted' => __( 'Product deleted', 'woocommerce' ), 'product.restored' => __( 'Product restored', 'woocommerce' ), 'action' => __( 'Action', 'woocommerce' ), ) ); foreach ( $topics as $topic_slug => $topic_name ) : $selected = $topic_slug === $topic_data['topic'] || $topic_slug === $topic_data['resource'] . '.' . $topic_data['event']; ?> <option value="<?php echo esc_attr( $topic_slug ); ?>" <?php selected( $selected, true, true ); ?>><?php echo esc_html( $topic_name ); ?></option> <?php endforeach; ?> </select> </td> </tr> <tr valign="top" id="webhook-action-event-wrap"> <th scope="row" class="titledesc"> <label for="webhook_action_event"> <?php esc_html_e( 'Action event', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Enter the action that will trigger this webhook.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <input name="webhook_action_event" id="webhook_action_event" type="text" class="input-text regular-input" value="<?php echo esc_attr( $topic_data['event'] ); ?>" /> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_delivery_url"> <?php esc_html_e( 'Delivery URL', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'URL where the webhook payload is delivered.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <input name="webhook_delivery_url" id="webhook_delivery_url" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_delivery_url() ); ?>" /> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_secret"> <?php esc_html_e( 'Secret', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'The secret key is used to generate a hash of the delivered webhook and provided in the request headers.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <input name="webhook_secret" id="webhook_secret" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_secret() ); ?>" /> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <label for="webhook_api_version"> <?php esc_html_e( 'API Version', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'REST API version used in the webhook deliveries.', 'woocommerce' ) ); ?> </label> </th> <td class="forminp"> <select name="webhook_api_version" id="webhook_api_version"> <?php foreach ( array_reverse( wc_get_webhook_rest_api_versions() ) as $version ) : ?> <option value="<?php echo esc_attr( $version ); ?>" <?php selected( $version, $webhook->get_api_version(), true ); ?>> <?php /* translators: %d: rest api version number */ echo esc_html( sprintf( __( 'WP REST API Integration v%d', 'woocommerce' ), str_replace( 'wp_api_v', '', $version ) ) ); ?> </option> <?php endforeach; ?> <option value="legacy_v3" <?php selected( 'legacy_v3', $webhook->get_api_version(), true ); ?>><?php esc_html_e( 'Legacy API v3 (deprecated)', 'woocommerce' ); ?></option> </select> </td> </tr> </tbody> </table> <?php do_action( 'woocommerce_webhook_options' ); ?> </div> <div id="webhook-actions" class="settings-panel"> <h2><?php esc_html_e( 'Webhook actions', 'woocommerce' ); ?></h2> <table class="form-table"> <tbody> <?php if ( $webhook->get_date_created() && '0000-00-00 00:00:00' !== $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) : ?> <?php if ( is_null( $webhook->get_date_modified() ) ) : ?> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Created at', 'woocommerce' ); ?> </th> <td class="forminp"> <?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) ) ); ?> </td> </tr> <?php else : ?> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Created at', 'woocommerce' ); ?> </th> <td class="forminp"> <?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) ) ); ?> </td> </tr> <tr valign="top"> <th scope="row" class="titledesc"> <?php esc_html_e( 'Updated at', 'woocommerce' ); ?> </th> <td class="forminp"> <?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ) ) ) ); ?> </td> </tr> <?php endif; ?> <?php endif; ?> <tr valign="top"> <td colspan="2" scope="row" style="padding-left: 0;"> <p class="submit"> <button type="submit" class="button button-primary button-large" name="save" id="publish" accesskey="p"><?php esc_html_e( 'Save webhook', 'woocommerce' ); ?></button> <?php if ( $webhook->get_id() ) : $delete_url = wp_nonce_url( add_query_arg( array( 'delete' => $webhook->get_id(), ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) ), 'delete-webhook' ); ?> <a style="color: #a00; text-decoration: none; margin-left: 10px;" href="<?php echo esc_url( $delete_url ); ?>"><?php esc_html_e( 'Delete permanently', 'woocommerce' ); ?></a> <?php endif; ?> </p> </td> </tr> </tbody> </table> </div> <script type="text/javascript"> jQuery( function ( $ ) { $( '#webhook-options' ).find( '#webhook_topic' ).on( 'change', function() { var current = $( this ).val(), action_event_field = $( '#webhook-options' ).find( '#webhook-action-event-wrap' ); action_event_field.hide(); if ( 'action' === current ) { action_event_field.show(); } }).trigger( 'change' ); }); </script> includes/admin/settings/views/class-wc-settings-rest-api.php 0000644 00000000377 15132754524 0020263 0 ustar 00 <?php // @codingStandardsIgnoreFile. /** * Settings class file. * * @deprecated 3.4.0 Replaced with class-wc-settings-advanced.php. * @todo remove in 4.0. */ defined( 'ABSPATH' ) || exit; return include __DIR__ . '/class-wc-settings-advanced.php'; includes/admin/settings/class-wc-settings-accounts.php 0000644 00000023525 15132754524 0017221 0 ustar 00 <?php /** * WooCommerce Account Settings. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Accounts', false ) ) { return new WC_Settings_Accounts(); } /** * WC_Settings_Accounts. */ class WC_Settings_Accounts extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'account'; $this->label = __( 'Accounts & Privacy', 'woocommerce' ); parent::__construct(); } /** * Get settings array. * * @return array */ protected function get_settings_for_default_section() { $erasure_text = esc_html__( 'account erasure request', 'woocommerce' ); $privacy_text = esc_html__( 'privacy page', 'woocommerce' ); if ( current_user_can( 'manage_privacy_options' ) ) { if ( version_compare( get_bloginfo( 'version' ), '5.3', '<' ) ) { $erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ), $erasure_text ); } else { $erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'erase-personal-data.php' ) ), $erasure_text ); } $privacy_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'options-privacy.php' ) ), $privacy_text ); } $account_settings = array( array( 'title' => '', 'type' => 'title', 'id' => 'account_registration_options', ), array( 'title' => __( 'Guest checkout', 'woocommerce' ), 'desc' => __( 'Allow customers to place orders without an account', 'woocommerce' ), 'id' => 'woocommerce_enable_guest_checkout', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'title' => __( 'Login', 'woocommerce' ), 'desc' => __( 'Allow customers to log into an existing account during checkout', 'woocommerce' ), 'id' => 'woocommerce_enable_checkout_login_reminder', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'autoload' => false, ), array( 'title' => __( 'Account creation', 'woocommerce' ), 'desc' => __( 'Allow customers to create an account during checkout', 'woocommerce' ), 'id' => 'woocommerce_enable_signup_and_login_from_checkout', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'desc' => __( 'Allow customers to create an account on the "My account" page', 'woocommerce' ), 'id' => 'woocommerce_enable_myaccount_registration', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => '', 'autoload' => false, ), array( 'desc' => __( 'When creating an account, automatically generate an account username for the customer based on their name, surname or email', 'woocommerce' ), 'id' => 'woocommerce_registration_generate_username', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => '', 'autoload' => false, ), array( 'desc' => __( 'When creating an account, automatically generate an account password', 'woocommerce' ), 'id' => 'woocommerce_registration_generate_password', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'autoload' => false, ), array( 'title' => __( 'Account erasure requests', 'woocommerce' ), 'desc' => __( 'Remove personal data from orders on request', 'woocommerce' ), /* Translators: %s URL to erasure request screen. */ 'desc_tip' => sprintf( esc_html__( 'When handling an %s, should personal data within orders be retained or removed?', 'woocommerce' ), $erasure_text ), 'id' => 'woocommerce_erasure_request_removes_order_data', 'type' => 'checkbox', 'default' => 'no', 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'desc' => __( 'Remove access to downloads on request', 'woocommerce' ), /* Translators: %s URL to erasure request screen. */ 'desc_tip' => sprintf( esc_html__( 'When handling an %s, should access to downloadable files be revoked and download logs cleared?', 'woocommerce' ), $erasure_text ), 'id' => 'woocommerce_erasure_request_removes_download_data', 'type' => 'checkbox', 'default' => 'no', 'checkboxgroup' => 'end', 'autoload' => false, ), array( 'title' => __( 'Personal data removal', 'woocommerce' ), 'desc' => __( 'Allow personal data to be removed in bulk from orders', 'woocommerce' ), 'desc_tip' => __( 'Adds an option to the orders screen for removing personal data in bulk. Note that removing personal data cannot be undone.', 'woocommerce' ), 'id' => 'woocommerce_allow_bulk_remove_personal_data', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'default' => 'no', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'account_registration_options', ), array( 'title' => __( 'Privacy policy', 'woocommerce' ), 'type' => 'title', 'id' => 'privacy_policy_options', /* translators: %s: privacy page link. */ 'desc' => sprintf( esc_html__( 'This section controls the display of your website privacy policy. The privacy notices below will not show up unless a %s is set.', 'woocommerce' ), $privacy_text ), ), array( 'title' => __( 'Registration privacy policy', 'woocommerce' ), 'desc_tip' => __( 'Optionally add some text about your store privacy policy to show on account registration forms.', 'woocommerce' ), 'id' => 'woocommerce_registration_privacy_policy_text', /* translators: %s privacy policy page name and link */ 'default' => sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), 'type' => 'textarea', 'css' => 'min-width: 50%; height: 75px;', ), array( 'title' => __( 'Checkout privacy policy', 'woocommerce' ), 'desc_tip' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ), 'id' => 'woocommerce_checkout_privacy_policy_text', /* translators: %s privacy policy page name and link */ 'default' => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), 'type' => 'textarea', 'css' => 'min-width: 50%; height: 75px;', ), array( 'type' => 'sectionend', 'id' => 'privacy_policy_options', ), array( 'title' => __( 'Personal data retention', 'woocommerce' ), 'desc' => __( 'Choose how long to retain personal data when it\'s no longer needed for processing. Leave the following options blank to retain this data indefinitely.', 'woocommerce' ), 'type' => 'title', 'id' => 'personal_data_retention', ), array( 'title' => __( 'Retain inactive accounts ', 'woocommerce' ), 'desc_tip' => __( 'Inactive accounts are those which have not logged in, or placed an order, for the specified duration. They will be deleted. Any orders will be converted into guest orders.', 'woocommerce' ), 'id' => 'woocommerce_delete_inactive_accounts', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => array( 'number' => '', 'unit' => 'months', ), 'autoload' => false, ), array( 'title' => __( 'Retain pending orders ', 'woocommerce' ), 'desc_tip' => __( 'Pending orders are unpaid and may have been abandoned by the customer. They will be trashed after the specified duration.', 'woocommerce' ), 'id' => 'woocommerce_trash_pending_orders', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => '', 'autoload' => false, ), array( 'title' => __( 'Retain failed orders', 'woocommerce' ), 'desc_tip' => __( 'Failed orders are unpaid and may have been abandoned by the customer. They will be trashed after the specified duration.', 'woocommerce' ), 'id' => 'woocommerce_trash_failed_orders', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => '', 'autoload' => false, ), array( 'title' => __( 'Retain cancelled orders', 'woocommerce' ), 'desc_tip' => __( 'Cancelled orders are unpaid and may have been cancelled by the store owner or customer. They will be trashed after the specified duration.', 'woocommerce' ), 'id' => 'woocommerce_trash_cancelled_orders', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => '', 'autoload' => false, ), array( 'title' => __( 'Retain completed orders', 'woocommerce' ), 'desc_tip' => __( 'Retain completed orders for a specified duration before anonymizing the personal data within them.', 'woocommerce' ), 'id' => 'woocommerce_anonymize_completed_orders', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), 'default' => array( 'number' => '', 'unit' => 'months', ), 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'personal_data_retention', ), ); return apply_filters( 'woocommerce_' . $this->id . '_settings', $account_settings ); } } return new WC_Settings_Accounts(); includes/admin/settings/class-wc-settings-general.php 0000644 00000024547 15132754524 0017024 0 ustar 00 <?php /** * WooCommerce General Settings * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_General', false ) ) { return new WC_Settings_General(); } /** * WC_Admin_Settings_General. */ class WC_Settings_General extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'general'; $this->label = __( 'General', 'woocommerce' ); parent::__construct(); } /** * Get settings or the default section. * * @return array */ protected function get_settings_for_default_section() { $currency_code_options = get_woocommerce_currencies(); foreach ( $currency_code_options as $code => $name ) { $currency_code_options[ $code ] = $name . ' (' . get_woocommerce_currency_symbol( $code ) . ')'; } $settings = array( array( 'title' => __( 'Store Address', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'This is where your business is located. Tax rates and shipping rates will use this address.', 'woocommerce' ), 'id' => 'store_address', ), array( 'title' => __( 'Address line 1', 'woocommerce' ), 'desc' => __( 'The street address for your business location.', 'woocommerce' ), 'id' => 'woocommerce_store_address', 'default' => '', 'type' => 'text', 'desc_tip' => true, ), array( 'title' => __( 'Address line 2', 'woocommerce' ), 'desc' => __( 'An additional, optional address line for your business location.', 'woocommerce' ), 'id' => 'woocommerce_store_address_2', 'default' => '', 'type' => 'text', 'desc_tip' => true, ), array( 'title' => __( 'City', 'woocommerce' ), 'desc' => __( 'The city in which your business is located.', 'woocommerce' ), 'id' => 'woocommerce_store_city', 'default' => '', 'type' => 'text', 'desc_tip' => true, ), array( 'title' => __( 'Country / State', 'woocommerce' ), 'desc' => __( 'The country and state or province, if any, in which your business is located.', 'woocommerce' ), 'id' => 'woocommerce_default_country', 'default' => 'US:CA', 'type' => 'single_select_country', 'desc_tip' => true, ), array( 'title' => __( 'Postcode / ZIP', 'woocommerce' ), 'desc' => __( 'The postal code, if any, in which your business is located.', 'woocommerce' ), 'id' => 'woocommerce_store_postcode', 'css' => 'min-width:50px;', 'default' => '', 'type' => 'text', 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'store_address', ), array( 'title' => __( 'General options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'general_options', ), array( 'title' => __( 'Selling location(s)', 'woocommerce' ), 'desc' => __( 'This option lets you limit which countries you are willing to sell to.', 'woocommerce' ), 'id' => 'woocommerce_allowed_countries', 'default' => 'all', 'type' => 'select', 'class' => 'wc-enhanced-select', 'css' => 'min-width: 350px;', 'desc_tip' => true, 'options' => array( 'all' => __( 'Sell to all countries', 'woocommerce' ), 'all_except' => __( 'Sell to all countries, except for…', 'woocommerce' ), 'specific' => __( 'Sell to specific countries', 'woocommerce' ), ), ), array( 'title' => __( 'Sell to all countries, except for…', 'woocommerce' ), 'desc' => '', 'id' => 'woocommerce_all_except_countries', 'css' => 'min-width: 350px;', 'default' => '', 'type' => 'multi_select_countries', ), array( 'title' => __( 'Sell to specific countries', 'woocommerce' ), 'desc' => '', 'id' => 'woocommerce_specific_allowed_countries', 'css' => 'min-width: 350px;', 'default' => '', 'type' => 'multi_select_countries', ), array( 'title' => __( 'Shipping location(s)', 'woocommerce' ), 'desc' => __( 'Choose which countries you want to ship to, or choose to ship to all locations you sell to.', 'woocommerce' ), 'id' => 'woocommerce_ship_to_countries', 'default' => '', 'type' => 'select', 'class' => 'wc-enhanced-select', 'desc_tip' => true, 'options' => array( '' => __( 'Ship to all countries you sell to', 'woocommerce' ), 'all' => __( 'Ship to all countries', 'woocommerce' ), 'specific' => __( 'Ship to specific countries only', 'woocommerce' ), 'disabled' => __( 'Disable shipping & shipping calculations', 'woocommerce' ), ), ), array( 'title' => __( 'Ship to specific countries', 'woocommerce' ), 'desc' => '', 'id' => 'woocommerce_specific_ship_to_countries', 'css' => '', 'default' => '', 'type' => 'multi_select_countries', ), array( 'title' => __( 'Default customer location', 'woocommerce' ), 'id' => 'woocommerce_default_customer_address', 'desc_tip' => __( 'This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.', 'woocommerce' ), 'default' => 'base', 'type' => 'select', 'class' => 'wc-enhanced-select', 'options' => array( '' => __( 'No location by default', 'woocommerce' ), 'base' => __( 'Shop base address', 'woocommerce' ), 'geolocation' => __( 'Geolocate', 'woocommerce' ), 'geolocation_ajax' => __( 'Geolocate (with page caching support)', 'woocommerce' ), ), ), array( 'title' => __( 'Enable taxes', 'woocommerce' ), 'desc' => __( 'Enable tax rates and calculations', 'woocommerce' ), 'id' => 'woocommerce_calc_taxes', 'default' => 'no', 'type' => 'checkbox', 'desc_tip' => __( 'Rates will be configurable and taxes will be calculated during checkout.', 'woocommerce' ), ), array( 'title' => __( 'Enable coupons', 'woocommerce' ), 'desc' => __( 'Enable the use of coupon codes', 'woocommerce' ), 'id' => 'woocommerce_enable_coupons', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'show_if_checked' => 'option', 'desc_tip' => __( 'Coupons can be applied from the cart and checkout pages.', 'woocommerce' ), ), array( 'desc' => __( 'Calculate coupon discounts sequentially', 'woocommerce' ), 'id' => 'woocommerce_calc_discounts_sequentially', 'default' => 'no', 'type' => 'checkbox', 'desc_tip' => __( 'When applying multiple coupons, apply the first coupon to the full price and the second coupon to the discounted price and so on.', 'woocommerce' ), 'show_if_checked' => 'yes', 'checkboxgroup' => 'end', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'general_options', ), array( 'title' => __( 'Currency options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ), 'id' => 'pricing_options', ), array( 'title' => __( 'Currency', 'woocommerce' ), 'desc' => __( 'This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ), 'id' => 'woocommerce_currency', 'default' => 'USD', 'type' => 'select', 'class' => 'wc-enhanced-select', 'desc_tip' => true, 'options' => $currency_code_options, ), array( 'title' => __( 'Currency position', 'woocommerce' ), 'desc' => __( 'This controls the position of the currency symbol.', 'woocommerce' ), 'id' => 'woocommerce_currency_pos', 'class' => 'wc-enhanced-select', 'default' => 'left', 'type' => 'select', 'options' => array( 'left' => __( 'Left', 'woocommerce' ), 'right' => __( 'Right', 'woocommerce' ), 'left_space' => __( 'Left with space', 'woocommerce' ), 'right_space' => __( 'Right with space', 'woocommerce' ), ), 'desc_tip' => true, ), array( 'title' => __( 'Thousand separator', 'woocommerce' ), 'desc' => __( 'This sets the thousand separator of displayed prices.', 'woocommerce' ), 'id' => 'woocommerce_price_thousand_sep', 'css' => 'width:50px;', 'default' => ',', 'type' => 'text', 'desc_tip' => true, ), array( 'title' => __( 'Decimal separator', 'woocommerce' ), 'desc' => __( 'This sets the decimal separator of displayed prices.', 'woocommerce' ), 'id' => 'woocommerce_price_decimal_sep', 'css' => 'width:50px;', 'default' => '.', 'type' => 'text', 'desc_tip' => true, ), array( 'title' => __( 'Number of decimals', 'woocommerce' ), 'desc' => __( 'This sets the number of decimal points shown in displayed prices.', 'woocommerce' ), 'id' => 'woocommerce_price_num_decimals', 'css' => 'width:50px;', 'default' => '2', 'desc_tip' => true, 'type' => 'number', 'custom_attributes' => array( 'min' => 0, 'step' => 1, ), ), array( 'type' => 'sectionend', 'id' => 'pricing_options', ), ); return apply_filters( 'woocommerce_general_settings', $settings ); } /** * Output a color picker input box. * * @param mixed $name Name of input. * @param string $id ID of input. * @param mixed $value Value of input. * @param string $desc (default: '') Description for input. */ public function color_picker( $name, $id, $value, $desc = '' ) { echo '<div class="color_box">' . wc_help_tip( $desc ) . ' <input name="' . esc_attr( $id ) . '" id="' . esc_attr( $id ) . '" type="text" value="' . esc_attr( $value ) . '" class="colorpick" /> <div id="colorPickerDiv_' . esc_attr( $id ) . '" class="colorpickdiv"></div> </div>'; } } return new WC_Settings_General(); includes/admin/settings/class-wc-settings-checkout.php 0000644 00000000417 15132754524 0017202 0 ustar 00 <?php // @codingStandardsIgnoreFile. /** * Settings class file. * * @deprecated 3.4.0 Replaced with class-wc-settings-payment-gateways.php. * @todo remove in 4.0. */ defined( 'ABSPATH' ) || exit; return include __DIR__ . '/class-wc-settings-payment-gateways.php'; includes/admin/settings/class-wc-settings-payment-gateways.php 0000644 00000021056 15132754524 0020676 0 ustar 00 <?php // @codingStandardsIgnoreLine. /** * WooCommerce Checkout Settings * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Payment_Gateways', false ) ) { return new WC_Settings_Payment_Gateways(); } /** * WC_Settings_Payment_Gateways. */ class WC_Settings_Payment_Gateways extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'checkout'; // @todo In future versions this may make more sense as 'payment' however to avoid breakage lets leave this alone until we refactor settings APIs in general. $this->label = _x( 'Payments', 'Settings tab label', 'woocommerce' ); add_action( 'woocommerce_admin_field_payment_gateways', array( $this, 'payment_gateways_setting' ) ); parent::__construct(); } /** * Get own sections. * * @return array */ protected function get_own_sections() { return array( '' => __( 'Payment methods', 'woocommerce' ), ); } /** * Get settings array. * * @return array */ protected function get_settings_for_default_section() { $settings = array( array( 'title' => __( 'Payment methods', 'woocommerce' ), 'desc' => __( 'Installed payment methods are listed below and can be sorted to control their display order on the frontend.', 'woocommerce' ), 'type' => 'title', 'id' => 'payment_gateways_options', ), array( 'type' => 'payment_gateways', ), array( 'type' => 'sectionend', 'id' => 'payment_gateways_options', ), ); return apply_filters( 'woocommerce_payment_gateways_settings', $settings ); } /** * Output the settings. */ public function output() { //phpcs:disable WordPress.Security.NonceVerification.Recommended global $current_section; // Load gateways so we can show any global options they may have. $payment_gateways = WC()->payment_gateways->payment_gateways(); if ( $current_section ) { foreach ( $payment_gateways as $gateway ) { if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) { if ( isset( $_GET['toggle_enabled'] ) ) { $enabled = $gateway->get_option( 'enabled' ); if ( $enabled ) { $gateway->settings['enabled'] = wc_string_to_bool( $enabled ) ? 'no' : 'yes'; } } $this->run_gateway_admin_options( $gateway ); break; } } } parent::output(); //phpcs:enable } /** * Run the 'admin_options' method on a given gateway. * This method exists to easy unit testing. * * @param object $gateway The gateway object to run the method on. */ protected function run_gateway_admin_options( $gateway ) { $gateway->admin_options(); } /** * Output payment gateway settings. */ public function payment_gateways_setting() { ?> <tr valign="top"> <td class="wc_payment_gateways_wrapper" colspan="2"> <table class="wc_gateways widefat" cellspacing="0" aria-describedby="payment_gateways_options-description"> <thead> <tr> <?php $default_columns = array( 'sort' => '', 'name' => __( 'Method', 'woocommerce' ), 'status' => __( 'Enabled', 'woocommerce' ), 'description' => __( 'Description', 'woocommerce' ), 'action' => '', ); $columns = apply_filters( 'woocommerce_payment_gateways_setting_columns', $default_columns ); foreach ( $columns as $key => $column ) { echo '<th class="' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>'; } ?> </tr> </thead> <tbody> <?php foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) { echo '<tr data-gateway_id="' . esc_attr( $gateway->id ) . '">'; foreach ( $columns as $key => $column ) { if ( ! array_key_exists( $key, $default_columns ) ) { do_action( 'woocommerce_payment_gateways_setting_column_' . $key, $gateway ); continue; } $width = ''; if ( in_array( $key, array( 'sort', 'status', 'action' ), true ) ) { $width = '1%'; } $method_title = $gateway->get_method_title() ? $gateway->get_method_title() : $gateway->get_title(); $custom_title = $gateway->get_title(); echo '<td class="' . esc_attr( $key ) . '" width="' . esc_attr( $width ) . '">'; switch ( $key ) { case 'sort': ?> <div class="wc-item-reorder-nav"> <button type="button" class="wc-move-up" tabindex="0" aria-hidden="false" aria-label="<?php /* Translators: %s Payment gateway name. */ echo esc_attr( sprintf( __( 'Move the "%s" payment method up', 'woocommerce' ), $method_title ) ); ?>"><?php esc_html_e( 'Move up', 'woocommerce' ); ?></button> <button type="button" class="wc-move-down" tabindex="0" aria-hidden="false" aria-label="<?php /* Translators: %s Payment gateway name. */ echo esc_attr( sprintf( __( 'Move the "%s" payment method down', 'woocommerce' ), $method_title ) ); ?>"><?php esc_html_e( 'Move down', 'woocommerce' ); ?></button> <input type="hidden" name="gateway_order[]" value="<?php echo esc_attr( $gateway->id ); ?>" /> </div> <?php break; case 'name': echo '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . strtolower( $gateway->id ) ) ) . '" class="wc-payment-gateway-method-title">' . wp_kses_post( $method_title ) . '</a>'; if ( $method_title !== $custom_title ) { echo '<span class="wc-payment-gateway-method-name"> – ' . wp_kses_post( $custom_title ) . '</span>'; } break; case 'description': echo wp_kses_post( $gateway->get_method_description() ); break; case 'action': if ( wc_string_to_bool( $gateway->enabled ) ) { /* Translators: %s Payment gateway name. */ echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Manage the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . strtolower( $gateway->id ) ) ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a>'; } else { /* Translators: %s Payment gateway name. */ echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . strtolower( $gateway->id ) ) ) . '">' . esc_html__( 'Set up', 'woocommerce' ) . '</a>'; } break; case 'status': echo '<a class="wc-payment-gateway-method-toggle-enabled" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . strtolower( $gateway->id ) ) ) . '">'; if ( wc_string_to_bool( $gateway->enabled ) ) { /* Translators: %s Payment gateway name. */ echo '<span class="woocommerce-input-toggle woocommerce-input-toggle--enabled" aria-label="' . esc_attr( sprintf( __( 'The "%s" payment method is currently enabled', 'woocommerce' ), $method_title ) ) . '">' . esc_attr__( 'Yes', 'woocommerce' ) . '</span>'; } else { /* Translators: %s Payment gateway name. */ echo '<span class="woocommerce-input-toggle woocommerce-input-toggle--disabled" aria-label="' . esc_attr( sprintf( __( 'The "%s" payment method is currently disabled', 'woocommerce' ), $method_title ) ) . '">' . esc_attr__( 'No', 'woocommerce' ) . '</span>'; } echo '</a>'; break; } echo '</td>'; } echo '</tr>'; } ?> </tbody> </table> </td> </tr> <?php } /** * Save settings. */ public function save() { global $current_section; $wc_payment_gateways = WC_Payment_Gateways::instance(); $this->save_settings_for_current_section(); if ( ! $current_section ) { // If section is empty, we're on the main settings page. This makes sure 'gateway ordering' is saved. $wc_payment_gateways->process_admin_options(); $wc_payment_gateways->init(); } else { // There is a section - this may be a gateway or custom section. foreach ( $wc_payment_gateways->payment_gateways() as $gateway ) { if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) { do_action( 'woocommerce_update_options_payment_gateways_' . $gateway->id ); $wc_payment_gateways->init(); } } $this->do_update_options_action(); } } } return new WC_Settings_Payment_Gateways(); includes/admin/settings/class-wc-settings-advanced.php 0000644 00000037533 15132754524 0017153 0 ustar 00 <?php /** * WooCommerce advanced settings * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; /** * Settings for API. */ if ( class_exists( 'WC_Settings_Advanced', false ) ) { return new WC_Settings_Advanced(); } /** * WC_Settings_Advanced. */ class WC_Settings_Advanced extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'advanced'; $this->label = __( 'Advanced', 'woocommerce' ); parent::__construct(); $this->notices(); } /** * Get own sections. * * @return array */ protected function get_own_sections() { return array( '' => __( 'Page setup', 'woocommerce' ), 'keys' => __( 'REST API', 'woocommerce' ), 'webhooks' => __( 'Webhooks', 'woocommerce' ), 'legacy_api' => __( 'Legacy API', 'woocommerce' ), 'woocommerce_com' => __( 'WooCommerce.com', 'woocommerce' ), ); } /** * Get settings for the default section. * * @return array */ protected function get_settings_for_default_section() { $settings = array( array( 'title' => __( 'Page setup', 'woocommerce' ), 'desc' => __( 'These pages need to be set so that WooCommerce knows where to send users to checkout.', 'woocommerce' ), 'type' => 'title', 'id' => 'advanced_page_options', ), array( 'title' => __( 'Cart page', 'woocommerce' ), /* Translators: %s Page contents. */ 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) ), 'id' => 'woocommerce_cart_page_id', 'type' => 'single_select_page_with_search', 'default' => '', 'class' => 'wc-page-search', 'css' => 'min-width:300px;', 'args' => array( 'exclude' => array( wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ), ), ), 'desc_tip' => true, 'autoload' => false, ), array( 'title' => __( 'Checkout page', 'woocommerce' ), /* Translators: %s Page contents. */ 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) ), 'id' => 'woocommerce_checkout_page_id', 'type' => 'single_select_page_with_search', 'default' => wc_get_page_id( 'checkout' ), 'class' => 'wc-page-search', 'css' => 'min-width:300px;', 'args' => array( 'exclude' => array( wc_get_page_id( 'cart' ), wc_get_page_id( 'myaccount' ), ), ), 'desc_tip' => true, 'autoload' => false, ), array( 'title' => __( 'My account page', 'woocommerce' ), /* Translators: %s Page contents. */ 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) ), 'id' => 'woocommerce_myaccount_page_id', 'type' => 'single_select_page_with_search', 'default' => '', 'class' => 'wc-page-search', 'css' => 'min-width:300px;', 'args' => array( 'exclude' => array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), ), ), 'desc_tip' => true, 'autoload' => false, ), array( 'title' => __( 'Terms and conditions', 'woocommerce' ), 'desc' => __( 'If you define a "Terms" page the customer will be asked if they accept them when checking out.', 'woocommerce' ), 'id' => 'woocommerce_terms_page_id', 'default' => '', 'class' => 'wc-page-search', 'css' => 'min-width:300px;', 'type' => 'single_select_page_with_search', 'args' => array( 'exclude' => wc_get_page_id( 'checkout' ) ), 'desc_tip' => true, 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'advanced_page_options', ), array( 'title' => '', 'type' => 'title', 'id' => 'checkout_process_options', ), 'force_ssl_checkout' => array( 'title' => __( 'Secure checkout', 'woocommerce' ), 'desc' => __( 'Force secure checkout', 'woocommerce' ), 'id' => 'woocommerce_force_ssl_checkout', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'show_if_checked' => 'option', /* Translators: %s Docs URL. */ 'desc_tip' => sprintf( __( 'Force SSL (HTTPS) on the checkout pages (<a href="%s" target="_blank">an SSL Certificate is required</a>).', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/#section-3' ), ), 'unforce_ssl_checkout' => array( 'desc' => __( 'Force HTTP when leaving the checkout', 'woocommerce' ), 'id' => 'woocommerce_unforce_ssl_checkout', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', 'show_if_checked' => 'yes', ), array( 'type' => 'sectionend', 'id' => 'checkout_process_options', ), array( 'title' => __( 'Checkout endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions during the checkout process. They should be unique.', 'woocommerce' ), 'id' => 'checkout_endpoint_options', ), array( 'title' => __( 'Pay', 'woocommerce' ), 'desc' => __( 'Endpoint for the "Checkout → Pay" page.', 'woocommerce' ), 'id' => 'woocommerce_checkout_pay_endpoint', 'type' => 'text', 'default' => 'order-pay', 'desc_tip' => true, ), array( 'title' => __( 'Order received', 'woocommerce' ), 'desc' => __( 'Endpoint for the "Checkout → Order received" page.', 'woocommerce' ), 'id' => 'woocommerce_checkout_order_received_endpoint', 'type' => 'text', 'default' => 'order-received', 'desc_tip' => true, ), array( 'title' => __( 'Add payment method', 'woocommerce' ), 'desc' => __( 'Endpoint for the "Checkout → Add payment method" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_add_payment_method_endpoint', 'type' => 'text', 'default' => 'add-payment-method', 'desc_tip' => true, ), array( 'title' => __( 'Delete payment method', 'woocommerce' ), 'desc' => __( 'Endpoint for the delete payment method page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_delete_payment_method_endpoint', 'type' => 'text', 'default' => 'delete-payment-method', 'desc_tip' => true, ), array( 'title' => __( 'Set default payment method', 'woocommerce' ), 'desc' => __( 'Endpoint for the setting a default payment method page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_set_default_payment_method_endpoint', 'type' => 'text', 'default' => 'set-default-payment-method', 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'checkout_endpoint_options', ), array( 'title' => __( 'Account endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions on the accounts pages. They should be unique and can be left blank to disable the endpoint.', 'woocommerce' ), 'id' => 'account_endpoint_options', ), array( 'title' => __( 'Orders', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Orders" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_orders_endpoint', 'type' => 'text', 'default' => 'orders', 'desc_tip' => true, ), array( 'title' => __( 'View order', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → View order" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_view_order_endpoint', 'type' => 'text', 'default' => 'view-order', 'desc_tip' => true, ), array( 'title' => __( 'Downloads', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Downloads" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_downloads_endpoint', 'type' => 'text', 'default' => 'downloads', 'desc_tip' => true, ), array( 'title' => __( 'Edit account', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Edit account" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_edit_account_endpoint', 'type' => 'text', 'default' => 'edit-account', 'desc_tip' => true, ), array( 'title' => __( 'Addresses', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Addresses" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_edit_address_endpoint', 'type' => 'text', 'default' => 'edit-address', 'desc_tip' => true, ), array( 'title' => __( 'Payment methods', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Payment methods" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_payment_methods_endpoint', 'type' => 'text', 'default' => 'payment-methods', 'desc_tip' => true, ), array( 'title' => __( 'Lost password', 'woocommerce' ), 'desc' => __( 'Endpoint for the "My account → Lost password" page.', 'woocommerce' ), 'id' => 'woocommerce_myaccount_lost_password_endpoint', 'type' => 'text', 'default' => 'lost-password', 'desc_tip' => true, ), array( 'title' => __( 'Logout', 'woocommerce' ), 'desc' => __( 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', 'woocommerce' ), 'id' => 'woocommerce_logout_endpoint', 'type' => 'text', 'default' => 'customer-logout', 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'account_endpoint_options', ), ); $settings = apply_filters( 'woocommerce_settings_pages', $settings ); if ( wc_site_is_https() ) { unset( $settings['unforce_ssl_checkout'], $settings['force_ssl_checkout'] ); } return $settings; } /** * Get settings for the WooCommerce.com section. * * @return array */ protected function get_settings_for_woocommerce_com_section() { $tracking_info_text = sprintf( '<a href="%s" target="_blank">%s</a>', 'https://woocommerce.com/usage-tracking', esc_html__( 'WooCommerce.com Usage Tracking Documentation', 'woocommerce' ) ); $settings = array( array( 'title' => esc_html__( 'Usage Tracking', 'woocommerce' ), 'type' => 'title', 'id' => 'tracking_options', 'desc' => __( 'Gathering usage data allows us to make WooCommerce better — your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense.', 'woocommerce' ), ), array( 'title' => __( 'Enable tracking', 'woocommerce' ), 'desc' => __( 'Allow usage of WooCommerce to be tracked', 'woocommerce' ), /* Translators: %s URL to tracking info screen. */ 'desc_tip' => sprintf( esc_html__( 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: %s.', 'woocommerce' ), $tracking_info_text ), 'id' => 'woocommerce_allow_tracking', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'default' => 'no', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'tracking_options', ), array( 'title' => esc_html__( 'Marketplace suggestions', 'woocommerce' ), 'type' => 'title', 'id' => 'marketplace_suggestions', 'desc' => __( 'We show contextual suggestions for official extensions that may be helpful to your store.', 'woocommerce' ), ), array( 'title' => __( 'Show Suggestions', 'woocommerce' ), 'desc' => __( 'Display suggestions within WooCommerce', 'woocommerce' ), 'desc_tip' => esc_html__( 'Leave this box unchecked if you do not want to see suggested extensions.', 'woocommerce' ), 'id' => 'woocommerce_show_marketplace_suggestions', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'default' => 'yes', 'autoload' => false, ), array( 'type' => 'sectionend', 'id' => 'marketplace_suggestions', ), ); return apply_filters( 'woocommerce_com_integration_settings', $settings ); } /** * Get settings for the legacy API section. * * @return array */ protected function get_settings_for_legacy_api_section() { $settings = array( array( 'title' => '', 'type' => 'title', 'desc' => '', 'id' => 'legacy_api_options', ), array( 'title' => __( 'Legacy API', 'woocommerce' ), 'desc' => __( 'Enable the legacy REST API', 'woocommerce' ), 'id' => 'woocommerce_api_enabled', 'type' => 'checkbox', 'default' => 'no', ), array( 'type' => 'sectionend', 'id' => 'legacy_api_options', ), ); return apply_filters( 'woocommerce_settings_rest_api', $settings ); } /** * Form method. * * @deprecated 3.4.4 * * @param string $method Method name. * * @return string */ public function form_method( $method ) { return 'post'; } /** * Notices. */ private function notices() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['section'] ) && 'webhooks' === $_GET['section'] ) { WC_Admin_Webhooks::notices(); } if ( isset( $_GET['section'] ) && 'keys' === $_GET['section'] ) { WC_Admin_API_Keys::notices(); } // phpcs:enable } /** * Output the settings. */ public function output() { global $current_section; if ( 'webhooks' === $current_section ) { WC_Admin_Webhooks::page_output(); } elseif ( 'keys' === $current_section ) { WC_Admin_API_Keys::page_output(); } else { parent::output(); } } /** * Save settings. */ public function save() { // phpcs:disable WordPress.Security.NonceVerification.Missing global $current_section; if ( apply_filters( 'woocommerce_rest_api_valid_to_save', ! in_array( $current_section, array( 'keys', 'webhooks' ), true ) ) ) { // Prevent the T&Cs and checkout page from being set to the same page. if ( isset( $_POST['woocommerce_terms_page_id'], $_POST['woocommerce_checkout_page_id'] ) && $_POST['woocommerce_terms_page_id'] === $_POST['woocommerce_checkout_page_id'] ) { $_POST['woocommerce_terms_page_id'] = ''; } // Prevent the Cart, checkout and my account page from being set to the same page. if ( isset( $_POST['woocommerce_cart_page_id'], $_POST['woocommerce_checkout_page_id'], $_POST['woocommerce_myaccount_page_id'] ) ) { if ( $_POST['woocommerce_cart_page_id'] === $_POST['woocommerce_checkout_page_id'] ) { $_POST['woocommerce_checkout_page_id'] = ''; } if ( $_POST['woocommerce_cart_page_id'] === $_POST['woocommerce_myaccount_page_id'] ) { $_POST['woocommerce_myaccount_page_id'] = ''; } if ( $_POST['woocommerce_checkout_page_id'] === $_POST['woocommerce_myaccount_page_id'] ) { $_POST['woocommerce_myaccount_page_id'] = ''; } } $this->save_settings_for_current_section(); $this->do_update_options_action(); } // phpcs:enable } } // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Commenting.Todo.CommentFound /** * WC_Settings_Rest_API class. * * @deprecated 3.4 in favour of WC_Settings_Advanced. */ class WC_Settings_Rest_API extends WC_Settings_Advanced { } return new WC_Settings_Advanced(); // phpcs:enable includes/admin/settings/class-wc-settings-shipping.php 0000644 00000031075 15132754524 0017222 0 ustar 00 <?php /** * WooCommerce Shipping Settings * * @package WooCommerce\Admin * @version 2.6.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Shipping', false ) ) { return new WC_Settings_Shipping(); } /** * WC_Settings_Shipping. */ class WC_Settings_Shipping extends WC_Settings_Page { /** * Constructor. */ public function __construct() { $this->id = 'shipping'; $this->label = __( 'Shipping', 'woocommerce' ); parent::__construct(); } /** * Add this page to settings. * * @param array $pages Current pages. * @return array|mixed */ public function add_settings_page( $pages ) { return wc_shipping_enabled() ? parent::add_settings_page( $pages ) : $pages; } /** * Get own sections. * * @return array */ protected function get_own_sections() { $sections = array( '' => __( 'Shipping zones', 'woocommerce' ), 'options' => __( 'Shipping options', 'woocommerce' ), 'classes' => __( 'Shipping classes', 'woocommerce' ), ); if ( ! $this->wc_is_installing() ) { // Load shipping methods so we can show any global options they may have. $shipping_methods = $this->get_shipping_methods(); foreach ( $shipping_methods as $method ) { if ( ! $method->has_settings() ) { continue; } $title = empty( $method->method_title ) ? ucfirst( $method->id ) : $method->method_title; $sections[ strtolower( $method->id ) ] = esc_html( $title ); } } return $sections; } /** * Is WC_INSTALLING constant defined? * This method exists to ease unit testing. * * @return bool True is the WC_INSTALLING constant is defined. */ protected function wc_is_installing() { return Constants::is_defined( 'WC_INSTALLING' ); } /** * Get the currently available shipping methods. * This method exists to ease unit testing. * * @return array Currently available shipping methods. */ protected function get_shipping_methods() { return WC()->shipping()->get_shipping_methods(); } /** * Get settings for the default section. * * The original implementation of 'get_settings' was returning the settings for the "Options" section * when the supplied value for $current_section was ''. * * @return array */ protected function get_settings_for_default_section() { return $this->get_settings_for_options_section(); } /** * Get settings for the options section. * * @return array */ protected function get_settings_for_options_section() { $settings = array( array( 'title' => __( 'Shipping options', 'woocommerce' ), 'type' => 'title', 'id' => 'shipping_options', ), array( 'title' => __( 'Calculations', 'woocommerce' ), 'desc' => __( 'Enable the shipping calculator on the cart page', 'woocommerce' ), 'id' => 'woocommerce_enable_shipping_calc', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', 'autoload' => false, ), array( 'desc' => __( 'Hide shipping costs until an address is entered', 'woocommerce' ), 'id' => 'woocommerce_shipping_cost_requires_address', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', ), array( 'title' => __( 'Shipping destination', 'woocommerce' ), 'desc' => __( 'This controls which shipping address is used by default.', 'woocommerce' ), 'id' => 'woocommerce_ship_to_destination', 'default' => 'billing', 'type' => 'radio', 'options' => array( 'shipping' => __( 'Default to customer shipping address', 'woocommerce' ), 'billing' => __( 'Default to customer billing address', 'woocommerce' ), 'billing_only' => __( 'Force shipping to the customer billing address', 'woocommerce' ), ), 'autoload' => false, 'desc_tip' => true, 'show_if_checked' => 'option', ), array( 'title' => __( 'Debug mode', 'woocommerce' ), 'desc' => __( 'Enable debug mode', 'woocommerce' ), 'desc_tip' => __( 'Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.', 'woocommerce' ), 'id' => 'woocommerce_shipping_debug_mode', 'default' => 'no', 'type' => 'checkbox', ), array( 'type' => 'sectionend', 'id' => 'shipping_options', ), ); return apply_filters( 'woocommerce_shipping_settings', $settings ); } /** * Output the settings. */ public function output() { global $current_section, $hide_save_button; // Load shipping methods so we can show any global options they may have. $shipping_methods = $this->get_shipping_methods(); if ( '' === $current_section ) { $this->output_zones_screen(); } elseif ( 'classes' === $current_section ) { $hide_save_button = true; $this->output_shipping_class_screen(); } else { $is_shipping_method = false; foreach ( $shipping_methods as $method ) { if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ), true ) && $method->has_settings() ) { $is_shipping_method = true; $method->admin_options(); } } if ( ! $is_shipping_method ) { parent::output(); } } } /** * Save settings. */ public function save() { global $current_section; switch ( $current_section ) { case 'options': $this->save_settings_for_current_section(); $this->do_update_options_action(); break; case 'classes': $this->do_update_options_action(); break; case '': break; default: $is_shipping_method = false; foreach ( $this->get_shipping_methods() as $method_id => $method ) { if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ), true ) ) { $is_shipping_method = true; $this->do_update_options_action( $method->id ); } } if ( ! $is_shipping_method ) { $this->save_settings_for_current_section(); } break; } // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'shipping', true ); } /** * Handles output of the shipping zones page in admin. */ protected function output_zones_screen() { // phpcs:disable WordPress.Security.NonceVerification.Recommended global $hide_save_button; if ( isset( $_REQUEST['zone_id'] ) ) { $hide_save_button = true; $this->zone_methods_screen( wc_clean( wp_unslash( $_REQUEST['zone_id'] ) ) ); } elseif ( isset( $_REQUEST['instance_id'] ) ) { $this->instance_settings_screen( absint( wp_unslash( $_REQUEST['instance_id'] ) ) ); } else { $hide_save_button = true; $this->zones_screen(); } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Show method for a zone * * @param int $zone_id Zone ID. */ protected function zone_methods_screen( $zone_id ) { if ( 'new' === $zone_id ) { $zone = new WC_Shipping_Zone(); } else { $zone = WC_Shipping_Zones::get_zone( absint( $zone_id ) ); } if ( ! $zone ) { wp_die( esc_html__( 'Zone does not exist!', 'woocommerce' ) ); } $allowed_countries = WC()->countries->get_shipping_countries(); $shipping_continents = WC()->countries->get_shipping_continents(); // Prepare locations. $locations = array(); $postcodes = array(); foreach ( $zone->get_zone_locations() as $location ) { if ( 'postcode' === $location->type ) { $postcodes[] = $location->code; } else { $locations[] = $location->type . ':' . $location->code; } } wp_localize_script( 'wc-shipping-zone-methods', 'shippingZoneMethodsLocalizeScript', array( 'methods' => $zone->get_shipping_methods( false, 'json' ), 'zone_name' => $zone->get_zone_name(), 'zone_id' => $zone->get_id(), 'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ), 'strings' => array( 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), 'save_changes_prompt' => __( 'Do you wish to save your changes first? Your changed data will be discarded if you choose to cancel.', 'woocommerce' ), 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), 'add_method_failed' => __( 'Shipping method could not be added. Please retry.', 'woocommerce' ), 'yes' => __( 'Yes', 'woocommerce' ), 'no' => __( 'No', 'woocommerce' ), 'default_zone_name' => __( 'Zone', 'woocommerce' ), ), ) ); wp_enqueue_script( 'wc-shipping-zone-methods' ); include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zone-methods.php'; } /** * Show zones */ protected function zones_screen() { $method_count = wc_get_shipping_method_count( false, true ); wp_localize_script( 'wc-shipping-zones', 'shippingZonesLocalizeScript', array( 'zones' => WC_Shipping_Zones::get_zones( 'json' ), 'default_zone' => array( 'zone_id' => 0, 'zone_name' => '', 'zone_order' => null, ), 'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ), 'strings' => array( 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), 'delete_confirmation_msg' => __( 'Are you sure you want to delete this zone? This action cannot be undone.', 'woocommerce' ), 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), 'no_shipping_methods_offered' => __( 'No shipping methods offered to this zone.', 'woocommerce' ), ), ) ); wp_enqueue_script( 'wc-shipping-zones' ); include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones.php'; } /** * Show instance settings * * @param int $instance_id Shipping instance ID. */ protected function instance_settings_screen( $instance_id ) { $zone = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id ); $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); if ( ! $shipping_method ) { wp_die( esc_html__( 'Invalid shipping method!', 'woocommerce' ) ); } if ( ! $zone ) { wp_die( esc_html__( 'Zone does not exist!', 'woocommerce' ) ); } if ( ! $shipping_method->has_settings() ) { wp_die( esc_html__( 'This shipping method does not have any settings to configure.', 'woocommerce' ) ); } if ( ! empty( $_POST['save'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'woocommerce-settings' ) ) { echo '<div class="updated error"><p>' . esc_html__( 'Edit failed. Please try again.', 'woocommerce' ) . '</p></div>'; } $shipping_method->process_admin_options(); $shipping_method->display_errors(); } include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones-instance.php'; } /** * Handles output of the shipping class settings screen. */ protected function output_shipping_class_screen() { $wc_shipping = WC_Shipping::instance(); wp_localize_script( 'wc-shipping-classes', 'shippingClassesLocalizeScript', array( 'classes' => $wc_shipping->get_shipping_classes(), 'default_shipping_class' => array( 'term_id' => 0, 'name' => '', 'description' => '', ), 'wc_shipping_classes_nonce' => wp_create_nonce( 'wc_shipping_classes_nonce' ), 'strings' => array( 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), ), ) ); wp_enqueue_script( 'wc-shipping-classes' ); // Extendable columns to show on the shipping classes screen. $shipping_class_columns = apply_filters( 'woocommerce_shipping_classes_columns', array( 'wc-shipping-class-name' => __( 'Shipping class', 'woocommerce' ), 'wc-shipping-class-slug' => __( 'Slug', 'woocommerce' ), 'wc-shipping-class-description' => __( 'Description', 'woocommerce' ), 'wc-shipping-class-count' => __( 'Product count', 'woocommerce' ), ) ); include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-classes.php'; } } return new WC_Settings_Shipping(); includes/admin/class-wc-admin-permalink-settings.php 0000644 00000022153 15132754524 0016606 0 ustar 00 <?php /** * Adds settings to the permalinks admin settings page * * @class WC_Admin_Permalink_Settings * @package WooCommerce\Admin * @version 2.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'WC_Admin_Permalink_Settings', false ) ) { return new WC_Admin_Permalink_Settings(); } /** * WC_Admin_Permalink_Settings Class. */ class WC_Admin_Permalink_Settings { /** * Permalink settings. * * @var array */ private $permalinks = array(); /** * Hook in tabs. */ public function __construct() { $this->settings_init(); $this->settings_save(); } /** * Init our settings. */ public function settings_init() { add_settings_section( 'woocommerce-permalink', __( 'Product permalinks', 'woocommerce' ), array( $this, 'settings' ), 'permalink' ); add_settings_field( 'woocommerce_product_category_slug', __( 'Product category base', 'woocommerce' ), array( $this, 'product_category_slug_input' ), 'permalink', 'optional' ); add_settings_field( 'woocommerce_product_tag_slug', __( 'Product tag base', 'woocommerce' ), array( $this, 'product_tag_slug_input' ), 'permalink', 'optional' ); add_settings_field( 'woocommerce_product_attribute_slug', __( 'Product attribute base', 'woocommerce' ), array( $this, 'product_attribute_slug_input' ), 'permalink', 'optional' ); $this->permalinks = wc_get_permalink_structure(); } /** * Show a slug input box. */ public function product_category_slug_input() { ?> <input name="woocommerce_product_category_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['category_base'] ); ?>" placeholder="<?php echo esc_attr_x( 'product-category', 'slug', 'woocommerce' ); ?>" /> <?php } /** * Show a slug input box. */ public function product_tag_slug_input() { ?> <input name="woocommerce_product_tag_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['tag_base'] ); ?>" placeholder="<?php echo esc_attr_x( 'product-tag', 'slug', 'woocommerce' ); ?>" /> <?php } /** * Show a slug input box. */ public function product_attribute_slug_input() { ?> <input name="woocommerce_product_attribute_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['attribute_base'] ); ?>" /><code>/attribute-name/attribute/</code> <?php } /** * Show the settings. */ public function settings() { /* translators: %s: Home URL */ echo wp_kses_post( wpautop( sprintf( __( 'If you like, you may enter custom structures for your product URLs here. For example, using <code>shop</code> would make your product links like <code>%sshop/sample-product/</code>. This setting affects product URLs only, not things such as product categories.', 'woocommerce' ), esc_url( home_url( '/' ) ) ) ) ); $shop_page_id = wc_get_page_id( 'shop' ); $base_slug = urldecode( ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ) ); $product_base = _x( 'product', 'default-slug', 'woocommerce' ); $structures = array( 0 => '', 1 => '/' . trailingslashit( $base_slug ), 2 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ), ); ?> <table class="form-table wc-permalink-structure"> <tbody> <tr> <th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[0] ); ?>" class="wctog" <?php checked( $structures[0], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Default', 'woocommerce' ); ?></label></th> <td><code class="default-example"><?php echo esc_html( home_url() ); ?>/?product=sample-product</code> <code class="non-default-example"><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $product_base ); ?>/sample-product/</code></td> </tr> <?php if ( $shop_page_id ) : ?> <tr> <th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[1] ); ?>" class="wctog" <?php checked( $structures[1], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Shop base', 'woocommerce' ); ?></label></th> <td><code><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $base_slug ); ?>/sample-product/</code></td> </tr> <tr> <th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[2] ); ?>" class="wctog" <?php checked( $structures[2], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Shop base with category', 'woocommerce' ); ?></label></th> <td><code><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $base_slug ); ?>/product-category/sample-product/</code></td> </tr> <?php endif; ?> <tr> <th><label><input name="product_permalink" id="woocommerce_custom_selection" type="radio" value="custom" class="tog" <?php checked( in_array( $this->permalinks['product_base'], $structures, true ), false ); ?> /> <?php esc_html_e( 'Custom base', 'woocommerce' ); ?></label></th> <td> <input name="product_permalink_structure" id="woocommerce_permalink_structure" type="text" value="<?php echo esc_attr( $this->permalinks['product_base'] ? trailingslashit( $this->permalinks['product_base'] ) : '' ); ?>" class="regular-text code"> <span class="description"><?php esc_html_e( 'Enter a custom base to use. A base must be set or WordPress will use default instead.', 'woocommerce' ); ?></span> </td> </tr> </tbody> </table> <?php wp_nonce_field( 'wc-permalinks', 'wc-permalinks-nonce' ); ?> <script type="text/javascript"> jQuery( function() { jQuery('input.wctog').on( 'change', function() { jQuery('#woocommerce_permalink_structure').val( jQuery( this ).val() ); }); jQuery('.permalink-structure input').on( 'change', function() { jQuery('.wc-permalink-structure').find('code.non-default-example, code.default-example').hide(); if ( jQuery(this).val() ) { jQuery('.wc-permalink-structure code.non-default-example').show(); jQuery('.wc-permalink-structure input').prop('disabled', false); } else { jQuery('.wc-permalink-structure code.default-example').show(); jQuery('.wc-permalink-structure input:eq(0)').trigger( 'click' ); jQuery('.wc-permalink-structure input').attr('disabled', 'disabled'); } }); jQuery('.permalink-structure input:checked').trigger( 'change' ); jQuery('#woocommerce_permalink_structure').on( 'focus', function(){ jQuery('#woocommerce_custom_selection').trigger( 'click' ); } ); } ); </script> <?php } /** * Save the settings. */ public function settings_save() { if ( ! is_admin() ) { return; } // We need to save the options ourselves; settings api does not trigger save for the permalinks page. if ( isset( $_POST['permalink_structure'], $_POST['wc-permalinks-nonce'], $_POST['woocommerce_product_category_slug'], $_POST['woocommerce_product_tag_slug'], $_POST['woocommerce_product_attribute_slug'] ) && wp_verify_nonce( wp_unslash( $_POST['wc-permalinks-nonce'] ), 'wc-permalinks' ) ) { // WPCS: input var ok, sanitization ok. wc_switch_to_site_locale(); $permalinks = (array) get_option( 'woocommerce_permalinks', array() ); $permalinks['category_base'] = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_category_slug'] ) ); // WPCS: input var ok, sanitization ok. $permalinks['tag_base'] = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_tag_slug'] ) ); // WPCS: input var ok, sanitization ok. $permalinks['attribute_base'] = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_attribute_slug'] ) ); // WPCS: input var ok, sanitization ok. // Generate product base. $product_base = isset( $_POST['product_permalink'] ) ? wc_clean( wp_unslash( $_POST['product_permalink'] ) ) : ''; // WPCS: input var ok, sanitization ok. if ( 'custom' === $product_base ) { if ( isset( $_POST['product_permalink_structure'] ) ) { // WPCS: input var ok. $product_base = preg_replace( '#/+#', '/', '/' . str_replace( '#', '', trim( wp_unslash( $_POST['product_permalink_structure'] ) ) ) ); // WPCS: input var ok, sanitization ok. } else { $product_base = '/'; } // This is an invalid base structure and breaks pages. if ( '/%product_cat%/' === trailingslashit( $product_base ) ) { $product_base = '/' . _x( 'product', 'slug', 'woocommerce' ) . $product_base; } } elseif ( empty( $product_base ) ) { $product_base = _x( 'product', 'slug', 'woocommerce' ); } $permalinks['product_base'] = wc_sanitize_permalink( $product_base ); // Shop base may require verbose page rules if nesting pages. $shop_page_id = wc_get_page_id( 'shop' ); $shop_permalink = ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); if ( $shop_page_id && stristr( trim( $permalinks['product_base'], '/' ), $shop_permalink ) ) { $permalinks['use_verbose_page_rules'] = true; } update_option( 'woocommerce_permalinks', $permalinks ); wc_restore_locale(); } } } return new WC_Admin_Permalink_Settings(); includes/admin/class-wc-admin-exporters.php 0000644 00000015057 15132754524 0015026 0 ustar 00 <?php /** * Init WooCommerce data exporters. * * @package WooCommerce\Admin * @version 3.1.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Admin_Exporters Class. */ class WC_Admin_Exporters { /** * Array of exporter IDs. * * @var string[] */ protected $exporters = array(); /** * Constructor. */ public function __construct() { if ( ! $this->export_allowed() ) { return; } add_action( 'admin_menu', array( $this, 'add_to_menus' ) ); add_action( 'admin_head', array( $this, 'hide_from_menus' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); add_action( 'admin_init', array( $this, 'download_export_file' ) ); add_action( 'wp_ajax_woocommerce_do_ajax_product_export', array( $this, 'do_ajax_product_export' ) ); // Register WooCommerce exporters. $this->exporters['product_exporter'] = array( 'menu' => 'edit.php?post_type=product', 'name' => __( 'Product Export', 'woocommerce' ), 'capability' => 'export', 'callback' => array( $this, 'product_exporter' ), ); } /** * Return true if WooCommerce export is allowed for current user, false otherwise. * * @return bool Whether current user can perform export. */ protected function export_allowed() { return current_user_can( 'edit_products' ) && current_user_can( 'export' ); } /** * Add menu items for our custom exporters. */ public function add_to_menus() { foreach ( $this->exporters as $id => $exporter ) { add_submenu_page( $exporter['menu'], $exporter['name'], $exporter['name'], $exporter['capability'], $id, $exporter['callback'] ); } } /** * Hide menu items from view so the pages exist, but the menu items do not. */ public function hide_from_menus() { global $submenu; foreach ( $this->exporters as $id => $exporter ) { if ( isset( $submenu[ $exporter['menu'] ] ) ) { foreach ( $submenu[ $exporter['menu'] ] as $key => $menu ) { if ( $id === $menu[2] ) { unset( $submenu[ $exporter['menu'] ][ $key ] ); } } } } } /** * Enqueue scripts. */ public function admin_scripts() { $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_register_script( 'wc-product-export', WC()->plugin_url() . '/assets/js/admin/wc-product-export' . $suffix . '.js', array( 'jquery' ), $version ); wp_localize_script( 'wc-product-export', 'wc_product_export_params', array( 'export_nonce' => wp_create_nonce( 'wc-product-export' ), ) ); } /** * Export page UI. */ public function product_exporter() { include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php'; include_once dirname( __FILE__ ) . '/views/html-admin-page-product-export.php'; } /** * Serve the generated file. */ public function download_export_file() { if ( isset( $_GET['action'], $_GET['nonce'] ) && wp_verify_nonce( wp_unslash( $_GET['nonce'] ), 'product-csv' ) && 'download_product_csv' === wp_unslash( $_GET['action'] ) ) { // WPCS: input var ok, sanitization ok. include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php'; $exporter = new WC_Product_CSV_Exporter(); if ( ! empty( $_GET['filename'] ) ) { // WPCS: input var ok. $exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // WPCS: input var ok, sanitization ok. } $exporter->export(); } } /** * AJAX callback for doing the actual export to the CSV file. */ public function do_ajax_product_export() { check_ajax_referer( 'wc-product-export', 'security' ); if ( ! $this->export_allowed() ) { wp_send_json_error( array( 'message' => __( 'Insufficient privileges to export products.', 'woocommerce' ) ) ); } include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php'; $step = isset( $_POST['step'] ) ? absint( $_POST['step'] ) : 1; // WPCS: input var ok, sanitization ok. $exporter = new WC_Product_CSV_Exporter(); if ( ! empty( $_POST['columns'] ) ) { // WPCS: input var ok. $exporter->set_column_names( wp_unslash( $_POST['columns'] ) ); // WPCS: input var ok, sanitization ok. } if ( ! empty( $_POST['selected_columns'] ) ) { // WPCS: input var ok. $exporter->set_columns_to_export( wp_unslash( $_POST['selected_columns'] ) ); // WPCS: input var ok, sanitization ok. } if ( ! empty( $_POST['export_meta'] ) ) { // WPCS: input var ok. $exporter->enable_meta_export( true ); } if ( ! empty( $_POST['export_types'] ) ) { // WPCS: input var ok. $exporter->set_product_types_to_export( wp_unslash( $_POST['export_types'] ) ); // WPCS: input var ok, sanitization ok. } if ( ! empty( $_POST['export_category'] ) && is_array( $_POST['export_category'] ) ) {// WPCS: input var ok. $exporter->set_product_category_to_export( wp_unslash( array_values( $_POST['export_category'] ) ) ); // WPCS: input var ok, sanitization ok. } if ( ! empty( $_POST['filename'] ) ) { // WPCS: input var ok. $exporter->set_filename( wp_unslash( $_POST['filename'] ) ); // WPCS: input var ok, sanitization ok. } $exporter->set_page( $step ); $exporter->generate_file(); $query_args = apply_filters( 'woocommerce_export_get_ajax_query_args', array( 'nonce' => wp_create_nonce( 'product-csv' ), 'action' => 'download_product_csv', 'filename' => $exporter->get_filename(), ) ); if ( 100 === $exporter->get_percent_complete() ) { wp_send_json_success( array( 'step' => 'done', 'percentage' => 100, 'url' => add_query_arg( $query_args, admin_url( 'edit.php?post_type=product&page=product_exporter' ) ), ) ); } else { wp_send_json_success( array( 'step' => ++$step, 'percentage' => $exporter->get_percent_complete(), 'columns' => $exporter->get_column_names(), ) ); } } /** * Gets the product types that can be exported. * * @since 5.1.0 * @return array The product types keys and labels. */ public static function get_product_types() { $product_types = wc_get_product_types(); $product_types['variation'] = __( 'Product variations', 'woocommerce' ); /** * Allow third-parties to filter the exportable product types. * * @since 5.1.0 * @param array $product_types { * The product type key and label. * * @type string Product type key - eg 'variable', 'simple' etc. * @type string A translated product label which appears in the export product type dropdown. * } */ return apply_filters( 'woocommerce_exporter_product_types', $product_types ); } } new WC_Admin_Exporters(); includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php 0000644 00000040410 15132754524 0017664 0 ustar 00 <?php /** * Coupon Data * * Display the coupon data meta box. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Meta_Box_Coupon_Data Class. */ class WC_Meta_Box_Coupon_Data { /** * Output the metabox. * * @param WP_Post $post */ public static function output( $post ) { wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); $coupon_id = absint( $post->ID ); $coupon = new WC_Coupon( $coupon_id ); ?> <style type="text/css"> #edit-slug-box, #minor-publishing-actions { display:none } </style> <div id="coupon_options" class="panel-wrap coupon_data"> <div class="wc-tabs-back"></div> <ul class="coupon_data_tabs wc-tabs" style="display:none;"> <?php $coupon_data_tabs = apply_filters( 'woocommerce_coupon_data_tabs', array( 'general' => array( 'label' => __( 'General', 'woocommerce' ), 'target' => 'general_coupon_data', 'class' => 'general_coupon_data', ), 'usage_restriction' => array( 'label' => __( 'Usage restriction', 'woocommerce' ), 'target' => 'usage_restriction_coupon_data', 'class' => '', ), 'usage_limit' => array( 'label' => __( 'Usage limits', 'woocommerce' ), 'target' => 'usage_limit_coupon_data', 'class' => '', ), ) ); foreach ( $coupon_data_tabs as $key => $tab ) : ?> <li class="<?php echo $key; ?>_options <?php echo $key; ?>_tab <?php echo implode( ' ', (array) $tab['class'] ); ?>"> <a href="#<?php echo $tab['target']; ?>"> <span><?php echo esc_html( $tab['label'] ); ?></span> </a> </li> <?php endforeach; ?> </ul> <div id="general_coupon_data" class="panel woocommerce_options_panel"> <?php // Type. woocommerce_wp_select( array( 'id' => 'discount_type', 'label' => __( 'Discount type', 'woocommerce' ), 'options' => wc_get_coupon_types(), 'value' => $coupon->get_discount_type( 'edit' ), ) ); // Amount. woocommerce_wp_text_input( array( 'id' => 'coupon_amount', 'label' => __( 'Coupon amount', 'woocommerce' ), 'placeholder' => wc_format_localized_price( 0 ), 'description' => __( 'Value of the coupon.', 'woocommerce' ), 'data_type' => 'percent' === $coupon->get_discount_type( 'edit' ) ? 'decimal' : 'price', 'desc_tip' => true, 'value' => $coupon->get_amount( 'edit' ), ) ); // Free Shipping. if ( wc_shipping_enabled() ) { woocommerce_wp_checkbox( array( 'id' => 'free_shipping', 'label' => __( 'Allow free shipping', 'woocommerce' ), 'description' => sprintf( __( 'Check this box if the coupon grants free shipping. A <a href="%s" target="_blank">free shipping method</a> must be enabled in your shipping zone and be set to require "a valid free shipping coupon" (see the "Free Shipping Requires" setting).', 'woocommerce' ), 'https://docs.woocommerce.com/document/free-shipping/' ), 'value' => wc_bool_to_string( $coupon->get_free_shipping( 'edit' ) ), ) ); } // Expiry date. $expiry_date = $coupon->get_date_expires( 'edit' ) ? $coupon->get_date_expires( 'edit' )->date( 'Y-m-d' ) : ''; woocommerce_wp_text_input( array( 'id' => 'expiry_date', 'value' => esc_attr( $expiry_date ), 'label' => __( 'Coupon expiry date', 'woocommerce' ), 'placeholder' => 'YYYY-MM-DD', 'description' => __( 'The coupon will expire at 00:00:00 of this date.', 'woocommerce' ), 'desc_tip' => true, 'class' => 'date-picker', 'custom_attributes' => array( 'pattern' => apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ), ), ) ); do_action( 'woocommerce_coupon_options', $coupon->get_id(), $coupon ); ?> </div> <div id="usage_restriction_coupon_data" class="panel woocommerce_options_panel"> <?php echo '<div class="options_group">'; // minimum spend. woocommerce_wp_text_input( array( 'id' => 'minimum_amount', 'label' => __( 'Minimum spend', 'woocommerce' ), 'placeholder' => __( 'No minimum', 'woocommerce' ), 'description' => __( 'This field allows you to set the minimum spend (subtotal) allowed to use the coupon.', 'woocommerce' ), 'data_type' => 'price', 'desc_tip' => true, 'value' => $coupon->get_minimum_amount( 'edit' ), ) ); // maximum spend. woocommerce_wp_text_input( array( 'id' => 'maximum_amount', 'label' => __( 'Maximum spend', 'woocommerce' ), 'placeholder' => __( 'No maximum', 'woocommerce' ), 'description' => __( 'This field allows you to set the maximum spend (subtotal) allowed when using the coupon.', 'woocommerce' ), 'data_type' => 'price', 'desc_tip' => true, 'value' => $coupon->get_maximum_amount( 'edit' ), ) ); // Individual use. woocommerce_wp_checkbox( array( 'id' => 'individual_use', 'label' => __( 'Individual use only', 'woocommerce' ), 'description' => __( 'Check this box if the coupon cannot be used in conjunction with other coupons.', 'woocommerce' ), 'value' => wc_bool_to_string( $coupon->get_individual_use( 'edit' ) ), ) ); // Exclude Sale Products. woocommerce_wp_checkbox( array( 'id' => 'exclude_sale_items', 'label' => __( 'Exclude sale items', 'woocommerce' ), 'description' => __( 'Check this box if the coupon should not apply to items on sale. Per-item coupons will only work if the item is not on sale. Per-cart coupons will only work if there are items in the cart that are not on sale.', 'woocommerce' ), 'value' => wc_bool_to_string( $coupon->get_exclude_sale_items( 'edit' ) ), ) ); echo '</div><div class="options_group">'; // Product ids. ?> <p class="form-field"> <label><?php _e( 'Products', 'woocommerce' ); ?></label> <select class="wc-product-search" multiple="multiple" style="width: 50%;" name="product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations"> <?php $product_ids = $coupon->get_product_ids( 'edit' ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( is_object( $product ) ) { echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Products that the coupon will be applied to, or that need to be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?> </p> <?php // Exclude Product ids. ?> <p class="form-field"> <label><?php _e( 'Exclude products', 'woocommerce' ); ?></label> <select class="wc-product-search" multiple="multiple" style="width: 50%;" name="exclude_product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations"> <?php $product_ids = $coupon->get_excluded_product_ids( 'edit' ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( is_object( $product ) ) { echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Products that the coupon will not be applied to, or that cannot be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?> </p> <?php echo '</div><div class="options_group">'; // Categories. ?> <p class="form-field"> <label for="product_categories"><?php _e( 'Product categories', 'woocommerce' ); ?></label> <select id="product_categories" name="product_categories[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'Any category', 'woocommerce' ); ?>"> <?php $category_ids = $coupon->get_product_categories( 'edit' ); $categories = get_terms( 'product_cat', 'orderby=name&hide_empty=0' ); if ( $categories ) { foreach ( $categories as $cat ) { echo '<option value="' . esc_attr( $cat->term_id ) . '"' . wc_selected( $cat->term_id, $category_ids ) . '>' . esc_html( $cat->name ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Product categories that the coupon will be applied to, or that need to be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?> </p> <?php // Exclude Categories. ?> <p class="form-field"> <label for="exclude_product_categories"><?php _e( 'Exclude categories', 'woocommerce' ); ?></label> <select id="exclude_product_categories" name="exclude_product_categories[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No categories', 'woocommerce' ); ?>"> <?php $category_ids = $coupon->get_excluded_product_categories( 'edit' ); $categories = get_terms( 'product_cat', 'orderby=name&hide_empty=0' ); if ( $categories ) { foreach ( $categories as $cat ) { echo '<option value="' . esc_attr( $cat->term_id ) . '"' . wc_selected( $cat->term_id, $category_ids ) . '>' . esc_html( $cat->name ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Product categories that the coupon will not be applied to, or that cannot be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?> </p> </div> <div class="options_group"> <?php // Customers. woocommerce_wp_text_input( array( 'id' => 'customer_email', 'label' => __( 'Allowed emails', 'woocommerce' ), 'placeholder' => __( 'No restrictions', 'woocommerce' ), 'description' => __( 'List of allowed billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.', 'woocommerce' ), 'value' => implode( ', ', (array) $coupon->get_email_restrictions( 'edit' ) ), 'desc_tip' => true, 'type' => 'email', 'class' => '', 'custom_attributes' => array( 'multiple' => 'multiple', ), ) ); ?> </div> <?php do_action( 'woocommerce_coupon_options_usage_restriction', $coupon->get_id(), $coupon ); ?> </div> <div id="usage_limit_coupon_data" class="panel woocommerce_options_panel"> <div class="options_group"> <?php // Usage limit per coupons. woocommerce_wp_text_input( array( 'id' => 'usage_limit', 'label' => __( 'Usage limit per coupon', 'woocommerce' ), 'placeholder' => esc_attr__( 'Unlimited usage', 'woocommerce' ), 'description' => __( 'How many times this coupon can be used before it is void.', 'woocommerce' ), 'type' => 'number', 'desc_tip' => true, 'class' => 'short', 'custom_attributes' => array( 'step' => 1, 'min' => 0, ), 'value' => $coupon->get_usage_limit( 'edit' ) ? $coupon->get_usage_limit( 'edit' ) : '', ) ); // Usage limit per product. woocommerce_wp_text_input( array( 'id' => 'limit_usage_to_x_items', 'label' => __( 'Limit usage to X items', 'woocommerce' ), 'placeholder' => esc_attr__( 'Apply to all qualifying items in cart', 'woocommerce' ), 'description' => __( 'The maximum number of individual items this coupon can apply to when using product discounts. Leave blank to apply to all qualifying items in cart.', 'woocommerce' ), 'desc_tip' => true, 'class' => 'short', 'type' => 'number', 'custom_attributes' => array( 'step' => 1, 'min' => 0, ), 'value' => $coupon->get_limit_usage_to_x_items( 'edit' ) ? $coupon->get_limit_usage_to_x_items( 'edit' ) : '', ) ); // Usage limit per users. woocommerce_wp_text_input( array( 'id' => 'usage_limit_per_user', 'label' => __( 'Usage limit per user', 'woocommerce' ), 'placeholder' => esc_attr__( 'Unlimited usage', 'woocommerce' ), 'description' => __( 'How many times this coupon can be used by an individual user. Uses billing email for guests, and user ID for logged in users.', 'woocommerce' ), 'desc_tip' => true, 'class' => 'short', 'type' => 'number', 'custom_attributes' => array( 'step' => 1, 'min' => 0, ), 'value' => $coupon->get_usage_limit_per_user( 'edit' ) ? $coupon->get_usage_limit_per_user( 'edit' ) : '', ) ); ?> </div> <?php do_action( 'woocommerce_coupon_options_usage_limit', $coupon->get_id(), $coupon ); ?> </div> <?php do_action( 'woocommerce_coupon_data_panels', $coupon->get_id(), $coupon ); ?> <div class="clear"></div> </div> <?php } /** * Save meta box data. * * @param int $post_id * @param WP_Post $post */ public static function save( $post_id, $post ) { // Check for dupe coupons. $coupon_code = wc_format_coupon_code( $post->post_title ); $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $post_id ); if ( $id_from_code ) { WC_Admin_Meta_Boxes::add_error( __( 'Coupon code already exists - customers will use the latest coupon with this code.', 'woocommerce' ) ); } $product_categories = isset( $_POST['product_categories'] ) ? (array) $_POST['product_categories'] : array(); $exclude_product_categories = isset( $_POST['exclude_product_categories'] ) ? (array) $_POST['exclude_product_categories'] : array(); $coupon = new WC_Coupon( $post_id ); $coupon->set_props( array( 'code' => $post->post_title, 'discount_type' => wc_clean( $_POST['discount_type'] ), 'amount' => wc_format_decimal( $_POST['coupon_amount'] ), 'date_expires' => wc_clean( $_POST['expiry_date'] ), 'individual_use' => isset( $_POST['individual_use'] ), 'product_ids' => isset( $_POST['product_ids'] ) ? array_filter( array_map( 'intval', (array) $_POST['product_ids'] ) ) : array(), 'excluded_product_ids' => isset( $_POST['exclude_product_ids'] ) ? array_filter( array_map( 'intval', (array) $_POST['exclude_product_ids'] ) ) : array(), 'usage_limit' => absint( $_POST['usage_limit'] ), 'usage_limit_per_user' => absint( $_POST['usage_limit_per_user'] ), 'limit_usage_to_x_items' => absint( $_POST['limit_usage_to_x_items'] ), 'free_shipping' => isset( $_POST['free_shipping'] ), 'product_categories' => array_filter( array_map( 'intval', $product_categories ) ), 'excluded_product_categories' => array_filter( array_map( 'intval', $exclude_product_categories ) ), 'exclude_sale_items' => isset( $_POST['exclude_sale_items'] ), 'minimum_amount' => wc_format_decimal( $_POST['minimum_amount'] ), 'maximum_amount' => wc_format_decimal( $_POST['maximum_amount'] ), 'email_restrictions' => array_filter( array_map( 'trim', explode( ',', wc_clean( $_POST['customer_email'] ) ) ) ), ) ); $coupon->save(); do_action( 'woocommerce_coupon_options_save', $post_id, $coupon ); } } includes/admin/meta-boxes/views/html-product-attribute.php 0000644 00000011370 15132754524 0020011 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>" class="woocommerce_attribute wc-metabox postbox closed <?php echo esc_attr( implode( ' ', $metabox_class ) ); ?>" rel="<?php echo esc_attr( $attribute->get_position() ); ?>"> <h3> <a href="#" class="remove_row delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a> <div class="handlediv" title="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div> <div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop to set admin attribute order', 'woocommerce' ); ?>"></div> <strong class="attribute_name"><?php echo wc_attribute_label( $attribute->get_name() ); ?></strong> </h3> <div class="woocommerce_attribute_data wc-metabox-content hidden"> <table cellpadding="0" cellspacing="0"> <tbody> <tr> <td class="attribute_name"> <label><?php esc_html_e( 'Name', 'woocommerce' ); ?>:</label> <?php if ( $attribute->is_taxonomy() ) : ?> <strong><?php echo wc_attribute_label( $attribute->get_name() ); ?></strong> <input type="hidden" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" /> <?php else : ?> <input type="text" class="attribute_name" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" /> <?php endif; ?> <input type="hidden" name="attribute_position[<?php echo esc_attr( $i ); ?>]" class="attribute_position" value="<?php echo esc_attr( $attribute->get_position() ); ?>" /> </td> <td rowspan="3"> <label><?php esc_html_e( 'Value(s)', 'woocommerce' ); ?>:</label> <?php if ( $attribute->is_taxonomy() && $attribute_taxonomy = $attribute->get_taxonomy_object() ) { $attribute_types = wc_get_attribute_types(); if ( ! array_key_exists( $attribute_taxonomy->attribute_type, $attribute_types ) ) { $attribute_taxonomy->attribute_type = 'select'; } if ( 'select' === $attribute_taxonomy->attribute_type ) { ?> <select multiple="multiple" data-placeholder="<?php esc_attr_e( 'Select terms', 'woocommerce' ); ?>" class="multiselect attribute_values wc-enhanced-select" name="attribute_values[<?php echo esc_attr( $i ); ?>][]"> <?php $args = array( 'orderby' => ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name', 'hide_empty' => 0, ); $all_terms = get_terms( $attribute->get_taxonomy(), apply_filters( 'woocommerce_product_attribute_terms', $args ) ); if ( $all_terms ) { foreach ( $all_terms as $term ) { $options = $attribute->get_options(); $options = ! empty( $options ) ? $options : array(); echo '<option value="' . esc_attr( $term->term_id ) . '"' . wc_selected( $term->term_id, $options ) . '>' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $term->name, $term ) ) . '</option>'; } } ?> </select> <button class="button plus select_all_attributes"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></button> <button class="button minus select_no_attributes"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></button> <button class="button fr plus add_new_attribute"><?php esc_html_e( 'Add new', 'woocommerce' ); ?></button> <?php } do_action( 'woocommerce_product_option_terms', $attribute_taxonomy, $i, $attribute ); } else { /* translators: %s: WC_DELIMITER */ ?> <textarea name="attribute_values[<?php echo esc_attr( $i ); ?>]" cols="5" rows="5" placeholder="<?php printf( esc_attr__( 'Enter some text, or some attributes by "%s" separating values.', 'woocommerce' ), WC_DELIMITER ); ?>"><?php echo esc_textarea( wc_implode_text_attributes( $attribute->get_options() ) ); ?></textarea> <?php } ?> </td> </tr> <tr> <td> <label><input type="checkbox" class="checkbox" <?php checked( $attribute->get_visible(), true ); ?> name="attribute_visibility[<?php echo esc_attr( $i ); ?>]" value="1" /> <?php esc_html_e( 'Visible on the product page', 'woocommerce' ); ?></label> </td> </tr> <tr> <td> <div class="enable_variation show_if_variable"> <label><input type="checkbox" class="checkbox" <?php checked( $attribute->get_variation(), true ); ?> name="attribute_variation[<?php echo esc_attr( $i ); ?>]" value="1" /> <?php esc_html_e( 'Used for variations', 'woocommerce' ); ?></label> </div> </td> </tr> <?php do_action( 'woocommerce_after_product_attribute_settings', $attribute, $i ); ?> </tbody> </table> </div> </div> includes/admin/meta-boxes/views/html-product-data-panel.php 0000644 00000004434 15132754524 0020017 0 ustar 00 <?php /** * Product data meta box. * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="panel-wrap product_data"> <span class="type_box hidden"> — <label for="product-type"> <select id="product-type" name="product-type"> <optgroup label="<?php esc_attr_e( 'Product Type', 'woocommerce' ); ?>"> <?php foreach ( wc_get_product_types() as $value => $label ) : ?> <option value="<?php echo esc_attr( $value ); ?>" <?php echo selected( $product_object->get_type(), $value, false ); ?>><?php echo esc_html( $label ); ?></option> <?php endforeach; ?> </optgroup> </select> </label> <?php foreach ( self::get_product_type_options() as $key => $option ) : if ( metadata_exists( 'post', $post->ID, '_' . $key ) ) { $selected_value = is_callable( array( $product_object, "is_$key" ) ) ? $product_object->{"is_$key"}() : 'yes' === get_post_meta( $post->ID, '_' . $key, true ); } else { $selected_value = 'yes' === ( isset( $option['default'] ) ? $option['default'] : 'no' ); } ?> <label for="<?php echo esc_attr( $option['id'] ); ?>" class="<?php echo esc_attr( $option['wrapper_class'] ); ?> tips" data-tip="<?php echo esc_attr( $option['description'] ); ?>"> <?php echo esc_html( $option['label'] ); ?>: <input type="checkbox" name="<?php echo esc_attr( $option['id'] ); ?>" id="<?php echo esc_attr( $option['id'] ); ?>" <?php echo checked( $selected_value, true, false ); ?> /> </label> <?php endforeach; ?> </span> <ul class="product_data_tabs wc-tabs"> <?php foreach ( self::get_product_data_tabs() as $key => $tab ) : ?> <li class="<?php echo esc_attr( $key ); ?>_options <?php echo esc_attr( $key ); ?>_tab <?php echo esc_attr( isset( $tab['class'] ) ? implode( ' ', (array) $tab['class'] ) : '' ); ?>"> <a href="#<?php echo esc_attr( $tab['target'] ); ?>"><span><?php echo esc_html( $tab['label'] ); ?></span></a> </li> <?php endforeach; ?> <?php do_action( 'woocommerce_product_write_panel_tabs' ); ?> </ul> <?php self::output_tabs(); self::output_variations(); do_action( 'woocommerce_product_data_panels' ); wc_do_deprecated_action( 'woocommerce_product_write_panels', array(), '2.6', 'Use woocommerce_product_data_panels action instead.' ); ?> <div class="clear"></div> </div> includes/admin/meta-boxes/views/html-order-download-permission.php 0000644 00000007034 15132754524 0021440 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div class="wc-metabox closed"> <h3 class="fixed"> <button type="button" data-permission_id="<?php echo esc_attr( $download->get_id() ); ?>" rel="<?php echo esc_attr( $download->get_product_id() ) . ',' . esc_attr( $download->get_download_id() ); ?>" class="revoke_access button"><?php esc_html_e( 'Revoke access', 'woocommerce' ); ?></button> <div class="handlediv" aria-label="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div> <strong> <?php printf( '#%s — %s — %s: %s — ', esc_html( $product->get_id() ), esc_html( apply_filters( 'woocommerce_admin_download_permissions_title', $product->get_name(), $download->get_product_id(), $download->get_order_id(), $download->get_order_key(), $download->get_download_id() ) ), esc_html( $file_count ), esc_html( wc_get_filename_from_url( $product->get_file_download_path( $download->get_download_id() ) ) ) ); printf( _n( 'Downloaded %s time', 'Downloaded %s times', $download->get_download_count(), 'woocommerce' ), esc_html( $download->get_download_count() ) ) ?> </strong> </h3> <table cellpadding="0" cellspacing="0" class="wc-metabox-content"> <tbody> <tr> <td> <label><?php esc_html_e( 'Downloads remaining', 'woocommerce' ); ?></label> <input type="hidden" name="permission_id[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $download->get_id() ); ?>" /> <input type="number" step="1" min="0" class="short" name="downloads_remaining[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $download->get_downloads_remaining() ); ?>" placeholder="<?php esc_attr_e( 'Unlimited', 'woocommerce' ); ?>" /> </td> <td> <label><?php esc_html_e( 'Access expires', 'woocommerce' ); ?></label> <input type="text" class="short date-picker" name="access_expires[<?php echo esc_attr( $loop ); ?>]" value="<?php echo ! is_null( $download->get_access_expires() ) ? esc_attr( date_i18n( 'Y-m-d', $download->get_access_expires()->getTimestamp() ) ) : ''; ?>" maxlength="10" placeholder="<?php esc_attr_e( 'Never', 'woocommerce' ); ?>" pattern="<?php echo esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ); ?>" /> </td> <td> <label><?php esc_html_e( 'Customer download link', 'woocommerce' ); ?></label> <?php $download_link = add_query_arg( array( 'download_file' => $download->get_product_id(), 'order' => $download->get_order_key(), 'email' => urlencode( $download->get_user_email() ), 'key' => $download->get_download_id(), ), trailingslashit( home_url() ) ); ?> <a id="copy-download-link" class="button" href="<?php echo esc_url( $download_link ); ?>" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>" data-tip-failed="<?php esc_attr_e( 'Copying to clipboard failed. You should be able to right-click the button and copy.', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy link', 'woocommerce' ); ?></a> </td> <td> <label><?php esc_html_e( 'Customer download log', 'woocommerce' ); ?></label> <?php $report_url = add_query_arg( 'permission_id', rawurlencode( $download->get_id() ), admin_url( 'admin.php?page=wc-reports&tab=orders&report=downloads' ) ); echo '<a class="button" href="' . esc_url( $report_url ) . '">'; esc_html_e( 'View report', 'woocommerce' ); echo '</a>'; ?> </td> </tr> </tbody> </table> </div> includes/admin/meta-boxes/views/html-order-notes.php 0000644 00000003076 15132754524 0016575 0 ustar 00 <?php /** * Order notes HTML for meta box. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; ?> <ul class="order_notes"> <?php if ( $notes ) { foreach ( $notes as $note ) { $css_class = array( 'note' ); $css_class[] = $note->customer_note ? 'customer-note' : ''; $css_class[] = 'system' === $note->added_by ? 'system-note' : ''; $css_class = apply_filters( 'woocommerce_order_note_class', array_filter( $css_class ), $note ); ?> <li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $css_class ) ); ?>"> <div class="note_content"> <?php echo wpautop( wptexturize( wp_kses_post( $note->content ) ) ); // @codingStandardsIgnoreLine ?> </div> <p class="meta"> <abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'Y-m-d H:i:s' ) ); ?>"> <?php /* translators: %1$s: note date %2$s: note time */ echo esc_html( sprintf( __( '%1$s at %2$s', 'woocommerce' ), $note->date_created->date_i18n( wc_date_format() ), $note->date_created->date_i18n( wc_time_format() ) ) ); ?> </abbr> <?php if ( 'system' !== $note->added_by ) : /* translators: %s: note author */ echo esc_html( sprintf( ' ' . __( 'by %s', 'woocommerce' ), $note->added_by ) ); endif; ?> <a href="#" class="delete_note" role="button"><?php esc_html_e( 'Delete note', 'woocommerce' ); ?></a> </p> </li> <?php } } else { ?> <li class="no-items"><?php esc_html_e( 'There are no notes yet.', 'woocommerce' ); ?></li> <?php } ?> </ul> includes/admin/meta-boxes/views/html-product-data-attributes.php 0000644 00000004171 15132754524 0021104 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="product_attributes" class="panel wc-metaboxes-wrapper hidden"> <div class="toolbar toolbar-top"> <span class="expand-close"> <a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a> </span> <select name="attribute_taxonomy" class="attribute_taxonomy"> <option value=""><?php esc_html_e( 'Custom product attribute', 'woocommerce' ); ?></option> <?php global $wc_product_attributes; // Array of defined attribute taxonomies. $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $tax ) { $attribute_taxonomy_name = wc_attribute_taxonomy_name( $tax->attribute_name ); $label = $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name; echo '<option value="' . esc_attr( $attribute_taxonomy_name ) . '">' . esc_html( $label ) . '</option>'; } } ?> </select> <button type="button" class="button add_attribute"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button> </div> <div class="product_attributes wc-metaboxes"> <?php // Product attributes - taxonomies and custom, ordered, with visibility and variation attributes set. $attributes = $product_object->get_attributes( 'edit' ); $i = -1; foreach ( $attributes as $attribute ) { $i++; $metabox_class = array(); if ( $attribute->is_taxonomy() ) { $metabox_class[] = 'taxonomy'; $metabox_class[] = $attribute->get_name(); } include __DIR__ . '/html-product-attribute.php'; } ?> </div> <div class="toolbar"> <span class="expand-close"> <a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a> </span> <button type="button" class="button save_attributes button-primary"><?php esc_html_e( 'Save attributes', 'woocommerce' ); ?></button> </div> <?php do_action( 'woocommerce_product_options_attributes' ); ?> </div> includes/admin/meta-boxes/views/html-variation-admin.php 0000644 00000055274 15132754524 0017425 0 ustar 00 <?php /** * Outputs a variation for editing. * * @package WooCommerce\Admin * @var int $variation_id * @var WP_POST $variation * @var WC_Product_Variation $variation_object * @var array $variation_data array of variation data @deprecated 4.4.0. */ defined( 'ABSPATH' ) || exit; ?> <div class="woocommerce_variation wc-metabox closed"> <h3> <a href="#" class="remove_variation delete" rel="<?php echo esc_attr( $variation_id ); ?>"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a> <div class="handlediv" aria-label="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div> <div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop, or click to set admin variation order', 'woocommerce' ); ?>"></div> <strong>#<?php echo esc_html( $variation_id ); ?> </strong> <?php $attribute_values = $variation_object->get_attributes( 'edit' ); foreach ( $product_object->get_attributes( 'edit' ) as $attribute ) { if ( ! $attribute->get_variation() ) { continue; } $selected_value = isset( $attribute_values[ sanitize_title( $attribute->get_name() ) ] ) ? $attribute_values[ sanitize_title( $attribute->get_name() ) ] : ''; ?> <select name="attribute_<?php echo esc_attr( sanitize_title( $attribute->get_name() ) . "[{$loop}]" ); ?>"> <option value=""> <?php /* translators: %s: attribute label */ printf( esc_html__( 'Any %s…', 'woocommerce' ), wc_attribute_label( $attribute->get_name() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </option> <?php if ( $attribute->is_taxonomy() ) : ?> <?php foreach ( $attribute->get_terms() as $option ) : ?> <option <?php selected( $selected_value, $option->slug ); ?> value="<?php echo esc_attr( $option->slug ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option->name, $option, $attribute->get_name(), $product_object ) ); ?></option> <?php endforeach; ?> <?php else : ?> <?php foreach ( $attribute->get_options() as $option ) : ?> <option <?php selected( $selected_value, $option ); ?> value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute->get_name(), $product_object ) ); ?></option> <?php endforeach; ?> <?php endif; ?> </select> <?php } ?> <input type="hidden" class="variable_post_id" name="variable_post_id[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $variation_id ); ?>" /> <input type="hidden" class="variation_menu_order" name="variation_menu_order[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $variation_object->get_menu_order( 'edit' ) ); ?>" /> <?php /** * Variations header action. * * @since 3.6.0 * * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_header', $variation ); ?> </h3> <div class="woocommerce_variable_attributes wc-metabox-content" style="display: none;"> <div class="data"> <p class="form-row form-row-first upload_image"> <a href="#" class="upload_image_button tips <?php echo $variation_object->get_image_id( 'edit' ) ? 'remove' : ''; ?>" data-tip="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_attr__( 'Remove this image', 'woocommerce' ) : esc_attr__( 'Upload an image', 'woocommerce' ); ?>" rel="<?php echo esc_attr( $variation_id ); ?>"> <img src="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_url( wp_get_attachment_thumb_url( $variation_object->get_image_id( 'edit' ) ) ) : esc_url( wc_placeholder_img_src() ); ?>" /><input type="hidden" name="upload_image_id[<?php echo esc_attr( $loop ); ?>]" class="upload_image_id" value="<?php echo esc_attr( $variation_object->get_image_id( 'edit' ) ); ?>" /> </a> </p> <?php if ( wc_product_sku_enabled() ) { woocommerce_wp_text_input( array( 'id' => "variable_sku{$loop}", 'name' => "variable_sku[{$loop}]", 'value' => $variation_object->get_sku( 'edit' ), 'placeholder' => $variation_object->get_sku(), 'label' => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>', 'desc_tip' => true, 'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ), 'wrapper_class' => 'form-row form-row-last', ) ); } ?> <p class="form-row form-row-full options"> <label> <?php esc_html_e( 'Enabled', 'woocommerce' ); ?> <input type="checkbox" class="checkbox" name="variable_enabled[<?php echo esc_attr( $loop ); ?>]" <?php checked( in_array( $variation_object->get_status( 'edit' ), array( 'publish', false ), true ), true ); ?> /> </label> <label class="tips" data-tip="<?php esc_attr_e( 'Enable this option if access is given to a downloadable file upon purchase of a product', 'woocommerce' ); ?>"> <?php esc_html_e( 'Downloadable', 'woocommerce' ); ?> <input type="checkbox" class="checkbox variable_is_downloadable" name="variable_is_downloadable[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_downloadable( 'edit' ), true ); ?> /> </label> <label class="tips" data-tip="<?php esc_attr_e( 'Enable this option if a product is not shipped or there is no shipping cost', 'woocommerce' ); ?>"> <?php esc_html_e( 'Virtual', 'woocommerce' ); ?> <input type="checkbox" class="checkbox variable_is_virtual" name="variable_is_virtual[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_virtual( 'edit' ), true ); ?> /> </label> <?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?> <label class="tips" data-tip="<?php esc_attr_e( 'Enable this option to enable stock management at variation level', 'woocommerce' ); ?>"> <?php esc_html_e( 'Manage stock?', 'woocommerce' ); ?> <input type="checkbox" class="checkbox variable_manage_stock" name="variable_manage_stock[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_manage_stock(), true ); // Use view context so 'parent' is considered. ?> /> </label> <?php endif; ?> <?php do_action( 'woocommerce_variation_options', $loop, $variation_data, $variation ); ?> </p> <div class="variable_pricing"> <?php $label = sprintf( /* translators: %s: currency symbol */ __( 'Regular price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); woocommerce_wp_text_input( array( 'id' => "variable_regular_price_{$loop}", 'name' => "variable_regular_price[{$loop}]", 'value' => wc_format_localized_price( $variation_object->get_regular_price( 'edit' ) ), 'label' => $label, 'data_type' => 'price', 'wrapper_class' => 'form-row form-row-first', 'placeholder' => __( 'Variation price (required)', 'woocommerce' ), ) ); $label = sprintf( /* translators: %s: currency symbol */ __( 'Sale price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); woocommerce_wp_text_input( array( 'id' => "variable_sale_price{$loop}", 'name' => "variable_sale_price[{$loop}]", 'value' => wc_format_localized_price( $variation_object->get_sale_price( 'edit' ) ), 'data_type' => 'price', 'label' => $label . ' <a href="#" class="sale_schedule">' . esc_html__( 'Schedule', 'woocommerce' ) . '</a><a href="#" class="cancel_sale_schedule hidden">' . esc_html__( 'Cancel schedule', 'woocommerce' ) . '</a>', 'wrapper_class' => 'form-row form-row-last', ) ); $sale_price_dates_from_timestamp = $variation_object->get_date_on_sale_from( 'edit' ) ? $variation_object->get_date_on_sale_from( 'edit' )->getOffsetTimestamp() : false; $sale_price_dates_to_timestamp = $variation_object->get_date_on_sale_to( 'edit' ) ? $variation_object->get_date_on_sale_to( 'edit' )->getOffsetTimestamp() : false; $sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : ''; $sale_price_dates_to = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : ''; echo '<div class="form-field sale_price_dates_fields hidden"> <p class="form-row form-row-first"> <label>' . esc_html__( 'Sale start date', 'woocommerce' ) . '</label> <input type="text" class="sale_price_dates_from" name="variable_sale_price_dates_from[' . esc_attr( $loop ) . ']" value="' . esc_attr( $sale_price_dates_from ) . '" placeholder="' . esc_attr_x( 'From…', 'placeholder', 'woocommerce' ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" /> </p> <p class="form-row form-row-last"> <label>' . esc_html__( 'Sale end date', 'woocommerce' ) . '</label> <input type="text" class="sale_price_dates_to" name="variable_sale_price_dates_to[' . esc_attr( $loop ) . ']" value="' . esc_attr( $sale_price_dates_to ) . '" placeholder="' . esc_attr_x( 'To…', 'placeholder', 'woocommerce' ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" /> </p> </div>'; /** * Variation options pricing action. * * @since 2.5.0 * * @param int $loop Position in the loop. * @param array $variation_data Variation data. * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_options_pricing', $loop, $variation_data, $variation ); ?> </div> <?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?> <div class="show_if_variation_manage_stock" style="display: none;"> <?php woocommerce_wp_text_input( array( 'id' => "variable_stock{$loop}", 'name' => "variable_stock[{$loop}]", 'value' => wc_stock_amount( $variation_object->get_stock_quantity( 'edit' ) ), 'label' => __( 'Stock quantity', 'woocommerce' ), 'desc_tip' => true, 'description' => __( "Enter a number to set stock quantity at the variation level. Use a variation's 'Manage stock?' check box above to enable/disable stock management at the variation level.", 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => 'any', ), 'data_type' => 'stock', 'wrapper_class' => 'form-row form-row-first', ) ); echo '<input type="hidden" name="variable_original_stock[' . esc_attr( $loop ) . ']" value="' . esc_attr( wc_stock_amount( $variation_object->get_stock_quantity( 'edit' ) ) ) . '" />'; woocommerce_wp_select( array( 'id' => "variable_backorders{$loop}", 'name' => "variable_backorders[{$loop}]", 'value' => $variation_object->get_backorders( 'edit' ), 'label' => __( 'Allow backorders?', 'woocommerce' ), 'options' => wc_get_product_backorder_options(), 'desc_tip' => true, 'description' => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'woocommerce' ), 'wrapper_class' => 'form-row form-row-last', ) ); $low_stock_placeholder = ( $product_object->get_manage_stock() && '' !== $product_object->get_low_stock_amount() ) ? sprintf( /* translators: %d: Amount of stock left */ esc_attr__( 'Parent product\'s threshold (%d)', 'woocommerce' ), esc_attr( $product_object->get_low_stock_amount() ) ) : sprintf( /* translators: %d: Amount of stock left */ esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ), esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) ) ); woocommerce_wp_text_input( array( 'id' => "variable_low_stock_amount{$loop}", 'name' => "variable_low_stock_amount[{$loop}]", 'value' => $variation_object->get_low_stock_amount( 'edit' ), 'placeholder' => $low_stock_placeholder, 'label' => __( 'Low stock threshold', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'When variation stock reaches this amount you will be notified by email. The default value for all variations can be set in the product Inventory tab. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => 'any', ), 'wrapper_class' => 'form-row', ) ); /** * Variation options inventory action. * * @since 2.5.0 * * @param int $loop Position in the loop. * @param array $variation_data Variation data. * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_options_inventory', $loop, $variation_data, $variation ); ?> </div> <?php endif; ?> <div> <?php woocommerce_wp_select( array( 'id' => "variable_stock_status{$loop}", 'name' => "variable_stock_status[{$loop}]", 'value' => $variation_object->get_stock_status( 'edit' ), 'label' => __( 'Stock status', 'woocommerce' ), 'options' => wc_get_product_stock_status_options(), 'desc_tip' => true, 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'wrapper_class' => 'form-row form-row-full variable_stock_status', ) ); if ( wc_product_weight_enabled() ) { $label = sprintf( /* translators: %s: weight unit */ __( 'Weight (%s)', 'woocommerce' ), esc_html( get_option( 'woocommerce_weight_unit' ) ) ); woocommerce_wp_text_input( array( 'id' => "variable_weight{$loop}", 'name' => "variable_weight[{$loop}]", 'value' => wc_format_localized_decimal( $variation_object->get_weight( 'edit' ) ), 'placeholder' => wc_format_localized_decimal( $product_object->get_weight() ), 'label' => $label, 'desc_tip' => true, 'description' => __( 'Weight in decimal form', 'woocommerce' ), 'type' => 'text', 'data_type' => 'decimal', 'wrapper_class' => 'form-row form-row-first hide_if_variation_virtual', ) ); } if ( wc_product_dimensions_enabled() ) { $parent_length = wc_format_localized_decimal( $product_object->get_length() ); $parent_width = wc_format_localized_decimal( $product_object->get_width() ); $parent_height = wc_format_localized_decimal( $product_object->get_height() ); ?> <p class="form-field form-row dimensions_field hide_if_variation_virtual form-row-last"> <label for="product_length"> <?php printf( /* translators: %s: dimension unit */ esc_html__( 'Dimensions (L×W×H) (%s)', 'woocommerce' ), esc_html( get_option( 'woocommerce_dimension_unit' ) ) ); ?> </label> <?php echo wc_help_tip( __( 'Length x width x height in decimal form', 'woocommerce' ) ); ?> <span class="wrap"> <input id="product_length" placeholder="<?php echo $parent_length ? esc_attr( $parent_length ) : esc_attr__( 'Length', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="variable_length[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_length( 'edit' ) ) ); ?>" /> <input placeholder="<?php echo $parent_width ? esc_attr( $parent_width ) : esc_attr__( 'Width', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="variable_width[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_width( 'edit' ) ) ); ?>" /> <input placeholder="<?php echo $parent_height ? esc_attr( $parent_height ) : esc_attr__( 'Height', 'woocommerce' ); ?>" class="input-text wc_input_decimal last" size="6" type="text" name="variable_height[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_height( 'edit' ) ) ); ?>" /> </span> </p> <?php } /** * Variation options dimensions action. * * @since 2.5.0 * * @param int $loop Position in the loop. * @param array $variation_data Variation data. * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_options_dimensions', $loop, $variation_data, $variation ); ?> </div> <div> <p class="form-row hide_if_variation_virtual form-row-full"> <label><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></label> <?php wp_dropdown_categories( array( 'taxonomy' => 'product_shipping_class', 'hide_empty' => 0, 'show_option_none' => __( 'Same as parent', 'woocommerce' ), 'name' => 'variable_shipping_class[' . $loop . ']', 'id' => '', 'selected' => $variation_object->get_shipping_class_id( 'edit' ), ) ); ?> </p> <?php if ( wc_tax_enabled() ) { woocommerce_wp_select( array( 'id' => "variable_tax_class{$loop}", 'name' => "variable_tax_class[{$loop}]", 'value' => $variation_object->get_tax_class( 'edit' ), 'label' => __( 'Tax class', 'woocommerce' ), 'options' => array( 'parent' => __( 'Same as parent', 'woocommerce' ) ) + wc_get_product_tax_class_options(), 'desc_tip' => 'true', 'description' => __( 'Choose a tax class for this product. Tax classes are used to apply different tax rates specific to certain types of product.', 'woocommerce' ), 'wrapper_class' => 'form-row form-row-full', ) ); /** * Variation options tax action. * * @since 2.5.0 * * @param int $loop Position in the loop. * @param array $variation_data Variation data. * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_options_tax', $loop, $variation_data, $variation ); } ?> </div> <div> <?php woocommerce_wp_textarea_input( array( 'id' => "variable_description{$loop}", 'name' => "variable_description[{$loop}]", 'value' => $variation_object->get_description( 'edit' ), 'label' => __( 'Description', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'Enter an optional description for this variation.', 'woocommerce' ), 'wrapper_class' => 'form-row form-row-full', ) ); ?> </div> <div class="show_if_variation_downloadable" style="display: none;"> <div class="form-row form-row-full downloadable_files"> <label><?php esc_html_e( 'Downloadable files', 'woocommerce' ); ?></label> <table class="widefat"> <thead> <div> <th><?php esc_html_e( 'Name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the name of the download shown to the customer.', 'woocommerce' ) ); ?></th> <th colspan="2"><?php esc_html_e( 'File URL', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the URL or absolute path to the file which customers will get access to. URLs entered here should already be encoded.', 'woocommerce' ) ); ?></th> <th> </th> </div> </thead> <tbody> <?php $downloads = $variation_object->get_downloads( 'edit' ); if ( $downloads ) { foreach ( $downloads as $key => $file ) { include __DIR__ . '/html-product-variation-download.php'; } } ?> </tbody> <tfoot> <div> <th colspan="4"> <a href="#" class="button insert" data-row=" <?php $key = ''; $file = array( 'file' => '', 'name' => '', ); ob_start(); require __DIR__ . '/html-product-variation-download.php'; echo esc_attr( ob_get_clean() ); ?> "><?php esc_html_e( 'Add file', 'woocommerce' ); ?></a> </th> </div> </tfoot> </table> </div> </div> <div class="show_if_variation_downloadable" style="display: none;"> <?php woocommerce_wp_text_input( array( 'id' => "variable_download_limit{$loop}", 'name' => "variable_download_limit[{$loop}]", 'value' => $variation_object->get_download_limit( 'edit' ) < 0 ? '' : $variation_object->get_download_limit( 'edit' ), 'label' => __( 'Download limit', 'woocommerce' ), 'placeholder' => __( 'Unlimited', 'woocommerce' ), 'description' => __( 'Leave blank for unlimited re-downloads.', 'woocommerce' ), 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => array( 'step' => '1', 'min' => '0', ), 'wrapper_class' => 'form-row form-row-first', ) ); woocommerce_wp_text_input( array( 'id' => "variable_download_expiry{$loop}", 'name' => "variable_download_expiry[{$loop}]", 'value' => $variation_object->get_download_expiry( 'edit' ) < 0 ? '' : $variation_object->get_download_expiry( 'edit' ), 'label' => __( 'Download expiry', 'woocommerce' ), 'placeholder' => __( 'Never', 'woocommerce' ), 'description' => __( 'Enter the number of days before a download link expires, or leave blank.', 'woocommerce' ), 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => array( 'step' => '1', 'min' => '0', ), 'wrapper_class' => 'form-row form-row-last', ) ); /** * Variation options download action. * * @since 2.5.0 * * @param int $loop Position in the loop. * @param array $variation_data Variation data. * @param WP_Post $variation Post data. */ do_action( 'woocommerce_variation_options_download', $loop, $variation_data, $variation ); ?> </div> <?php do_action( 'woocommerce_product_after_variable_attributes', $loop, $variation_data, $variation ); ?> </div> </div> </div> includes/admin/meta-boxes/views/html-order-refund.php 0000644 00000004655 15132754524 0016734 0 ustar 00 <?php /** * Show order refund * * @var object $refund The refund object. * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } $who_refunded = new WP_User( $refund->get_refunded_by() ); ?> <tr class="refund <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_refund_id="<?php echo esc_attr( $refund->get_id() ); ?>"> <td class="thumb"><div></div></td> <td class="name"> <?php if ( $who_refunded->exists() ) { printf( /* translators: 1: refund id 2: refund date 3: username */ esc_html__( 'Refund #%1$s - %2$s by %3$s', 'woocommerce' ), esc_html( $refund->get_id() ), esc_html( wc_format_datetime( $refund->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ) ), sprintf( '<abbr class="refund_by" title="%1$s">%2$s</abbr>', /* translators: 1: ID who refunded */ sprintf( esc_attr__( 'ID: %d', 'woocommerce' ), absint( $who_refunded->ID ) ), esc_html( $who_refunded->display_name ) ) ); } else { printf( /* translators: 1: refund id 2: refund date */ esc_html__( 'Refund #%1$s - %2$s', 'woocommerce' ), esc_html( $refund->get_id() ), esc_html( wc_format_datetime( $refund->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ) ) ); } ?> <?php if ( $refund->get_reason() ) : ?> <p class="description"><?php echo esc_html( $refund->get_reason() ); ?></p> <?php endif; ?> <input type="hidden" class="order_refund_id" name="order_refund_id[]" value="<?php echo esc_attr( $refund->get_id() ); ?>" /> <?php do_action( 'woocommerce_after_order_refund_item_name', $refund ); ?> </td> <?php do_action( 'woocommerce_admin_order_item_values', null, $refund, $refund->get_id() ); ?> <td class="item_cost" width="1%"> </td> <td class="quantity" width="1%"> </td> <td class="line_cost" width="1%"> <div class="view"> <?php echo wp_kses_post( wc_price( '-' . $refund->get_amount(), array( 'currency' => $refund->get_currency() ) ) ); ?> </div> </td> <?php if ( wc_tax_enabled() ) : $total_taxes = count( $order_taxes ); ?> <?php for ( $i = 0; $i < $total_taxes; $i++ ) : ?> <td class="line_tax" width="1%"></td> <?php endfor; ?> <?php endif; ?> <td class="wc-order-edit-line-item"> <div class="wc-order-edit-line-item-actions"> <a class="delete_refund" href="#"></a> </div> </td> </tr> includes/admin/meta-boxes/views/html-order-shipping.php 0000644 00000012141 15132754524 0017257 0 ustar 00 <?php /** * Shows a shipping line * * @package WooCommerce\Admin * * @var object $item The item being displayed * @var int $item_id The id of the item being displayed * * @package WooCommerce\Admin\Views */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <tr class="shipping <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>"> <td class="thumb"><div></div></td> <td class="name"> <div class="view"> <?php echo esc_html( $item->get_name() ? $item->get_name() : __( 'Shipping', 'woocommerce' ) ); ?> </div> <div class="edit" style="display: none;"> <input type="hidden" name="shipping_method_id[]" value="<?php echo esc_attr( $item_id ); ?>" /> <input type="text" class="shipping_method_name" placeholder="<?php esc_attr_e( 'Shipping name', 'woocommerce' ); ?>" name="shipping_method_title[<?php echo esc_attr( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_name() ); ?>" /> <select class="shipping_method" name="shipping_method[<?php echo esc_attr( $item_id ); ?>]"> <optgroup label="<?php esc_attr_e( 'Shipping method', 'woocommerce' ); ?>"> <option value=""><?php esc_html_e( 'N/A', 'woocommerce' ); ?></option> <?php $found_method = false; foreach ( $shipping_methods as $method ) { $is_active = $item->get_method_id() === $method->id; echo '<option value="' . esc_attr( $method->id ) . '" ' . selected( true, $is_active, false ) . '>' . esc_html( $method->get_method_title() ) . '</option>'; if ( $is_active ) { $found_method = true; } } if ( ! $found_method && $item->get_method_id() ) { echo '<option value="' . esc_attr( $item->get_method_id() ) . '" selected="selected">' . esc_html__( 'Other', 'woocommerce' ) . '</option>'; } else { echo '<option value="other">' . esc_html__( 'Other', 'woocommerce' ) . '</option>'; } ?> </optgroup> </select> </div> <?php do_action( 'woocommerce_before_order_itemmeta', $item_id, $item, null ); ?> <?php require __DIR__ . '/html-order-item-meta.php'; ?> <?php do_action( 'woocommerce_after_order_itemmeta', $item_id, $item, null ); ?> </td> <?php do_action( 'woocommerce_admin_order_item_values', null, $item, absint( $item_id ) ); ?> <td class="item_cost" width="1%"> </td> <td class="quantity" width="1%"> </td> <td class="line_cost" width="1%"> <div class="view"> <?php echo wp_kses_post( wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ) ); $refunded = $order->get_total_refunded_for_item( $item_id, 'shipping' ); if ( $refunded ) { echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' ); } ?> </div> <div class="edit" style="display: none;"> <input type="text" name="shipping_cost[<?php echo esc_attr( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" /> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" /> </div> </td> <?php $tax_data = $item->get_taxes(); if ( $tax_data && wc_tax_enabled() ) { foreach ( $order_taxes as $tax_item ) { $tax_item_id = $tax_item->get_rate_id(); $tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : ''; ?> <td class="line_tax" width="1%"> <div class="view"> <?php echo wp_kses_post( ( '' !== $tax_item_total ) ? wc_price( $tax_item_total, array( 'currency' => $order->get_currency() ) ) : '–' ); $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' ); if ( $refunded ) { echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' ); } ?> </div> <div class="edit" style="display: none;"> <input type="text" name="shipping_taxes[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo ( isset( $tax_item_total ) ) ? esc_attr( wc_format_localized_price( $tax_item_total ) ) : ''; ?>" class="line_tax wc_input_price" /> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" /> </div> </td> <?php } } ?> <td class="wc-order-edit-line-item"> <?php if ( $order->is_editable() ) : ?> <div class="wc-order-edit-line-item-actions"> <a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a> </div> <?php endif; ?> </td> </tr> includes/admin/meta-boxes/views/html-product-data-variations.php 0000644 00000024751 15132754524 0021103 0 ustar 00 <?php /** * Product data variations * * @package WooCommerce\Admin\Metaboxes\Views */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="variable_product_options" class="panel wc-metaboxes-wrapper hidden"> <div id="variable_product_options_inner"> <?php if ( ! count( $variation_attributes ) ) : ?> <div id="message" class="inline notice woocommerce-message"> <p><?php echo wp_kses_post( __( 'Before you can add a variation you need to add some variation attributes on the <strong>Attributes</strong> tab.', 'woocommerce' ) ); ?></p> <p><a class="button-primary" href="<?php echo esc_url( apply_filters( 'woocommerce_docs_url', 'https://docs.woocommerce.com/document/variable-product/', 'product-variations' ) ); ?>" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p> </div> <?php else : ?> <div class="toolbar toolbar-variations-defaults"> <div class="variations-defaults"> <strong><?php esc_html_e( 'Default Form Values', 'woocommerce' ); ?>: <?php echo wc_help_tip( __( 'These are the attributes that will be pre-selected on the frontend.', 'woocommerce' ) ); ?></strong> <?php foreach ( $variation_attributes as $attribute ) { $selected_value = isset( $default_attributes[ sanitize_title( $attribute->get_name() ) ] ) ? $default_attributes[ sanitize_title( $attribute->get_name() ) ] : ''; ?> <select name="default_attribute_<?php echo esc_attr( sanitize_title( $attribute->get_name() ) ); ?>" data-current="<?php echo esc_attr( $selected_value ); ?>"> <?php /* translators: WooCommerce attribute label */ ?> <option value=""><?php echo esc_html( sprintf( __( 'No default %s…', 'woocommerce' ), wc_attribute_label( $attribute->get_name() ) ) ); ?></option> <?php if ( $attribute->is_taxonomy() ) : ?> <?php foreach ( $attribute->get_terms() as $option ) : ?> <option <?php selected( $selected_value, $option->slug ); ?> value="<?php echo esc_attr( $option->slug ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option->name, $option, $attribute->get_name(), $product_object ) ); ?></option> <?php endforeach; ?> <?php else : ?> <?php foreach ( $attribute->get_options() as $option ) : ?> <option <?php selected( $selected_value, $option ); ?> value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute->get_name(), $product_object ) ); ?></option> <?php endforeach; ?> <?php endif; ?> </select> <?php } ?> </div> <div class="clear"></div> </div> <?php do_action( 'woocommerce_variable_product_before_variations' ); ?> <div class="toolbar toolbar-top"> <select id="field_to_edit" class="variation_actions"> <option data-global="true" value="add_variation"><?php esc_html_e( 'Add variation', 'woocommerce' ); ?></option> <option data-global="true" value="link_all_variations"><?php esc_html_e( 'Create variations from all attributes', 'woocommerce' ); ?></option> <option value="delete_all"><?php esc_html_e( 'Delete all variations', 'woocommerce' ); ?></option> <optgroup label="<?php esc_attr_e( 'Status', 'woocommerce' ); ?>"> <option value="toggle_enabled"><?php esc_html_e( 'Toggle "Enabled"', 'woocommerce' ); ?></option> <option value="toggle_downloadable"><?php esc_html_e( 'Toggle "Downloadable"', 'woocommerce' ); ?></option> <option value="toggle_virtual"><?php esc_html_e( 'Toggle "Virtual"', 'woocommerce' ); ?></option> </optgroup> <optgroup label="<?php esc_attr_e( 'Pricing', 'woocommerce' ); ?>"> <option value="variable_regular_price"><?php esc_html_e( 'Set regular prices', 'woocommerce' ); ?></option> <option value="variable_regular_price_increase"><?php esc_html_e( 'Increase regular prices (fixed amount or percentage)', 'woocommerce' ); ?></option> <option value="variable_regular_price_decrease"><?php esc_html_e( 'Decrease regular prices (fixed amount or percentage)', 'woocommerce' ); ?></option> <option value="variable_sale_price"><?php esc_html_e( 'Set sale prices', 'woocommerce' ); ?></option> <option value="variable_sale_price_increase"><?php esc_html_e( 'Increase sale prices (fixed amount or percentage)', 'woocommerce' ); ?></option> <option value="variable_sale_price_decrease"><?php esc_html_e( 'Decrease sale prices (fixed amount or percentage)', 'woocommerce' ); ?></option> <option value="variable_sale_schedule"><?php esc_html_e( 'Set scheduled sale dates', 'woocommerce' ); ?></option> </optgroup> <optgroup label="<?php esc_attr_e( 'Inventory', 'woocommerce' ); ?>"> <option value="toggle_manage_stock"><?php esc_html_e( 'Toggle "Manage stock"', 'woocommerce' ); ?></option> <option value="variable_stock"><?php esc_html_e( 'Stock', 'woocommerce' ); ?></option> <option value="variable_stock_status_instock"><?php esc_html_e( 'Set Status - In stock', 'woocommerce' ); ?></option> <option value="variable_stock_status_outofstock"><?php esc_html_e( 'Set Status - Out of stock', 'woocommerce' ); ?></option> <option value="variable_stock_status_onbackorder"><?php esc_html_e( 'Set Status - On backorder', 'woocommerce' ); ?></option> <option value="variable_low_stock_amount"><?php esc_html_e( 'Low stock threshold', 'woocommerce' ); ?></option> </optgroup> <optgroup label="<?php esc_attr_e( 'Shipping', 'woocommerce' ); ?>"> <option value="variable_length"><?php esc_html_e( 'Length', 'woocommerce' ); ?></option> <option value="variable_width"><?php esc_html_e( 'Width', 'woocommerce' ); ?></option> <option value="variable_height"><?php esc_html_e( 'Height', 'woocommerce' ); ?></option> <option value="variable_weight"><?php esc_html_e( 'Weight', 'woocommerce' ); ?></option> </optgroup> <optgroup label="<?php esc_attr_e( 'Downloadable products', 'woocommerce' ); ?>"> <option value="variable_download_limit"><?php esc_html_e( 'Download limit', 'woocommerce' ); ?></option> <option value="variable_download_expiry"><?php esc_html_e( 'Download expiry', 'woocommerce' ); ?></option> </optgroup> <?php do_action( 'woocommerce_variable_product_bulk_edit_actions' ); ?> </select> <a class="button bulk_edit do_variation_action"><?php esc_html_e( 'Go', 'woocommerce' ); ?></a> <div class="variations-pagenav"> <?php /* translators: variations count */ ?> <span class="displaying-num"><?php echo esc_html( sprintf( _n( '%s item', '%s items', $variations_count, 'woocommerce' ), $variations_count ) ); ?></span> <span class="expand-close"> (<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>) </span> <span class="pagination-links"> <a class="first-page disabled" title="<?php esc_attr_e( 'Go to the first page', 'woocommerce' ); ?>" href="#">«</a> <a class="prev-page disabled" title="<?php esc_attr_e( 'Go to the previous page', 'woocommerce' ); ?>" href="#">‹</a> <span class="paging-select"> <label for="current-page-selector-1" class="screen-reader-text"><?php esc_html_e( 'Select Page', 'woocommerce' ); ?></label> <select class="page-selector" id="current-page-selector-1" title="<?php esc_attr_e( 'Current page', 'woocommerce' ); ?>"> <?php for ( $i = 1; $i <= $variations_total_pages; $i++ ) : ?> <option value="<?php echo $i; // WPCS: XSS ok. ?>"><?php echo $i; // WPCS: XSS ok. ?></option> <?php endfor; ?> </select> <?php echo esc_html_x( 'of', 'number of pages', 'woocommerce' ); ?> <span class="total-pages"><?php echo esc_html( $variations_total_pages ); ?></span> </span> <a class="next-page" title="<?php esc_attr_e( 'Go to the next page', 'woocommerce' ); ?>" href="#">›</a> <a class="last-page" title="<?php esc_attr_e( 'Go to the last page', 'woocommerce' ); ?>" href="#">»</a> </span> </div> <div class="clear"></div> </div> <div class="woocommerce_variations wc-metaboxes" data-attributes="<?php echo wc_esc_json( wp_json_encode( wc_list_pluck( $variation_attributes, 'get_data' ) ) ); // WPCS: XSS ok. ?>" data-total="<?php echo esc_attr( $variations_count ); ?>" data-total_pages="<?php echo esc_attr( $variations_total_pages ); ?>" data-page="1" data-edited="false"></div> <div class="toolbar"> <button type="button" class="button-primary save-variation-changes" disabled="disabled"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button> <button type="button" class="button cancel-variation-changes" disabled="disabled"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button> <div class="variations-pagenav"> <?php /* translators: variations count*/ ?> <span class="displaying-num"><?php echo esc_html( sprintf( _n( '%s item', '%s items', $variations_count, 'woocommerce' ), $variations_count ) ); ?></span> <span class="expand-close"> (<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>) </span> <span class="pagination-links"> <a class="first-page disabled" title="<?php esc_attr_e( 'Go to the first page', 'woocommerce' ); ?>" href="#">«</a> <a class="prev-page disabled" title="<?php esc_attr_e( 'Go to the previous page', 'woocommerce' ); ?>" href="#">‹</a> <span class="paging-select"> <label for="current-page-selector-1" class="screen-reader-text"><?php esc_html_e( 'Select Page', 'woocommerce' ); ?></label> <select class="page-selector" id="current-page-selector-1" title="<?php esc_attr_e( 'Current page', 'woocommerce' ); ?>"> <?php for ( $i = 1; $i <= $variations_total_pages; $i++ ) : ?> <option value="<?php echo $i; // WPCS: XSS ok. ?>"><?php echo $i; // WPCS: XSS ok. ?></option> <?php endfor; ?> </select> <?php echo esc_html_x( 'of', 'number of pages', 'woocommerce' ); ?> <span class="total-pages"><?php echo esc_html( $variations_total_pages ); ?></span> </span> <a class="next-page" title="<?php esc_attr_e( 'Go to the next page', 'woocommerce' ); ?>" href="#">›</a> <a class="last-page" title="<?php esc_attr_e( 'Go to the last page', 'woocommerce' ); ?>" href="#">»</a> </span> </div> <div class="clear"></div> </div> <?php endif; ?> </div> </div> includes/admin/meta-boxes/views/html-order-fee.php 0000644 00000007742 15132754524 0016210 0 ustar 00 <?php /** * Shows an order item fee * * @var object $item The item being displayed * @var int $item_id The id of the item being displayed */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <tr class="fee <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>"> <td class="thumb"><div></div></td> <td class="name"> <div class="view"> <?php echo esc_html( $item->get_name() ? $item->get_name() : __( 'Fee', 'woocommerce' ) ); ?> </div> <div class="edit" style="display: none;"> <input type="text" placeholder="<?php esc_attr_e( 'Fee name', 'woocommerce' ); ?>" name="order_item_name[<?php echo absint( $item_id ); ?>]" value="<?php echo ( $item->get_name() ) ? esc_attr( $item->get_name() ) : ''; ?>" /> <input type="hidden" class="order_item_id" name="order_item_id[]" value="<?php echo esc_attr( $item_id ); ?>" /> <input type="hidden" name="order_item_tax_class[<?php echo absint( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_tax_class() ); ?>" /> </div> <?php do_action( 'woocommerce_after_order_fee_item_name', $item_id, $item, null ); ?> </td> <?php do_action( 'woocommerce_admin_order_item_values', null, $item, absint( $item_id ) ); ?> <td class="item_cost" width="1%"> </td> <td class="quantity" width="1%"> </td> <td class="line_cost" width="1%"> <div class="view"> <?php echo wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); if ( $refunded = $order->get_total_refunded_for_item( $item_id, 'fee' ) ) { echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; } ?> </div> <div class="edit" style="display: none;"> <input type="text" name="line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" /> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" /> </div> </td> <?php if ( ( $tax_data = $item->get_taxes() ) && wc_tax_enabled() ) { foreach ( $order_taxes as $tax_item ) { $tax_item_id = $tax_item->get_rate_id(); $tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : ''; ?> <td class="line_tax" width="1%"> <div class="view"> <?php echo ( '' !== $tax_item_total ) ? wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ) : '–'; if ( $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'fee' ) ) { echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; } ?> </div> <div class="edit" style="display: none;"> <input type="text" name="line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo ( isset( $tax_item_total ) ) ? esc_attr( wc_format_localized_price( $tax_item_total ) ) : ''; ?>" class="line_tax wc_input_price" /> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" /> </div> </td> <?php } } ?> <td class="wc-order-edit-line-item"> <?php if ( $order->is_editable() ) : ?> <div class="wc-order-edit-line-item-actions"> <a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a> </div> <?php endif; ?> </td> </tr> includes/admin/meta-boxes/views/html-order-items.php 0000644 00000051715 15132754524 0016571 0 ustar 00 <?php /** * Order items HTML for meta box. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; global $wpdb; $payment_gateway = wc_get_payment_gateway_by_order( $order ); $line_items = $order->get_items( apply_filters( 'woocommerce_admin_order_item_types', 'line_item' ) ); $discounts = $order->get_items( 'discount' ); $line_items_fee = $order->get_items( 'fee' ); $line_items_shipping = $order->get_items( 'shipping' ); if ( wc_tax_enabled() ) { $order_taxes = $order->get_taxes(); $tax_classes = WC_Tax::get_tax_classes(); $classes_options = wc_get_product_tax_class_options(); $show_tax_columns = count( $order_taxes ) === 1; } ?> <div class="woocommerce_order_items_wrapper wc-order-items-editable"> <table cellpadding="0" cellspacing="0" class="woocommerce_order_items"> <thead> <tr> <th class="item sortable" colspan="2" data-sort="string-ins"><?php esc_html_e( 'Item', 'woocommerce' ); ?></th> <?php do_action( 'woocommerce_admin_order_item_headers', $order ); ?> <th class="item_cost sortable" data-sort="float"><?php esc_html_e( 'Cost', 'woocommerce' ); ?></th> <th class="quantity sortable" data-sort="int"><?php esc_html_e( 'Qty', 'woocommerce' ); ?></th> <th class="line_cost sortable" data-sort="float"><?php esc_html_e( 'Total', 'woocommerce' ); ?></th> <?php if ( ! empty( $order_taxes ) ) : foreach ( $order_taxes as $tax_id => $tax_item ) : $tax_class = wc_get_tax_class_by_tax_id( $tax_item['rate_id'] ); $tax_class_name = isset( $classes_options[ $tax_class ] ) ? $classes_options[ $tax_class ] : __( 'Tax', 'woocommerce' ); $column_label = ! empty( $tax_item['label'] ) ? $tax_item['label'] : __( 'Tax', 'woocommerce' ); /* translators: %1$s: tax item name %2$s: tax class name */ $column_tip = sprintf( esc_html__( '%1$s (%2$s)', 'woocommerce' ), $tax_item['name'], $tax_class_name ); ?> <th class="line_tax tips" data-tip="<?php echo esc_attr( $column_tip ); ?>"> <?php echo esc_attr( $column_label ); ?> <input type="hidden" class="order-tax-id" name="order_taxes[<?php echo esc_attr( $tax_id ); ?>]" value="<?php echo esc_attr( $tax_item['rate_id'] ); ?>"> <?php if ( $order->is_editable() ) : ?> <a class="delete-order-tax" href="#" data-rate_id="<?php echo esc_attr( $tax_id ); ?>"></a> <?php endif; ?> </th> <?php endforeach; endif; ?> <th class="wc-order-edit-line-item" width="1%"> </th> </tr> </thead> <tbody id="order_line_items"> <?php foreach ( $line_items as $item_id => $item ) { do_action( 'woocommerce_before_order_item_' . $item->get_type() . '_html', $item_id, $item, $order ); include __DIR__ . '/html-order-item.php'; do_action( 'woocommerce_order_item_' . $item->get_type() . '_html', $item_id, $item, $order ); } do_action( 'woocommerce_admin_order_items_after_line_items', $order->get_id() ); ?> </tbody> <tbody id="order_fee_line_items"> <?php foreach ( $line_items_fee as $item_id => $item ) { include __DIR__ . '/html-order-fee.php'; } do_action( 'woocommerce_admin_order_items_after_fees', $order->get_id() ); ?> </tbody> <tbody id="order_shipping_line_items"> <?php $shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array(); foreach ( $line_items_shipping as $item_id => $item ) { include __DIR__ . '/html-order-shipping.php'; } do_action( 'woocommerce_admin_order_items_after_shipping', $order->get_id() ); ?> </tbody> <tbody id="order_refunds"> <?php $refunds = $order->get_refunds(); if ( $refunds ) { foreach ( $refunds as $refund ) { include __DIR__ . '/html-order-refund.php'; } do_action( 'woocommerce_admin_order_items_after_refunds', $order->get_id() ); } ?> </tbody> </table> </div> <div class="wc-order-data-row wc-order-totals-items wc-order-items-editable"> <?php $coupons = $order->get_items( 'coupon' ); if ( $coupons ) : ?> <div class="wc-used-coupons"> <ul class="wc_coupon_list"> <li><strong><?php esc_html_e( 'Coupon(s)', 'woocommerce' ); ?></strong></li> <?php foreach ( $coupons as $item_id => $item ) : $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' LIMIT 1;", $item->get_code() ) ); // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited $class = $order->is_editable() ? 'code editable' : 'code'; ?> <li class="<?php echo esc_attr( $class ); ?>"> <?php if ( $post_id ) : ?> <?php $post_url = apply_filters( 'woocommerce_admin_order_item_coupon_url', add_query_arg( array( 'post' => $post_id, 'action' => 'edit', ), admin_url( 'post.php' ) ), $item, $order ); ?> <a href="<?php echo esc_url( $post_url ); ?>" class="tips" data-tip="<?php echo esc_attr( wc_price( $item->get_discount(), array( 'currency' => $order->get_currency() ) ) ); ?>"> <span><?php echo esc_html( $item->get_code() ); ?></span> </a> <?php else : ?> <span class="tips" data-tip="<?php echo esc_attr( wc_price( $item->get_discount(), array( 'currency' => $order->get_currency() ) ) ); ?>"> <span><?php echo esc_html( $item->get_code() ); ?></span> </span> <?php endif; ?> <?php if ( $order->is_editable() ) : ?> <a class="remove-coupon" href="javascript:void(0)" aria-label="Remove" data-code="<?php echo esc_attr( $item->get_code() ); ?>"></a> <?php endif; ?> </li> <?php endforeach; ?> </ul> </div> <?php endif; ?> <table class="wc-order-totals"> <tr> <td class="label"><?php esc_html_e( 'Items Subtotal:', 'woocommerce' ); ?></td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_subtotal(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php if ( 0 < $order->get_total_discount() ) : ?> <tr> <td class="label"><?php esc_html_e( 'Coupon(s):', 'woocommerce' ); ?></td> <td width="1%"></td> <td class="total">- <?php echo wc_price( $order->get_total_discount(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php endif; ?> <?php if ( 0 < $order->get_total_fees() ) : ?> <tr> <td class="label"><?php esc_html_e( 'Fees:', 'woocommerce' ); ?></td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_total_fees(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php endif; ?> <?php do_action( 'woocommerce_admin_order_totals_after_discount', $order->get_id() ); ?> <?php if ( $order->get_shipping_methods() ) : ?> <tr> <td class="label"><?php esc_html_e( 'Shipping:', 'woocommerce' ); ?></td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_shipping_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php endif; ?> <?php do_action( 'woocommerce_admin_order_totals_after_shipping', $order->get_id() ); ?> <?php if ( wc_tax_enabled() ) : ?> <?php foreach ( $order->get_tax_totals() as $code => $tax_total ) : ?> <tr> <td class="label"><?php echo esc_html( $tax_total->label ); ?>:</td> <td width="1%"></td> <td class="total"> <?php // We use wc_round_tax_total here because tax may need to be round up or round down depending upon settings, whereas wc_price alone will always round it down. echo wc_price( wc_round_tax_total( $tax_total->amount ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <?php endforeach; ?> <?php endif; ?> <?php do_action( 'woocommerce_admin_order_totals_after_tax', $order->get_id() ); ?> <tr> <td class="label"><?php esc_html_e( 'Order Total', 'woocommerce' ); ?>:</td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> </table> <div class="clear"></div> <?php if ( in_array( $order->get_status(), array( 'processing', 'completed', 'refunded' ), true ) && ! empty( $order->get_date_paid() ) ) : ?> <table class="wc-order-totals" style="border-top: 1px solid #999; margin-top:12px; padding-top:12px"> <tr> <td class="<?php echo $order->get_total_refunded() ? 'label' : 'label label-highlight'; ?>"><?php esc_html_e( 'Paid', 'woocommerce' ); ?>: <br /></td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> <tr> <td> <span class="description"> <?php if ( $order->get_payment_method_title() ) { /* translators: 1: payment date. 2: payment method */ echo esc_html( sprintf( __( '%1$s via %2$s', 'woocommerce' ), $order->get_date_paid()->date_i18n( get_option( 'date_format' ) ), $order->get_payment_method_title() ) ); } else { echo esc_html( $order->get_date_paid()->date_i18n( get_option( 'date_format' ) ) ); } ?> </span> </td> <td colspan="2"></td> </tr> </table> <div class="clear"></div> <?php endif; ?> <?php if ( $order->get_total_refunded() ) : ?> <table class="wc-order-totals" style="border-top: 1px solid #999; margin-top:12px; padding-top:12px"> <tr> <td class="label refunded-total"><?php esc_html_e( 'Refunded', 'woocommerce' ); ?>:</td> <td width="1%"></td> <td class="total refunded-total">-<?php echo wc_price( $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td> </tr> <?php do_action( 'woocommerce_admin_order_totals_after_refunded', $order->get_id() ); ?> <tr> <td class="label label-highlight"><?php esc_html_e( 'Net Payment', 'woocommerce' ); ?>:</td> <td width="1%"></td> <td class="total"> <?php echo wc_price( $order->get_total() - $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </td> </tr> </table> <?php endif; ?> <div class="clear"></div> <table class="wc-order-totals"> <?php do_action( 'woocommerce_admin_order_totals_after_total', $order->get_id() ); ?> </table> <div class="clear"></div> </div> <div class="wc-order-data-row wc-order-bulk-actions wc-order-data-row-toggle"> <p class="add-items"> <?php if ( $order->is_editable() ) : ?> <button type="button" class="button add-line-item"><?php esc_html_e( 'Add item(s)', 'woocommerce' ); ?></button> <?php if ( wc_coupons_enabled() ) : ?> <button type="button" class="button add-coupon"><?php esc_html_e( 'Apply coupon', 'woocommerce' ); ?></button> <?php endif; ?> <?php else : ?> <span class="description"><?php echo wc_help_tip( __( 'To edit this order change the status back to "Pending payment"', 'woocommerce' ) ); ?> <?php esc_html_e( 'This order is no longer editable.', 'woocommerce' ); ?></span> <?php endif; ?> <?php if ( 0 < $order->get_total() - $order->get_total_refunded() || 0 < absint( $order->get_item_count() - $order->get_item_count_refunded() ) ) : ?> <button type="button" class="button refund-items"><?php esc_html_e( 'Refund', 'woocommerce' ); ?></button> <?php endif; ?> <?php // Allow adding custom buttons. do_action( 'woocommerce_order_item_add_action_buttons', $order ); ?> <?php if ( $order->is_editable() ) : ?> <button type="button" class="button button-primary calculate-action"><?php esc_html_e( 'Recalculate', 'woocommerce' ); ?></button> <?php endif; ?> </p> </div> <div class="wc-order-data-row wc-order-add-item wc-order-data-row-toggle" style="display:none;"> <button type="button" class="button add-order-item"><?php esc_html_e( 'Add product(s)', 'woocommerce' ); ?></button> <button type="button" class="button add-order-fee"><?php esc_html_e( 'Add fee', 'woocommerce' ); ?></button> <button type="button" class="button add-order-shipping"><?php esc_html_e( 'Add shipping', 'woocommerce' ); ?></button> <?php if ( wc_tax_enabled() ) : ?> <button type="button" class="button add-order-tax"><?php esc_html_e( 'Add tax', 'woocommerce' ); ?></button> <?php endif; ?> <?php // Allow adding custom buttons. do_action( 'woocommerce_order_item_add_line_buttons', $order ); ?> <button type="button" class="button cancel-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button> <button type="button" class="button button-primary save-action"><?php esc_html_e( 'Save', 'woocommerce' ); ?></button> </div> <?php if ( 0 < $order->get_total() - $order->get_total_refunded() || 0 < absint( $order->get_item_count() - $order->get_item_count_refunded() ) ) : ?> <div class="wc-order-data-row wc-order-refund-items wc-order-data-row-toggle" style="display: none;"> <table class="wc-order-totals"> <?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?> <tr> <td class="label"><label for="restock_refunded_items"><?php esc_html_e( 'Restock refunded items', 'woocommerce' ); ?>:</label></td> <td class="total"><input type="checkbox" id="restock_refunded_items" name="restock_refunded_items" <?php checked( apply_filters( 'woocommerce_restock_refunded_items', true ) ); ?> /></td> </tr> <?php endif; ?> <tr> <td class="label"><?php esc_html_e( 'Amount already refunded', 'woocommerce' ); ?>:</td> <td class="total">-<?php echo wc_price( $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td> </tr> <tr> <td class="label"><?php esc_html_e( 'Total available to refund', 'woocommerce' ); ?>:</td> <td class="total"><?php echo wc_price( $order->get_total() - $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td> </tr> <tr> <td class="label"> <label for="refund_amount"> <?php echo wc_help_tip( __( 'Refund the line items above. This will show the total amount to be refunded', 'woocommerce' ) ); ?> <?php esc_html_e( 'Refund amount', 'woocommerce' ); ?>: </label> </td> <td class="total"> <input type="text" id="refund_amount" name="refund_amount" class="wc_input_price" <?php if ( wc_tax_enabled() ) { // If taxes are enabled, using this refund amount can cause issues due to taxes not being refunded also. // The refunds should be added to the line items, not the order as a whole. echo 'readonly'; } ?> /> <div class="clear"></div> </td> </tr> <tr> <td class="label"> <label for="refund_reason"> <?php echo wc_help_tip( __( 'Note: the refund reason will be visible by the customer.', 'woocommerce' ) ); ?> <?php esc_html_e( 'Reason for refund (optional):', 'woocommerce' ); ?> </label> </td> <td class="total"> <input type="text" id="refund_reason" name="refund_reason" /> <div class="clear"></div> </td> </tr> </table> <div class="clear"></div> <div class="refund-actions"> <?php $refund_amount = '<span class="wc-order-refund-amount">' . wc_price( 0, array( 'currency' => $order->get_currency() ) ) . '</span>'; $gateway_name = false !== $payment_gateway ? ( ! empty( $payment_gateway->method_title ) ? $payment_gateway->method_title : $payment_gateway->get_title() ) : __( 'Payment gateway', 'woocommerce' ); if ( false !== $payment_gateway && $payment_gateway->can_refund_order( $order ) ) { /* translators: refund amount, gateway name */ echo '<button type="button" class="button button-primary do-api-refund">' . sprintf( esc_html__( 'Refund %1$s via %2$s', 'woocommerce' ), wp_kses_post( $refund_amount ), esc_html( $gateway_name ) ) . '</button>'; } ?> <?php /* translators: refund amount */ ?> <button type="button" class="button button-primary do-manual-refund tips" data-tip="<?php esc_attr_e( 'You will need to manually issue a refund through your payment gateway after using this.', 'woocommerce' ); ?>"><?php printf( esc_html__( 'Refund %s manually', 'woocommerce' ), wp_kses_post( $refund_amount ) ); ?></button> <button type="button" class="button cancel-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button> <input type="hidden" id="refunded_amount" name="refunded_amount" value="<?php echo esc_attr( $order->get_total_refunded() ); ?>" /> <div class="clear"></div> </div> </div> <?php endif; ?> <script type="text/template" id="tmpl-wc-modal-add-products"> <div class="wc-backbone-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Add products', 'woocommerce' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text">Close modal panel</span> </button> </header> <article> <form action="" method="post"> <table class="widefat"> <thead> <tr> <th><?php esc_html_e( 'Product', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Quantity', 'woocommerce' ); ?></th> </tr> </thead> <?php $row = ' <td><select class="wc-product-search" name="item_id" data-allow_clear="true" data-display_stock="true" data-exclude_type="variable" data-placeholder="' . esc_attr__( 'Search for a product…', 'woocommerce' ) . '"></select></td> <td><input type="number" step="1" min="0" max="9999" autocomplete="off" name="item_qty" placeholder="1" size="4" class="quantity" /></td>'; ?> <tbody data-row="<?php echo esc_attr( $row ); ?>"> <tr> <?php echo $row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </tr> </tbody> </table> </form> </article> <footer> <div class="inner"> <button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <script type="text/template" id="tmpl-wc-modal-add-tax"> <div class="wc-backbone-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Add tax', 'woocommerce' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text">Close modal panel</span> </button> </header> <article> <form action="" method="post"> <table class="widefat"> <thead> <tr> <th> </th> <th><?php esc_html_e( 'Rate name', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Rate code', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Rate %', 'woocommerce' ); ?></th> </tr> </thead> <?php $rates = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates ORDER BY tax_rate_name LIMIT 100" ); foreach ( $rates as $rate ) { echo ' <tr> <td><input type="radio" id="add_order_tax_' . absint( $rate->tax_rate_id ) . '" name="add_order_tax" value="' . absint( $rate->tax_rate_id ) . '" /></td> <td><label for="add_order_tax_' . absint( $rate->tax_rate_id ) . '">' . esc_html( WC_Tax::get_rate_label( $rate ) ) . '</label></td> <td>' . ( isset( $classes_options[ $rate->tax_rate_class ] ) ? esc_html( $classes_options[ $rate->tax_rate_class ] ) : '-' ) . '</td> <td>' . esc_html( WC_Tax::get_rate_code( $rate ) ) . '</td> <td>' . esc_html( WC_Tax::get_rate_percent( $rate ) ) . '</td> </tr> '; } ?> </table> <?php if ( absint( $wpdb->get_var( "SELECT COUNT(tax_rate_id) FROM {$wpdb->prefix}woocommerce_tax_rates;" ) ) > 100 ) : ?> <p> <label for="manual_tax_rate_id"><?php esc_html_e( 'Or, enter tax rate ID:', 'woocommerce' ); ?></label><br/> <input type="number" name="manual_tax_rate_id" id="manual_tax_rate_id" step="1" placeholder="<?php esc_attr_e( 'Optional', 'woocommerce' ); ?>" /> </p> <?php endif; ?> </form> </article> <footer> <div class="inner"> <button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/admin/meta-boxes/views/html-product-data-advanced.php 0000644 00000003053 15132754524 0020461 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="advanced_product_data" class="panel woocommerce_options_panel hidden"> <div class="options_group hide_if_external hide_if_grouped"> <?php woocommerce_wp_textarea_input( array( 'id' => '_purchase_note', 'value' => $product_object->get_purchase_note( 'edit' ), 'label' => __( 'Purchase note', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'Enter an optional note to send the customer after purchase.', 'woocommerce' ), ) ); ?> </div> <div class="options_group"> <?php woocommerce_wp_text_input( array( 'id' => 'menu_order', 'value' => $product_object->get_menu_order( 'edit' ), 'label' => __( 'Menu order', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'Custom ordering position.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => '1', ), ) ); ?> </div> <?php if ( post_type_supports( 'product', 'comments' ) ) : ?> <div class="options_group reviews"> <?php woocommerce_wp_checkbox( array( 'id' => 'comment_status', 'value' => $product_object->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', 'label' => __( 'Enable reviews', 'woocommerce' ), 'cbvalue' => 'open', ) ); do_action( 'woocommerce_product_options_reviews' ); ?> </div> <?php endif; ?> <?php do_action( 'woocommerce_product_options_advanced' ); ?> </div> includes/admin/meta-boxes/views/html-product-data-shipping.php 0000644 00000005506 15132754524 0020542 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="shipping_product_data" class="panel woocommerce_options_panel hidden"> <div class="options_group"> <?php if ( wc_product_weight_enabled() ) { woocommerce_wp_text_input( array( 'id' => '_weight', 'value' => $product_object->get_weight( 'edit' ), 'label' => __( 'Weight', 'woocommerce' ) . ' (' . get_option( 'woocommerce_weight_unit' ) . ')', 'placeholder' => wc_format_localized_decimal( 0 ), 'desc_tip' => true, 'description' => __( 'Weight in decimal form', 'woocommerce' ), 'type' => 'text', 'data_type' => 'decimal', ) ); } if ( wc_product_dimensions_enabled() ) { ?> <p class="form-field dimensions_field"> <?php /* translators: WooCommerce dimension unit*/ ?> <label for="product_length"><?php printf( __( 'Dimensions (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?></label> <span class="wrap"> <input id="product_length" placeholder="<?php esc_attr_e( 'Length', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="_length" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_length( 'edit' ) ) ); ?>" /> <input id="product_width" placeholder="<?php esc_attr_e( 'Width', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="_width" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_width( 'edit' ) ) ); ?>" /> <input id="product_height" placeholder="<?php esc_attr_e( 'Height', 'woocommerce' ); ?>" class="input-text wc_input_decimal last" size="6" type="text" name="_height" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_height( 'edit' ) ) ); ?>" /> </span> <?php echo wc_help_tip( __( 'LxWxH in decimal form', 'woocommerce' ) ); ?> </p> <?php } do_action( 'woocommerce_product_options_dimensions' ); ?> </div> <div class="options_group"> <?php $args = array( 'taxonomy' => 'product_shipping_class', 'hide_empty' => 0, 'show_option_none' => __( 'No shipping class', 'woocommerce' ), 'name' => 'product_shipping_class', 'id' => 'product_shipping_class', 'selected' => $product_object->get_shipping_class_id( 'edit' ), 'class' => 'select short', 'orderby' => 'name', ); ?> <p class="form-field shipping_class_field"> <label for="product_shipping_class"><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></label> <?php wp_dropdown_categories( $args ); ?> <?php echo wc_help_tip( __( 'Shipping classes are used by certain shipping methods to group similar products.', 'woocommerce' ) ); ?> </p> <?php do_action( 'woocommerce_product_options_shipping' ); ?> </div> </div> includes/admin/meta-boxes/views/html-product-data-linked-products.php 0000644 00000006724 15132754524 0022033 0 ustar 00 <?php /** * Linked product options. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; ?> <div id="linked_product_data" class="panel woocommerce_options_panel hidden"> <div class="options_group show_if_grouped"> <p class="form-field"> <label for="grouped_products"><?php esc_html_e( 'Grouped products', 'woocommerce' ); ?></label> <select class="wc-product-search" multiple="multiple" style="width: 50%;" id="grouped_products" name="grouped_products[]" data-sortable="true" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products" data-exclude="<?php echo intval( $post->ID ); ?>"> <?php $product_ids = $product_object->is_type( 'grouped' ) ? $product_object->get_children( 'edit' ) : array(); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( is_object( $product ) ) { echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'This lets you choose which products are part of this group.', 'woocommerce' ) ); // WPCS: XSS ok. ?> </p> </div> <div class="options_group"> <p class="form-field"> <label for="upsell_ids"><?php esc_html_e( 'Upsells', 'woocommerce' ); ?></label> <select class="wc-product-search" multiple="multiple" style="width: 50%;" id="upsell_ids" name="upsell_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations" data-exclude="<?php echo intval( $post->ID ); ?>"> <?php $product_ids = $product_object->get_upsell_ids( 'edit' ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( is_object( $product ) ) { echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Upsells are products which you recommend instead of the currently viewed product, for example, products that are more profitable or better quality or more expensive.', 'woocommerce' ) ); // WPCS: XSS ok. ?> </p> <p class="form-field hide_if_grouped hide_if_external"> <label for="crosssell_ids"><?php esc_html_e( 'Cross-sells', 'woocommerce' ); ?></label> <select class="wc-product-search" multiple="multiple" style="width: 50%;" id="crosssell_ids" name="crosssell_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations" data-exclude="<?php echo intval( $post->ID ); ?>"> <?php $product_ids = $product_object->get_cross_sell_ids( 'edit' ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( is_object( $product ) ) { echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>'; } } ?> </select> <?php echo wc_help_tip( __( 'Cross-sells are products which you promote in the cart, based on the current product.', 'woocommerce' ) ); // WPCS: XSS ok. ?> </p> </div> <?php do_action( 'woocommerce_product_options_related' ); ?> </div> includes/admin/meta-boxes/views/html-order-item.php 0000644 00000025513 15132754524 0016403 0 ustar 00 <?php /** * Shows an order item * * @package WooCommerce\Admin * @var object $item The item being displayed * @var int $item_id The id of the item being displayed */ defined( 'ABSPATH' ) || exit; $product = $item->get_product(); $product_link = $product ? admin_url( 'post.php?post=' . $item->get_product_id() . '&action=edit' ) : ''; $thumbnail = $product ? apply_filters( 'woocommerce_admin_order_item_thumbnail', $product->get_image( 'thumbnail', array( 'title' => '' ), false ), $item_id, $item ) : ''; $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empty( $class ) ? $class : '', $item, $order ); ?> <tr class="item <?php echo esc_attr( $row_class ); ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>"> <td class="thumb"> <?php echo '<div class="wc-order-item-thumbnail">' . wp_kses_post( $thumbnail ) . '</div>'; ?> </td> <td class="name" data-sort-value="<?php echo esc_attr( $item->get_name() ); ?>"> <?php echo $product_link ? '<a href="' . esc_url( $product_link ) . '" class="wc-order-item-name">' . wp_kses_post( $item->get_name() ) . '</a>' : '<div class="wc-order-item-name">' . wp_kses_post( $item->get_name() ) . '</div>'; if ( $product && $product->get_sku() ) { echo '<div class="wc-order-item-sku"><strong>' . esc_html__( 'SKU:', 'woocommerce' ) . '</strong> ' . esc_html( $product->get_sku() ) . '</div>'; } if ( $item->get_variation_id() ) { echo '<div class="wc-order-item-variation"><strong>' . esc_html__( 'Variation ID:', 'woocommerce' ) . '</strong> '; if ( 'product_variation' === get_post_type( $item->get_variation_id() ) ) { echo esc_html( $item->get_variation_id() ); } else { /* translators: %s: variation id */ printf( esc_html__( '%s (No longer exists)', 'woocommerce' ), esc_html( $item->get_variation_id() ) ); } echo '</div>'; } ?> <input type="hidden" class="order_item_id" name="order_item_id[]" value="<?php echo esc_attr( $item_id ); ?>" /> <input type="hidden" name="order_item_tax_class[<?php echo absint( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_tax_class() ); ?>" /> <?php do_action( 'woocommerce_before_order_itemmeta', $item_id, $item, $product ); ?> <?php require __DIR__ . '/html-order-item-meta.php'; ?> <?php do_action( 'woocommerce_after_order_itemmeta', $item_id, $item, $product ); ?> </td> <?php do_action( 'woocommerce_admin_order_item_values', $product, $item, absint( $item_id ) ); ?> <td class="item_cost" width="1%" data-sort-value="<?php echo esc_attr( $order->get_item_subtotal( $item, false, true ) ); ?>"> <div class="view"> <?php echo wc_price( $order->get_item_subtotal( $item, false, true ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </td> <td class="quantity" width="1%"> <div class="view"> <?php echo '<small class="times">×</small> ' . esc_html( $item->get_quantity() ); $refunded_qty = $order->get_qty_refunded_for_item( $item_id ); if ( $refunded_qty ) { echo '<small class="refunded">-' . esc_html( $refunded_qty * -1 ) . '</small>'; } ?> </div> <?php $step = apply_filters( 'woocommerce_quantity_input_step', '1', $product ); /** * Filter to change the product quantity stepping in the order editor of the admin area. * * @since 5.8.0 * @param string $step The current step amount to be used in the quantity editor. * @param WC_Product $product The product that is being edited. * @param string $context The context in which the quantity editor is shown, 'edit' or 'refund'. */ $step_edit = apply_filters( 'woocommerce_quantity_input_step_admin', $step, $product, 'edit' ); $step_refund = apply_filters( 'woocommerce_quantity_input_step_admin', $step, $product, 'refund' ); /** * Filter to change the product quantity minimum in the order editor of the admin area. * * @since 5.8.0 * @param string $step The current minimum amount to be used in the quantity editor. * @param WC_Product $product The product that is being edited. * @param string $context The context in which the quantity editor is shown, 'edit' or 'refund'. */ $min_edit = apply_filters( 'woocommerce_quantity_input_min_admin', '0', $product, 'edit' ); $min_refund = apply_filters( 'woocommerce_quantity_input_min_admin', '0', $product, 'refund' ); ?> <div class="edit" style="display: none;"> <input type="number" step="<?php echo esc_attr( $step_edit ); ?>" min="<?php echo esc_attr( $min_edit ); ?>" autocomplete="off" name="order_item_qty[<?php echo absint( $item_id ); ?>]" placeholder="0" value="<?php echo esc_attr( $item->get_quantity() ); ?>" data-qty="<?php echo esc_attr( $item->get_quantity() ); ?>" size="4" class="quantity" /> </div> <div class="refund" style="display: none;"> <input type="number" step="<?php echo esc_attr( $step_refund ); ?>" min="<?php echo esc_attr( $min_refund ); ?>" max="<?php echo absint( $item->get_quantity() ); ?>" autocomplete="off" name="refund_order_item_qty[<?php echo absint( $item_id ); ?>]" placeholder="0" size="4" class="refund_order_item_qty" /> </div> </td> <td class="line_cost" width="1%" data-sort-value="<?php echo esc_attr( $item->get_total() ); ?>"> <div class="view"> <?php echo wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( $item->get_subtotal() !== $item->get_total() ) { /* translators: %s: discount amount */ echo '<span class="wc-order-item-discount">' . sprintf( esc_html__( '%s discount', 'woocommerce' ), wc_price( wc_format_decimal( $item->get_subtotal() - $item->get_total(), '' ), array( 'currency' => $order->get_currency() ) ) ) . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } $refunded = $order->get_total_refunded_for_item( $item_id ); if ( $refunded ) { echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?> </div> <div class="edit" style="display: none;"> <div class="split-input"> <div class="input"> <label><?php esc_attr_e( 'Before discount', 'woocommerce' ); ?></label> <input type="text" name="line_subtotal[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_subtotal() ) ); ?>" class="line_subtotal wc_input_price" data-subtotal="<?php echo esc_attr( wc_format_localized_price( $item->get_subtotal() ) ); ?>" /> </div> <div class="input"> <label><?php esc_attr_e( 'Total', 'woocommerce' ); ?></label> <input type="text" name="line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" data-tip="<?php esc_attr_e( 'After pre-tax discounts.', 'woocommerce' ); ?>" data-total="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" /> </div> </div> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" /> </div> </td> <?php $tax_data = wc_tax_enabled() ? $item->get_taxes() : false; if ( $tax_data ) { foreach ( $order_taxes as $tax_item ) { $tax_item_id = $tax_item->get_rate_id(); $tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : ''; $tax_item_subtotal = isset( $tax_data['subtotal'][ $tax_item_id ] ) ? $tax_data['subtotal'][ $tax_item_id ] : ''; if ( '' !== $tax_item_subtotal ) { $round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); $tax_item_total = wc_round_tax_total( $tax_item_total, $round_at_subtotal ? wc_get_rounding_precision() : null ); $tax_item_subtotal = wc_round_tax_total( $tax_item_subtotal, $round_at_subtotal ? wc_get_rounding_precision() : null ); } ?> <td class="line_tax" width="1%"> <div class="view"> <?php if ( '' !== $tax_item_total ) { echo wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { echo '–'; } $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id ); if ( $refunded ) { echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?> </div> <div class="edit" style="display: none;"> <div class="split-input"> <div class="input"> <label><?php esc_attr_e( 'Before discount', 'woocommerce' ); ?></label> <input type="text" name="line_subtotal_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $tax_item_subtotal ) ); ?>" class="line_subtotal_tax wc_input_price" data-subtotal_tax="<?php echo esc_attr( wc_format_localized_price( $tax_item_subtotal ) ); ?>" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" /> </div> <div class="input"> <label><?php esc_attr_e( 'Total', 'woocommerce' ); ?></label> <input type="text" name="line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $tax_item_total ) ); ?>" class="line_tax wc_input_price" data-total_tax="<?php echo esc_attr( wc_format_localized_price( $tax_item_total ) ); ?>" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" /> </div> </div> </div> <div class="refund" style="display: none;"> <input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" /> </div> </td> <?php } } ?> <td class="wc-order-edit-line-item" width="1%"> <div class="wc-order-edit-line-item-actions"> <?php if ( $order->is_editable() ) : ?> <a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit item', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete item', 'woocommerce' ); ?>"></a> <?php endif; ?> </div> </td> </tr> includes/admin/meta-boxes/views/html-product-data-inventory.php 0000644 00000011450 15132754524 0020751 0 ustar 00 <?php /** * Displays the inventory tab in the product data meta box. * * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <div id="inventory_product_data" class="panel woocommerce_options_panel hidden"> <div class="options_group"> <?php if ( wc_product_sku_enabled() ) { woocommerce_wp_text_input( array( 'id' => '_sku', 'value' => $product_object->get_sku( 'edit' ), 'label' => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>', 'desc_tip' => true, 'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ), ) ); } do_action( 'woocommerce_product_options_sku' ); if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { woocommerce_wp_checkbox( array( 'id' => '_manage_stock', 'value' => $product_object->get_manage_stock( 'edit' ) ? 'yes' : 'no', 'wrapper_class' => 'show_if_simple show_if_variable', 'label' => __( 'Manage stock?', 'woocommerce' ), 'description' => __( 'Enable stock management at product level', 'woocommerce' ), ) ); do_action( 'woocommerce_product_options_stock' ); echo '<div class="stock_fields show_if_simple show_if_variable">'; woocommerce_wp_text_input( array( 'id' => '_stock', 'value' => wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ), 'label' => __( 'Stock quantity', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'Stock quantity. If this is a variable product this value will be used to control stock for all variations, unless you define stock at variation level.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => 'any', ), 'data_type' => 'stock', ) ); echo '<input type="hidden" name="_original_stock" value="' . esc_attr( wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ) ) . '" />'; woocommerce_wp_select( array( 'id' => '_backorders', 'value' => $product_object->get_backorders( 'edit' ), 'label' => __( 'Allow backorders?', 'woocommerce' ), 'options' => wc_get_product_backorder_options(), 'desc_tip' => true, 'description' => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'woocommerce' ), ) ); woocommerce_wp_text_input( array( 'id' => '_low_stock_amount', 'value' => $product_object->get_low_stock_amount( 'edit' ), 'placeholder' => sprintf( /* translators: %d: Amount of stock left */ esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ), esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) ) ), 'label' => __( 'Low stock threshold', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'When product stock reaches this amount you will be notified by email. It is possible to define different values for each variation individually. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => 'any', ), ) ); do_action( 'woocommerce_product_options_stock_fields' ); echo '</div>'; } woocommerce_wp_select( array( 'id' => '_stock_status', 'value' => $product_object->get_stock_status( 'edit' ), 'wrapper_class' => 'stock_status_field hide_if_variable hide_if_external hide_if_grouped', 'label' => __( 'Stock status', 'woocommerce' ), 'options' => wc_get_product_stock_status_options(), 'desc_tip' => true, 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), ) ); do_action( 'woocommerce_product_options_stock_status' ); ?> </div> <div class="options_group show_if_simple show_if_variable"> <?php woocommerce_wp_checkbox( array( 'id' => '_sold_individually', 'value' => $product_object->get_sold_individually( 'edit' ) ? 'yes' : 'no', 'wrapper_class' => 'show_if_simple show_if_variable', 'label' => __( 'Sold individually', 'woocommerce' ), 'description' => __( 'Enable this to only allow one of this item to be bought in a single order', 'woocommerce' ), ) ); do_action( 'woocommerce_product_options_sold_individually' ); ?> </div> <?php do_action( 'woocommerce_product_options_inventory_product_data' ); ?> </div> includes/admin/meta-boxes/views/html-product-variation-download.php 0000644 00000002125 15132754524 0021605 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <tr> <td class="file_name"> <input type="text" class="input_text" placeholder="<?php esc_attr_e( 'File name', 'woocommerce' ); ?>" name="_wc_variation_file_names[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $file['name'] ); ?>" /> <input type="hidden" name="_wc_variation_file_hashes[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $key ); ?>" /> </td> <td class="file_url"><input type="text" class="input_text" placeholder="<?php esc_attr_e( 'http://', 'woocommerce' ); ?>" name="_wc_variation_file_urls[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $file['file'] ); ?>" /></td> <td class="file_url_choose" width="1%"><a href="#" class="button upload_file_button" data-choose="<?php esc_attr_e( 'Choose file', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Insert file URL', 'woocommerce' ); ?>"><?php esc_html_e( 'Choose file', 'woocommerce' ); ?></a></td> <td width="1%"><a href="#" class="delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></td> </tr> includes/admin/meta-boxes/views/html-product-data-general.php 0000644 00000017205 15132754524 0020335 0 ustar 00 <?php /** * Product general data panel. * * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; ?> <div id="general_product_data" class="panel woocommerce_options_panel"> <div class="options_group show_if_external"> <?php woocommerce_wp_text_input( array( 'id' => '_product_url', 'value' => is_callable( array( $product_object, 'get_product_url' ) ) ? $product_object->get_product_url( 'edit' ) : '', 'label' => __( 'Product URL', 'woocommerce' ), 'placeholder' => 'https://', 'description' => __( 'Enter the external URL to the product.', 'woocommerce' ), ) ); woocommerce_wp_text_input( array( 'id' => '_button_text', 'value' => is_callable( array( $product_object, 'get_button_text' ) ) ? $product_object->get_button_text( 'edit' ) : '', 'label' => __( 'Button text', 'woocommerce' ), 'placeholder' => _x( 'Buy product', 'placeholder', 'woocommerce' ), 'description' => __( 'This text will be shown on the button linking to the external product.', 'woocommerce' ), ) ); do_action( 'woocommerce_product_options_external' ); ?> </div> <div class="options_group pricing show_if_simple show_if_external hidden"> <?php woocommerce_wp_text_input( array( 'id' => '_regular_price', 'value' => $product_object->get_regular_price( 'edit' ), 'label' => __( 'Regular price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')', 'data_type' => 'price', ) ); woocommerce_wp_text_input( array( 'id' => '_sale_price', 'value' => $product_object->get_sale_price( 'edit' ), 'data_type' => 'price', 'label' => __( 'Sale price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')', 'description' => '<a href="#" class="sale_schedule">' . __( 'Schedule', 'woocommerce' ) . '</a>', ) ); $sale_price_dates_from_timestamp = $product_object->get_date_on_sale_from( 'edit' ) ? $product_object->get_date_on_sale_from( 'edit' )->getOffsetTimestamp() : false; $sale_price_dates_to_timestamp = $product_object->get_date_on_sale_to( 'edit' ) ? $product_object->get_date_on_sale_to( 'edit' )->getOffsetTimestamp() : false; $sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : ''; $sale_price_dates_to = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : ''; echo '<p class="form-field sale_price_dates_fields"> <label for="_sale_price_dates_from">' . esc_html__( 'Sale price dates', 'woocommerce' ) . '</label> <input type="text" class="short" name="_sale_price_dates_from" id="_sale_price_dates_from" value="' . esc_attr( $sale_price_dates_from ) . '" placeholder="' . esc_html( _x( 'From…', 'placeholder', 'woocommerce' ) ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" /> <input type="text" class="short" name="_sale_price_dates_to" id="_sale_price_dates_to" value="' . esc_attr( $sale_price_dates_to ) . '" placeholder="' . esc_html( _x( 'To…', 'placeholder', 'woocommerce' ) ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" /> <a href="#" class="description cancel_sale_schedule">' . esc_html__( 'Cancel', 'woocommerce' ) . '</a>' . wc_help_tip( __( 'The sale will start at 00:00:00 of "From" date and end at 23:59:59 of "To" date.', 'woocommerce' ) ) . ' </p>'; do_action( 'woocommerce_product_options_pricing' ); ?> </div> <div class="options_group show_if_downloadable hidden"> <div class="form-field downloadable_files"> <label><?php esc_html_e( 'Downloadable files', 'woocommerce' ); ?></label> <table class="widefat"> <thead> <tr> <th class="sort"> </th> <th><?php esc_html_e( 'Name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the name of the download shown to the customer.', 'woocommerce' ) ); ?></th> <th colspan="2"><?php esc_html_e( 'File URL', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the URL or absolute path to the file which customers will get access to. URLs entered here should already be encoded.', 'woocommerce' ) ); ?></th> <th> </th> </tr> </thead> <tbody> <?php $downloadable_files = $product_object->get_downloads( 'edit' ); if ( $downloadable_files ) { foreach ( $downloadable_files as $key => $file ) { include __DIR__ . '/html-product-download.php'; } } ?> </tbody> <tfoot> <tr> <th colspan="5"> <a href="#" class="button insert" data-row=" <?php $key = ''; $file = array( 'file' => '', 'name' => '', ); ob_start(); require __DIR__ . '/html-product-download.php'; echo esc_attr( ob_get_clean() ); ?> "><?php esc_html_e( 'Add File', 'woocommerce' ); ?></a> </th> </tr> </tfoot> </table> </div> <?php woocommerce_wp_text_input( array( 'id' => '_download_limit', 'value' => -1 === $product_object->get_download_limit( 'edit' ) ? '' : $product_object->get_download_limit( 'edit' ), 'label' => __( 'Download limit', 'woocommerce' ), 'placeholder' => __( 'Unlimited', 'woocommerce' ), 'description' => __( 'Leave blank for unlimited re-downloads.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => '1', 'min' => '0', ), ) ); woocommerce_wp_text_input( array( 'id' => '_download_expiry', 'value' => -1 === $product_object->get_download_expiry( 'edit' ) ? '' : $product_object->get_download_expiry( 'edit' ), 'label' => __( 'Download expiry', 'woocommerce' ), 'placeholder' => __( 'Never', 'woocommerce' ), 'description' => __( 'Enter the number of days before a download link expires, or leave blank.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( 'step' => '1', 'min' => '0', ), ) ); do_action( 'woocommerce_product_options_downloads' ); ?> </div> <?php if ( wc_tax_enabled() ) : ?> <div class="options_group show_if_simple show_if_external show_if_variable"> <?php woocommerce_wp_select( array( 'id' => '_tax_status', 'value' => $product_object->get_tax_status( 'edit' ), 'label' => __( 'Tax status', 'woocommerce' ), 'options' => array( 'taxable' => __( 'Taxable', 'woocommerce' ), 'shipping' => __( 'Shipping only', 'woocommerce' ), 'none' => _x( 'None', 'Tax status', 'woocommerce' ), ), 'desc_tip' => 'true', 'description' => __( 'Define whether or not the entire product is taxable, or just the cost of shipping it.', 'woocommerce' ), ) ); woocommerce_wp_select( array( 'id' => '_tax_class', 'value' => $product_object->get_tax_class( 'edit' ), 'label' => __( 'Tax class', 'woocommerce' ), 'options' => wc_get_product_tax_class_options(), 'desc_tip' => 'true', 'description' => __( 'Choose a tax class for this product. Tax classes are used to apply different tax rates specific to certain types of product.', 'woocommerce' ), ) ); do_action( 'woocommerce_product_options_tax' ); ?> </div> <?php endif; ?> <?php do_action( 'woocommerce_product_options_general_product_data' ); ?> </div> includes/admin/meta-boxes/views/html-product-download.php 0000644 00000001726 15132754524 0017621 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } ?> <tr> <td class="sort"></td> <td class="file_name"> <input type="text" class="input_text" placeholder="<?php esc_attr_e( 'File name', 'woocommerce' ); ?>" name="_wc_file_names[]" value="<?php echo esc_attr( $file['name'] ); ?>" /> <input type="hidden" name="_wc_file_hashes[]" value="<?php echo esc_attr( $key ); ?>" /> </td> <td class="file_url"><input type="text" class="input_text" placeholder="<?php esc_attr_e( 'http://', 'woocommerce' ); ?>" name="_wc_file_urls[]" value="<?php echo esc_attr( $file['file'] ); ?>" /></td> <td class="file_url_choose" width="1%"><a href="#" class="button upload_file_button" data-choose="<?php esc_attr_e( 'Choose file', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Insert file URL', 'woocommerce' ); ?>"><?php echo esc_html__( 'Choose file', 'woocommerce' ); ?></a></td> <td width="1%"><a href="#" class="delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></td> </tr> includes/admin/meta-boxes/views/html-order-item-meta.php 0000644 00000004134 15132754524 0017323 0 ustar 00 <?php if ( ! defined( 'ABSPATH' ) ) { exit; } $hidden_order_itemmeta = apply_filters( 'woocommerce_hidden_order_itemmeta', array( '_qty', '_tax_class', '_product_id', '_variation_id', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', 'method_id', 'cost', '_reduced_stock', '_restock_refunded_items', ) ); ?><div class="view"> <?php if ( $meta_data = $item->get_formatted_meta_data( '' ) ) : ?> <table cellspacing="0" class="display_meta"> <?php foreach ( $meta_data as $meta_id => $meta ) : if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) { continue; } ?> <tr> <th><?php echo wp_kses_post( $meta->display_key ); ?>:</th> <td><?php echo wp_kses_post( force_balance_tags( $meta->display_value ) ); ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> </div> <div class="edit" style="display: none;"> <table class="meta" cellspacing="0"> <tbody class="meta_items"> <?php if ( $meta_data = $item->get_formatted_meta_data( '' ) ) : ?> <?php foreach ( $meta_data as $meta_id => $meta ) : if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) { continue; } ?> <tr data-meta_id="<?php echo esc_attr( $meta_id ); ?>"> <td> <input type="text" maxlength="255" placeholder="<?php esc_attr_e( 'Name (required)', 'woocommerce' ); ?>" name="meta_key[<?php echo esc_attr( $item_id ); ?>][<?php echo esc_attr( $meta_id ); ?>]" value="<?php echo esc_attr( $meta->key ); ?>" /> <textarea placeholder="<?php esc_attr_e( 'Value (required)', 'woocommerce' ); ?>" name="meta_value[<?php echo esc_attr( $item_id ); ?>][<?php echo esc_attr( $meta_id ); ?>]"><?php echo esc_textarea( rawurldecode( $meta->value ) ); ?></textarea> </td> <td width="1%"><button class="remove_order_item_meta button">×</button></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> <tfoot> <tr> <td colspan="4"><button class="add_order_item_meta button"><?php esc_html_e( 'Add meta', 'woocommerce' ); ?></button></td> </tr> </tfoot> </table> </div> includes/admin/meta-boxes/class-wc-meta-box-product-data.php 0000644 00000061734 15132754524 0020055 0 ustar 00 <?php /** * Product Data * * Displays the product data box, tabbed, with several panels covering price, stock etc. * * @package WooCommerce\Admin\Meta Boxes * @version 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Meta_Box_Product_Data Class. */ class WC_Meta_Box_Product_Data { /** * Output the metabox. * * @param WP_Post $post Post object. */ public static function output( $post ) { global $thepostid, $product_object; $thepostid = $post->ID; $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); include __DIR__ . '/views/html-product-data-panel.php'; } /** * Show tab content/settings. */ private static function output_tabs() { global $post, $thepostid, $product_object; include __DIR__ . '/views/html-product-data-general.php'; include __DIR__ . '/views/html-product-data-inventory.php'; include __DIR__ . '/views/html-product-data-shipping.php'; include __DIR__ . '/views/html-product-data-linked-products.php'; include __DIR__ . '/views/html-product-data-attributes.php'; include __DIR__ . '/views/html-product-data-advanced.php'; } /** * Return array of product type options. * * @return array */ private static function get_product_type_options() { return apply_filters( 'product_type_options', array( 'virtual' => array( 'id' => '_virtual', 'wrapper_class' => 'show_if_simple', 'label' => __( 'Virtual', 'woocommerce' ), 'description' => __( 'Virtual products are intangible and are not shipped.', 'woocommerce' ), 'default' => 'no', ), 'downloadable' => array( 'id' => '_downloadable', 'wrapper_class' => 'show_if_simple', 'label' => __( 'Downloadable', 'woocommerce' ), 'description' => __( 'Downloadable products give access to a file upon purchase.', 'woocommerce' ), 'default' => 'no', ), ) ); } /** * Return array of tabs to show. * * @return array */ private static function get_product_data_tabs() { $tabs = apply_filters( 'woocommerce_product_data_tabs', array( 'general' => array( 'label' => __( 'General', 'woocommerce' ), 'target' => 'general_product_data', 'class' => array( 'hide_if_grouped' ), 'priority' => 10, ), 'inventory' => array( 'label' => __( 'Inventory', 'woocommerce' ), 'target' => 'inventory_product_data', 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_grouped', 'show_if_external' ), 'priority' => 20, ), 'shipping' => array( 'label' => __( 'Shipping', 'woocommerce' ), 'target' => 'shipping_product_data', 'class' => array( 'hide_if_virtual', 'hide_if_grouped', 'hide_if_external' ), 'priority' => 30, ), 'linked_product' => array( 'label' => __( 'Linked Products', 'woocommerce' ), 'target' => 'linked_product_data', 'class' => array(), 'priority' => 40, ), 'attribute' => array( 'label' => __( 'Attributes', 'woocommerce' ), 'target' => 'product_attributes', 'class' => array(), 'priority' => 50, ), 'variations' => array( 'label' => __( 'Variations', 'woocommerce' ), 'target' => 'variable_product_options', 'class' => array( 'show_if_variable' ), 'priority' => 60, ), 'advanced' => array( 'label' => __( 'Advanced', 'woocommerce' ), 'target' => 'advanced_product_data', 'class' => array(), 'priority' => 70, ), ) ); // Sort tabs based on priority. uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) ); return $tabs; } /** * Callback to sort product data tabs on priority. * * @since 3.1.0 * @param int $a First item. * @param int $b Second item. * * @return bool */ private static function product_data_tabs_sort( $a, $b ) { if ( ! isset( $a['priority'], $b['priority'] ) ) { return -1; } if ( $a['priority'] === $b['priority'] ) { return 0; } return $a['priority'] < $b['priority'] ? -1 : 1; } /** * Filter callback for finding variation attributes. * * @param WC_Product_Attribute $attribute Product attribute. * @return bool */ private static function filter_variation_attributes( $attribute ) { return true === $attribute->get_variation(); } /** * Show options for the variable product type. */ public static function output_variations() { global $post, $wpdb, $product_object; $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); $default_attributes = $product_object->get_default_attributes(); $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); $variations_total_pages = ceil( $variations_count / $variations_per_page ); include __DIR__ . '/views/html-product-data-variations.php'; } /** * Prepare downloads for save. * * @param array $file_names File names. * @param array $file_urls File urls. * @param array $file_hashes File hashes. * * @return array */ private static function prepare_downloads( $file_names, $file_urls, $file_hashes ) { $downloads = array(); if ( ! empty( $file_urls ) ) { $file_url_size = count( $file_urls ); for ( $i = 0; $i < $file_url_size; $i ++ ) { if ( ! empty( $file_urls[ $i ] ) ) { $downloads[] = array( 'name' => wc_clean( $file_names[ $i ] ), 'file' => wp_unslash( trim( $file_urls[ $i ] ) ), 'download_id' => wc_clean( $file_hashes[ $i ] ), ); } } } return $downloads; } /** * Prepare children for save. * * @return array */ private static function prepare_children() { return isset( $_POST['grouped_products'] ) ? array_filter( array_map( 'intval', (array) $_POST['grouped_products'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing } /** * Prepare attributes for save. * * @param array $data Attribute data. * * @return array */ public static function prepare_attributes( $data = false ) { $attributes = array(); if ( ! $data ) { $data = stripslashes_deep( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } if ( isset( $data['attribute_names'], $data['attribute_values'] ) ) { $attribute_names = $data['attribute_names']; $attribute_values = $data['attribute_values']; $attribute_visibility = isset( $data['attribute_visibility'] ) ? $data['attribute_visibility'] : array(); $attribute_variation = isset( $data['attribute_variation'] ) ? $data['attribute_variation'] : array(); $attribute_position = $data['attribute_position']; $attribute_names_max_key = max( array_keys( $attribute_names ) ); for ( $i = 0; $i <= $attribute_names_max_key; $i++ ) { if ( empty( $attribute_names[ $i ] ) || ! isset( $attribute_values[ $i ] ) ) { continue; } $attribute_id = 0; $attribute_name = wc_clean( esc_html( $attribute_names[ $i ] ) ); if ( 'pa_' === substr( $attribute_name, 0, 3 ) ) { $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); } $options = isset( $attribute_values[ $i ] ) ? $attribute_values[ $i ] : ''; if ( is_array( $options ) ) { // Term ids sent as array. $options = wp_parse_id_list( $options ); } else { // Terms or text sent in textarea. $options = 0 < $attribute_id ? wc_sanitize_textarea( esc_html( wc_sanitize_term_text_based( $options ) ) ) : wc_sanitize_textarea( esc_html( $options ) ); $options = wc_get_text_attributes( $options ); } if ( empty( $options ) ) { continue; } $attribute = new WC_Product_Attribute(); $attribute->set_id( $attribute_id ); $attribute->set_name( $attribute_name ); $attribute->set_options( $options ); $attribute->set_position( $attribute_position[ $i ] ); $attribute->set_visible( isset( $attribute_visibility[ $i ] ) ); $attribute->set_variation( isset( $attribute_variation[ $i ] ) ); $attributes[] = apply_filters( 'woocommerce_admin_meta_boxes_prepare_attribute', $attribute, $data, $i ); } } return $attributes; } /** * Prepare attributes for a specific variation or defaults. * * @param array $all_attributes List of attribute keys. * @param string $key_prefix Attribute key prefix. * @param int $index Attribute array index. * @return array */ private static function prepare_set_attributes( $all_attributes, $key_prefix = 'attribute_', $index = null ) { $attributes = array(); if ( $all_attributes ) { foreach ( $all_attributes as $attribute ) { if ( $attribute->get_variation() ) { $attribute_key = sanitize_title( $attribute->get_name() ); if ( ! is_null( $index ) ) { $value = isset( $_POST[ $key_prefix . $attribute_key ][ $index ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ][ $index ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } else { $value = isset( $_POST[ $key_prefix . $attribute_key ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } if ( $attribute->is_taxonomy() ) { // Don't use wc_clean as it destroys sanitized characters. $value = sanitize_title( $value ); } else { $value = html_entity_decode( wc_clean( $value ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // WPCS: sanitization ok. } $attributes[ $attribute_key ] = $value; } } } return $attributes; } /** * Save meta box data. * * @param int $post_id WP post id. * @param WP_Post $post Post object. */ public static function save( $post_id, $post ) { // phpcs:disable WordPress.Security.NonceVerification.Missing // Process product type first so we have the correct class to run setters. $product_type = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( wp_unslash( $_POST['product-type'] ) ); $classname = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : 'simple' ); $product = new $classname( $post_id ); $attributes = self::prepare_attributes(); $stock = null; // Handle stock changes. if ( isset( $_POST['_stock'] ) ) { if ( isset( $_POST['_original_stock'] ) && wc_stock_amount( $product->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['_original_stock'] ) ) ) { /* translators: 1: product ID 2: quantity in stock */ WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $product->get_id(), $product->get_stock_quantity( 'edit' ) ) ); } else { $stock = wc_stock_amount( wp_unslash( $_POST['_stock'] ) ); } } // Handle dates. $date_on_sale_from = ''; $date_on_sale_to = ''; // Force date from to beginning of day. if ( isset( $_POST['_sale_price_dates_from'] ) ) { $date_on_sale_from = wc_clean( wp_unslash( $_POST['_sale_price_dates_from'] ) ); if ( ! empty( $date_on_sale_from ) ) { $date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } } // Force date to to the end of the day. if ( isset( $_POST['_sale_price_dates_to'] ) ) { $date_on_sale_to = wc_clean( wp_unslash( $_POST['_sale_price_dates_to'] ) ); if ( ! empty( $date_on_sale_to ) ) { $date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } } $errors = $product->set_props( array( 'sku' => isset( $_POST['_sku'] ) ? wc_clean( wp_unslash( $_POST['_sku'] ) ) : null, 'purchase_note' => isset( $_POST['_purchase_note'] ) ? wp_kses_post( wp_unslash( $_POST['_purchase_note'] ) ) : '', 'downloadable' => isset( $_POST['_downloadable'] ), 'virtual' => isset( $_POST['_virtual'] ), 'featured' => isset( $_POST['_featured'] ), 'catalog_visibility' => isset( $_POST['_visibility'] ) ? wc_clean( wp_unslash( $_POST['_visibility'] ) ) : null, 'tax_status' => isset( $_POST['_tax_status'] ) ? wc_clean( wp_unslash( $_POST['_tax_status'] ) ) : null, 'tax_class' => isset( $_POST['_tax_class'] ) ? sanitize_title( wp_unslash( $_POST['_tax_class'] ) ) : null, 'weight' => isset( $_POST['_weight'] ) ? wc_clean( wp_unslash( $_POST['_weight'] ) ) : null, 'length' => isset( $_POST['_length'] ) ? wc_clean( wp_unslash( $_POST['_length'] ) ) : null, 'width' => isset( $_POST['_width'] ) ? wc_clean( wp_unslash( $_POST['_width'] ) ) : null, 'height' => isset( $_POST['_height'] ) ? wc_clean( wp_unslash( $_POST['_height'] ) ) : null, 'shipping_class_id' => isset( $_POST['product_shipping_class'] ) ? absint( wp_unslash( $_POST['product_shipping_class'] ) ) : null, 'sold_individually' => ! empty( $_POST['_sold_individually'] ), 'upsell_ids' => isset( $_POST['upsell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['upsell_ids'] ) ) : array(), 'cross_sell_ids' => isset( $_POST['crosssell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['crosssell_ids'] ) ) : array(), 'regular_price' => isset( $_POST['_regular_price'] ) ? wc_clean( wp_unslash( $_POST['_regular_price'] ) ) : null, 'sale_price' => isset( $_POST['_sale_price'] ) ? wc_clean( wp_unslash( $_POST['_sale_price'] ) ) : null, 'date_on_sale_from' => $date_on_sale_from, 'date_on_sale_to' => $date_on_sale_to, 'manage_stock' => ! empty( $_POST['_manage_stock'] ), 'backorders' => isset( $_POST['_backorders'] ) ? wc_clean( wp_unslash( $_POST['_backorders'] ) ) : null, 'stock_status' => isset( $_POST['_stock_status'] ) ? wc_clean( wp_unslash( $_POST['_stock_status'] ) ) : null, 'stock_quantity' => $stock, 'low_stock_amount' => isset( $_POST['_low_stock_amount'] ) && '' !== $_POST['_low_stock_amount'] ? wc_stock_amount( wp_unslash( $_POST['_low_stock_amount'] ) ) : '', 'download_limit' => isset( $_POST['_download_limit'] ) && '' !== $_POST['_download_limit'] ? absint( wp_unslash( $_POST['_download_limit'] ) ) : '', 'download_expiry' => isset( $_POST['_download_expiry'] ) && '' !== $_POST['_download_expiry'] ? absint( wp_unslash( $_POST['_download_expiry'] ) ) : '', // Those are sanitized inside prepare_downloads. 'downloads' => self::prepare_downloads( isset( $_POST['_wc_file_names'] ) ? wp_unslash( $_POST['_wc_file_names'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized isset( $_POST['_wc_file_urls'] ) ? wp_unslash( $_POST['_wc_file_urls'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized isset( $_POST['_wc_file_hashes'] ) ? wp_unslash( $_POST['_wc_file_hashes'] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ), 'product_url' => isset( $_POST['_product_url'] ) ? esc_url_raw( wp_unslash( $_POST['_product_url'] ) ) : '', 'button_text' => isset( $_POST['_button_text'] ) ? wc_clean( wp_unslash( $_POST['_button_text'] ) ) : '', 'children' => 'grouped' === $product_type ? self::prepare_children() : null, 'reviews_allowed' => ! empty( $_POST['comment_status'] ) && 'open' === $_POST['comment_status'], 'attributes' => $attributes, 'default_attributes' => self::prepare_set_attributes( $attributes, 'default_attribute_' ), ) ); if ( is_wp_error( $errors ) ) { WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); } /** * Set props before save. * * @since 3.0.0 */ do_action( 'woocommerce_admin_process_product_object', $product ); $product->save(); if ( $product->is_type( 'variable' ) ) { $original_post_title = isset( $_POST['original_post_title'] ) ? wc_clean( wp_unslash( $_POST['original_post_title'] ) ) : ''; $post_title = isset( $_POST['post_title'] ) ? wc_clean( wp_unslash( $_POST['post_title'] ) ) : ''; $product->get_data_store()->sync_variation_names( $product, $original_post_title, $post_title ); } do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Save variation meta box data. * * @param int $post_id WP post id. * @param WP_Post $post Post object. */ public static function save_variations( $post_id, $post ) { global $wpdb; // phpcs:disable WordPress.Security.NonceVerification.Missing if ( isset( $_POST['variable_post_id'] ) ) { $parent = wc_get_product( $post_id ); $parent->set_default_attributes( self::prepare_set_attributes( $parent->get_attributes(), 'default_attribute_' ) ); $parent->save(); $max_loop = max( array_keys( wp_unslash( $_POST['variable_post_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data_store = $parent->get_data_store(); $data_store->sort_all_product_variations( $parent->get_id() ); $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; $new_variation_menu_order_value = ! empty( $_POST['new_variation_menu_order_value'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_value'] ) ) : false; // Only perform this operation if setting menu order via the prompt. if ( $new_variation_menu_order_id && $new_variation_menu_order_value ) { /* * We need to gather all the variations with menu order that is * equal or greater than the menu order that is newly set and * increment them by one so that we can correctly insert the updated * variation menu order. */ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order = menu_order + 1 WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' AND menu_order >= %d AND ID != %d", $post_id, $new_variation_menu_order_value, $new_variation_menu_order_id ) ); } for ( $i = 0; $i <= $max_loop; $i++ ) { if ( ! isset( $_POST['variable_post_id'][ $i ] ) ) { continue; } $variation_id = absint( $_POST['variable_post_id'][ $i ] ); $variation = wc_get_product_object( 'variation', $variation_id ); $stock = null; // Handle stock changes. if ( isset( $_POST['variable_stock'], $_POST['variable_stock'][ $i ] ) ) { if ( isset( $_POST['variable_original_stock'], $_POST['variable_original_stock'][ $i ] ) && wc_stock_amount( $variation->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['variable_original_stock'][ $i ] ) ) ) { /* translators: 1: product ID 2: quantity in stock */ WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $variation->get_id(), $variation->get_stock_quantity( 'edit' ) ) ); } else { $stock = wc_stock_amount( wp_unslash( $_POST['variable_stock'][ $i ] ) ); } } // Handle dates. $date_on_sale_from = ''; $date_on_sale_to = ''; // Force date from to beginning of day. if ( isset( $_POST['variable_sale_price_dates_from'][ $i ] ) ) { $date_on_sale_from = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_from'][ $i ] ) ); if ( ! empty( $date_on_sale_from ) ) { $date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } } // Force date to to the end of the day. if ( isset( $_POST['variable_sale_price_dates_to'][ $i ] ) ) { $date_on_sale_to = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_to'][ $i ] ) ); if ( ! empty( $date_on_sale_to ) ) { $date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } } $errors = $variation->set_props( array( 'status' => isset( $_POST['variable_enabled'][ $i ] ) ? 'publish' : 'private', 'menu_order' => isset( $_POST['variation_menu_order'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variation_menu_order'][ $i ] ) ) : null, 'regular_price' => isset( $_POST['variable_regular_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_regular_price'][ $i ] ) ) : null, 'sale_price' => isset( $_POST['variable_sale_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sale_price'][ $i ] ) ) : null, 'virtual' => isset( $_POST['variable_is_virtual'][ $i ] ), 'downloadable' => isset( $_POST['variable_is_downloadable'][ $i ] ), 'date_on_sale_from' => $date_on_sale_from, 'date_on_sale_to' => $date_on_sale_to, 'description' => isset( $_POST['variable_description'][ $i ] ) ? wp_kses_post( wp_unslash( $_POST['variable_description'][ $i ] ) ) : null, 'download_limit' => isset( $_POST['variable_download_limit'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_limit'][ $i ] ) ) : null, 'download_expiry' => isset( $_POST['variable_download_expiry'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_expiry'][ $i ] ) ) : null, // Those are sanitized inside prepare_downloads. 'downloads' => self::prepare_downloads( isset( $_POST['_wc_variation_file_names'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_names'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized isset( $_POST['_wc_variation_file_urls'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_urls'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized isset( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ), 'manage_stock' => isset( $_POST['variable_manage_stock'][ $i ] ), 'stock_quantity' => $stock, 'low_stock_amount' => isset( $_POST['variable_low_stock_amount'][ $i ] ) && '' !== $_POST['variable_low_stock_amount'][ $i ] ? wc_stock_amount( wp_unslash( $_POST['variable_low_stock_amount'][ $i ] ) ) : '', 'backorders' => isset( $_POST['variable_backorders'], $_POST['variable_backorders'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_backorders'][ $i ] ) ) : null, 'stock_status' => isset( $_POST['variable_stock_status'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_stock_status'][ $i ] ) ) : null, 'image_id' => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null, 'attributes' => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ), 'sku' => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sku'][ $i ] ) ) : '', 'weight' => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_weight'][ $i ] ) ) : '', 'length' => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_length'][ $i ] ) ) : '', 'width' => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_width'][ $i ] ) ) : '', 'height' => isset( $_POST['variable_height'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_height'][ $i ] ) ) : '', 'shipping_class_id' => isset( $_POST['variable_shipping_class'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_shipping_class'][ $i ] ) ) : null, 'tax_class' => isset( $_POST['variable_tax_class'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_tax_class'][ $i ] ) ) : null, ) ); if ( is_wp_error( $errors ) ) { WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); } /** * Set variation props before save. * * @param object $variation WC_Product_Variation object. * @param int $i * @since 3.8.0 */ do_action( 'woocommerce_admin_process_variation_object', $variation, $i ); $variation->save(); do_action( 'woocommerce_save_product_variation', $variation_id, $i ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing } } includes/admin/meta-boxes/class-wc-meta-box-product-reviews.php 0000644 00000002757 15132754524 0020630 0 ustar 00 <?php /** * Product Reviews * * Functions for displaying product reviews data meta box. * * @package WooCommerce\Admin\Meta Boxes */ defined( 'ABSPATH' ) || exit; /** * WC_Meta_Box_Product_Reviews */ class WC_Meta_Box_Product_Reviews { /** * Output the metabox. * * @param object $comment Comment being shown. */ public static function output( $comment ) { wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); $current = get_comment_meta( $comment->comment_ID, 'rating', true ); ?> <select name="rating" id="rating"> <?php for ( $rating = 1; $rating <= 5; $rating ++ ) { printf( '<option value="%1$s"%2$s>%1$s</option>', $rating, selected( $current, $rating, false ) ); // WPCS: XSS ok. } ?> </select> <?php } /** * Save meta box data * * @param mixed $data Data to save. * @return mixed */ public static function save( $data ) { // Not allowed, return regular value without updating meta. if ( ! isset( $_POST['woocommerce_meta_nonce'], $_POST['rating'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // WPCS: input var ok, sanitization ok. return $data; } if ( $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok. return $data; } $comment_id = $data['comment_ID']; update_comment_meta( $comment_id, 'rating', intval( wp_unslash( $_POST['rating'] ) ) ); // WPCS: input var ok. // Return regular value after updating. return $data; } } includes/admin/meta-boxes/class-wc-meta-box-order-data.php 0000644 00000052564 15132754524 0017511 0 ustar 00 <?php /** * Order Data * * Functions for displaying the order data meta box. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Meta Boxes * @version 2.2.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Meta_Box_Order_Data Class. */ class WC_Meta_Box_Order_Data { /** * Billing fields. * * @var array */ protected static $billing_fields = array(); /** * Shipping fields. * * @var array */ protected static $shipping_fields = array(); /** * Init billing and shipping fields we display + save. */ public static function init_address_fields() { self::$billing_fields = apply_filters( 'woocommerce_admin_billing_fields', array( 'first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'show' => false, ), 'last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'show' => false, ), 'company' => array( 'label' => __( 'Company', 'woocommerce' ), 'show' => false, ), 'address_1' => array( 'label' => __( 'Address line 1', 'woocommerce' ), 'show' => false, ), 'address_2' => array( 'label' => __( 'Address line 2', 'woocommerce' ), 'show' => false, ), 'city' => array( 'label' => __( 'City', 'woocommerce' ), 'show' => false, ), 'postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'show' => false, ), 'country' => array( 'label' => __( 'Country / Region', 'woocommerce' ), 'show' => false, 'class' => 'js_field-country select short', 'type' => 'select', 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), ), 'state' => array( 'label' => __( 'State / County', 'woocommerce' ), 'class' => 'js_field-state select short', 'show' => false, ), 'email' => array( 'label' => __( 'Email address', 'woocommerce' ), ), 'phone' => array( 'label' => __( 'Phone', 'woocommerce' ), ), ) ); self::$shipping_fields = apply_filters( 'woocommerce_admin_shipping_fields', array( 'first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'show' => false, ), 'last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'show' => false, ), 'company' => array( 'label' => __( 'Company', 'woocommerce' ), 'show' => false, ), 'address_1' => array( 'label' => __( 'Address line 1', 'woocommerce' ), 'show' => false, ), 'address_2' => array( 'label' => __( 'Address line 2', 'woocommerce' ), 'show' => false, ), 'city' => array( 'label' => __( 'City', 'woocommerce' ), 'show' => false, ), 'postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'show' => false, ), 'country' => array( 'label' => __( 'Country / Region', 'woocommerce' ), 'show' => false, 'type' => 'select', 'class' => 'js_field-country select short', 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_shipping_countries(), ), 'state' => array( 'label' => __( 'State / County', 'woocommerce' ), 'class' => 'js_field-state select short', 'show' => false, ), 'phone' => array( 'label' => __( 'Phone', 'woocommerce' ), ), ) ); } /** * Output the metabox. * * @param WP_Post $post */ public static function output( $post ) { global $theorder; if ( ! is_object( $theorder ) ) { $theorder = wc_get_order( $post->ID ); } $order = $theorder; self::init_address_fields(); if ( WC()->payment_gateways() ) { $payment_gateways = WC()->payment_gateways->payment_gateways(); } else { $payment_gateways = array(); } $payment_method = $order->get_payment_method(); $order_type_object = get_post_type_object( $post->post_type ); wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); ?> <style type="text/css"> #post-body-content, #titlediv { display:none } </style> <div class="panel-wrap woocommerce"> <input name="post_title" type="hidden" value="<?php echo empty( $post->post_title ) ? __( 'Order', 'woocommerce' ) : esc_attr( $post->post_title ); ?>" /> <input name="post_status" type="hidden" value="<?php echo esc_attr( $post->post_status ); ?>" /> <div id="order_data" class="panel woocommerce-order-data"> <h2 class="woocommerce-order-data__heading"> <?php /* translators: 1: order type 2: order number */ printf( esc_html__( '%1$s #%2$s details', 'woocommerce' ), esc_html( $order_type_object->labels->singular_name ), esc_html( $order->get_order_number() ) ); ?> </h2> <p class="woocommerce-order-data__meta order_number"> <?php $meta_list = array(); if ( $payment_method && 'other' !== $payment_method ) { /* translators: %s: payment method */ $payment_method_string = sprintf( __( 'Payment via %s', 'woocommerce' ), esc_html( isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_title() : $payment_method ) ); if ( $transaction_id = $order->get_transaction_id() ) { if ( isset( $payment_gateways[ $payment_method ] ) && ( $url = $payment_gateways[ $payment_method ]->get_transaction_url( $order ) ) ) { $payment_method_string .= ' (<a href="' . esc_url( $url ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>)'; } else { $payment_method_string .= ' (' . esc_html( $transaction_id ) . ')'; } } $meta_list[] = $payment_method_string; } if ( $order->get_date_paid() ) { /* translators: 1: date 2: time */ $meta_list[] = sprintf( __( 'Paid on %1$s @ %2$s', 'woocommerce' ), wc_format_datetime( $order->get_date_paid() ), wc_format_datetime( $order->get_date_paid(), get_option( 'time_format' ) ) ); } if ( $ip_address = $order->get_customer_ip_address() ) { /* translators: %s: IP address */ $meta_list[] = sprintf( __( 'Customer IP: %s', 'woocommerce' ), '<span class="woocommerce-Order-customerIP">' . esc_html( $ip_address ) . '</span>' ); } echo wp_kses_post( implode( '. ', $meta_list ) ); ?> </p> <div class="order_data_column_container"> <div class="order_data_column"> <h3><?php esc_html_e( 'General', 'woocommerce' ); ?></h3> <p class="form-field form-field-wide"> <label for="order_date"><?php _e( 'Date created:', 'woocommerce' ); ?></label> <input type="text" class="date-picker" name="order_date" maxlength="10" value="<?php echo esc_attr( date_i18n( 'Y-m-d', strtotime( $post->post_date ) ) ); ?>" pattern="<?php echo esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ); ?>" />@ ‎ <input type="number" class="hour" placeholder="<?php esc_attr_e( 'h', 'woocommerce' ); ?>" name="order_date_hour" min="0" max="23" step="1" value="<?php echo esc_attr( date_i18n( 'H', strtotime( $post->post_date ) ) ); ?>" pattern="([01]?[0-9]{1}|2[0-3]{1})" />: <input type="number" class="minute" placeholder="<?php esc_attr_e( 'm', 'woocommerce' ); ?>" name="order_date_minute" min="0" max="59" step="1" value="<?php echo esc_attr( date_i18n( 'i', strtotime( $post->post_date ) ) ); ?>" pattern="[0-5]{1}[0-9]{1}" /> <input type="hidden" name="order_date_second" value="<?php echo esc_attr( date_i18n( 's', strtotime( $post->post_date ) ) ); ?>" /> </p> <p class="form-field form-field-wide wc-order-status"> <label for="order_status"> <?php _e( 'Status:', 'woocommerce' ); if ( $order->needs_payment() ) { printf( '<a href="%s">%s</a>', esc_url( $order->get_checkout_payment_url() ), __( 'Customer payment page →', 'woocommerce' ) ); } ?> </label> <select id="order_status" name="order_status" class="wc-enhanced-select"> <?php $statuses = wc_get_order_statuses(); foreach ( $statuses as $status => $status_name ) { echo '<option value="' . esc_attr( $status ) . '" ' . selected( $status, 'wc-' . $order->get_status( 'edit' ), false ) . '>' . esc_html( $status_name ) . '</option>'; } ?> </select> </p> <p class="form-field form-field-wide wc-customer-user"> <!--email_off--> <!-- Disable CloudFlare email obfuscation --> <label for="customer_user"> <?php _e( 'Customer:', 'woocommerce' ); if ( $order->get_user_id( 'edit' ) ) { $args = array( 'post_status' => 'all', 'post_type' => 'shop_order', '_customer_user' => $order->get_user_id( 'edit' ), ); printf( '<a href="%s">%s</a>', esc_url( add_query_arg( $args, admin_url( 'edit.php' ) ) ), ' ' . __( 'View other orders →', 'woocommerce' ) ); printf( '<a href="%s">%s</a>', esc_url( add_query_arg( 'user_id', $order->get_user_id( 'edit' ), admin_url( 'user-edit.php' ) ) ), ' ' . __( 'Profile →', 'woocommerce' ) ); } ?> </label> <?php $user_string = ''; $user_id = ''; if ( $order->get_user_id() ) { $user_id = absint( $order->get_user_id() ); $user = get_user_by( 'id', $user_id ); /* translators: 1: user display name 2: user ID 3: user email */ $user_string = sprintf( esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), $user->display_name, absint( $user->ID ), $user->user_email ); } ?> <select class="wc-customer-search" id="customer_user" name="customer_user" data-placeholder="<?php esc_attr_e( 'Guest', 'woocommerce' ); ?>" data-allow_clear="true"> <option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option> </select> <!--/email_off--> </p> <?php do_action( 'woocommerce_admin_order_data_after_order_details', $order ); ?> </div> <div class="order_data_column"> <h3> <?php esc_html_e( 'Billing', 'woocommerce' ); ?> <a href="#" class="edit_address"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> <span> <a href="#" class="load_customer_billing" style="display:none;"><?php esc_html_e( 'Load billing address', 'woocommerce' ); ?></a> </span> </h3> <div class="address"> <?php // Display values. if ( $order->get_formatted_billing_address() ) { echo '<p>' . wp_kses( $order->get_formatted_billing_address(), array( 'br' => array() ) ) . '</p>'; } else { echo '<p class="none_set"><strong>' . __( 'Address:', 'woocommerce' ) . '</strong> ' . __( 'No billing address set.', 'woocommerce' ) . '</p>'; } foreach ( self::$billing_fields as $key => $field ) { if ( isset( $field['show'] ) && false === $field['show'] ) { continue; } $field_name = 'billing_' . $key; if ( isset( $field['value'] ) ) { $field_value = $field['value']; } elseif ( is_callable( array( $order, 'get_' . $field_name ) ) ) { $field_value = $order->{"get_$field_name"}( 'edit' ); } else { $field_value = $order->get_meta( '_' . $field_name ); } if ( 'billing_phone' === $field_name ) { $field_value = wc_make_phone_clickable( $field_value ); } elseif ( 'billing_email' === $field_name ) { $field_value = '<a href="' . esc_url( 'mailto:' . $field_value ) . '">' . $field_value . '</a>'; } else { $field_value = make_clickable( esc_html( $field_value ) ); } if ( $field_value ) { echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( $field_value ) . '</p>'; } } ?> </div> <div class="edit_address"> <?php // Display form. foreach ( self::$billing_fields as $key => $field ) { if ( ! isset( $field['type'] ) ) { $field['type'] = 'text'; } if ( ! isset( $field['id'] ) ) { $field['id'] = '_billing_' . $key; } $field_name = 'billing_' . $key; if ( ! isset( $field['value'] ) ) { if ( is_callable( array( $order, 'get_' . $field_name ) ) ) { $field['value'] = $order->{"get_$field_name"}( 'edit' ); } else { $field['value'] = $order->get_meta( '_' . $field_name ); } } switch ( $field['type'] ) { case 'select': woocommerce_wp_select( $field ); break; default: woocommerce_wp_text_input( $field ); break; } } ?> <p class="form-field form-field-wide"> <label><?php esc_html_e( 'Payment method:', 'woocommerce' ); ?></label> <select name="_payment_method" id="_payment_method" class="first"> <option value=""><?php esc_html_e( 'N/A', 'woocommerce' ); ?></option> <?php $found_method = false; foreach ( $payment_gateways as $gateway ) { if ( 'yes' === $gateway->enabled ) { echo '<option value="' . esc_attr( $gateway->id ) . '" ' . selected( $payment_method, $gateway->id, false ) . '>' . esc_html( $gateway->get_title() ) . '</option>'; if ( $payment_method == $gateway->id ) { $found_method = true; } } } if ( ! $found_method && ! empty( $payment_method ) ) { echo '<option value="' . esc_attr( $payment_method ) . '" selected="selected">' . esc_html__( 'Other', 'woocommerce' ) . '</option>'; } else { echo '<option value="other">' . esc_html__( 'Other', 'woocommerce' ) . '</option>'; } ?> </select> </p> <?php woocommerce_wp_text_input( array( 'id' => '_transaction_id', 'label' => __( 'Transaction ID', 'woocommerce' ), 'value' => $order->get_transaction_id( 'edit' ), ) ); ?> </div> <?php do_action( 'woocommerce_admin_order_data_after_billing_address', $order ); ?> </div> <div class="order_data_column"> <h3> <?php esc_html_e( 'Shipping', 'woocommerce' ); ?> <a href="#" class="edit_address"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> <span> <a href="#" class="load_customer_shipping" style="display:none;"><?php esc_html_e( 'Load shipping address', 'woocommerce' ); ?></a> <a href="#" class="billing-same-as-shipping" style="display:none;"><?php esc_html_e( 'Copy billing address', 'woocommerce' ); ?></a> </span> </h3> <div class="address"> <?php // Display values. if ( $order->get_formatted_shipping_address() ) { echo '<p>' . wp_kses( $order->get_formatted_shipping_address(), array( 'br' => array() ) ) . '</p>'; } else { echo '<p class="none_set"><strong>' . __( 'Address:', 'woocommerce' ) . '</strong> ' . __( 'No shipping address set.', 'woocommerce' ) . '</p>'; } if ( ! empty( self::$shipping_fields ) ) { foreach ( self::$shipping_fields as $key => $field ) { if ( isset( $field['show'] ) && false === $field['show'] ) { continue; } $field_name = 'shipping_' . $key; if ( is_callable( array( $order, 'get_' . $field_name ) ) ) { $field_value = $order->{"get_$field_name"}( 'edit' ); } else { $field_value = $order->get_meta( '_' . $field_name ); } if ( 'shipping_phone' === $field_name ) { $field_value = wc_make_phone_clickable( $field_value ); } if ( $field_value ) { echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( $field_value ) . '</p>'; } } } if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) && $post->post_excerpt ) { echo '<p class="order_note"><strong>' . __( 'Customer provided note:', 'woocommerce' ) . '</strong> ' . nl2br( esc_html( $post->post_excerpt ) ) . '</p>'; } ?> </div> <div class="edit_address"> <?php // Display form. if ( ! empty( self::$shipping_fields ) ) { foreach ( self::$shipping_fields as $key => $field ) { if ( ! isset( $field['type'] ) ) { $field['type'] = 'text'; } if ( ! isset( $field['id'] ) ) { $field['id'] = '_shipping_' . $key; } $field_name = 'shipping_' . $key; if ( is_callable( array( $order, 'get_' . $field_name ) ) ) { $field['value'] = $order->{"get_$field_name"}( 'edit' ); } else { $field['value'] = $order->get_meta( '_' . $field_name ); } switch ( $field['type'] ) { case 'select': woocommerce_wp_select( $field ); break; default: woocommerce_wp_text_input( $field ); break; } } } if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) : ?> <p class="form-field form-field-wide"> <label for="excerpt"><?php _e( 'Customer provided note', 'woocommerce' ); ?>:</label> <textarea rows="1" cols="40" name="excerpt" tabindex="6" id="excerpt" placeholder="<?php esc_attr_e( 'Customer notes about the order', 'woocommerce' ); ?>"><?php echo wp_kses_post( $post->post_excerpt ); ?></textarea> </p> <?php endif; ?> </div> <?php do_action( 'woocommerce_admin_order_data_after_shipping_address', $order ); ?> </div> </div> <div class="clear"></div> </div> </div> <?php } /** * Save meta box data. * * @param int $order_id Order ID. */ public static function save( $order_id ) { self::init_address_fields(); // Ensure gateways are loaded in case they need to insert data into the emails. WC()->payment_gateways(); WC()->shipping(); // Get order object. $order = wc_get_order( $order_id ); $props = array(); // Create order key. if ( ! $order->get_order_key() ) { $props['order_key'] = wc_generate_order_key(); } // Update customer. $customer_id = isset( $_POST['customer_user'] ) ? absint( $_POST['customer_user'] ) : 0; if ( $customer_id !== $order->get_customer_id() ) { $props['customer_id'] = $customer_id; } // Update billing fields. if ( ! empty( self::$billing_fields ) ) { foreach ( self::$billing_fields as $key => $field ) { if ( ! isset( $field['id'] ) ) { $field['id'] = '_billing_' . $key; } if ( ! isset( $_POST[ $field['id'] ] ) ) { continue; } if ( is_callable( array( $order, 'set_billing_' . $key ) ) ) { $props[ 'billing_' . $key ] = wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ); } else { $order->update_meta_data( $field['id'], wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ) ); } } } // Update shipping fields. if ( ! empty( self::$shipping_fields ) ) { foreach ( self::$shipping_fields as $key => $field ) { if ( ! isset( $field['id'] ) ) { $field['id'] = '_shipping_' . $key; } if ( ! isset( $_POST[ $field['id'] ] ) ) { continue; } if ( is_callable( array( $order, 'set_shipping_' . $key ) ) ) { $props[ 'shipping_' . $key ] = wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ); } else { $order->update_meta_data( $field['id'], wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ) ); } } } if ( isset( $_POST['_transaction_id'] ) ) { $props['transaction_id'] = wc_clean( wp_unslash( $_POST['_transaction_id'] ) ); } // Payment method handling. if ( $order->get_payment_method() !== wp_unslash( $_POST['_payment_method'] ) ) { $methods = WC()->payment_gateways->payment_gateways(); $payment_method = wc_clean( wp_unslash( $_POST['_payment_method'] ) ); $payment_method_title = $payment_method; if ( isset( $methods ) && isset( $methods[ $payment_method ] ) ) { $payment_method_title = $methods[ $payment_method ]->get_title(); } if ( $payment_method == 'other') { $payment_method_title = esc_html__( 'Other', 'woocommerce' ); } $props['payment_method'] = $payment_method; $props['payment_method_title'] = $payment_method_title; } // Update date. if ( empty( $_POST['order_date'] ) ) { $date = time(); } else { $date = gmdate( 'Y-m-d H:i:s', strtotime( $_POST['order_date'] . ' ' . (int) $_POST['order_date_hour'] . ':' . (int) $_POST['order_date_minute'] . ':' . (int) $_POST['order_date_second'] ) ); } $props['date_created'] = $date; // Set created via prop if new post. if ( isset( $_POST['original_post_status'] ) && $_POST['original_post_status'] === 'auto-draft' ) { $props['created_via'] = 'admin'; } // Save order data. $order->set_props( $props ); $order->set_status( wc_clean( wp_unslash( $_POST['order_status'] ) ), '', true ); $order->save(); } } includes/admin/meta-boxes/class-wc-meta-box-order-items.php 0000644 00000002015 15132754524 0017703 0 ustar 00 <?php /** * Order Data * * Functions for displaying the order items meta box. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Meta_Box_Order_Items Class. */ class WC_Meta_Box_Order_Items { /** * Output the metabox. * * @param WP_Post $post */ public static function output( $post ) { global $post, $thepostid, $theorder; if ( ! is_int( $thepostid ) ) { $thepostid = $post->ID; } if ( ! is_object( $theorder ) ) { $theorder = wc_get_order( $thepostid ); } $order = $theorder; $data = get_post_meta( $post->ID ); include __DIR__ . '/views/html-order-items.php'; } /** * Save meta box data. * * @param int $post_id */ public static function save( $post_id ) { /** * This $_POST variable's data has been validated and escaped * inside `wc_save_order_items()` function. */ wc_save_order_items( $post_id, $_POST ); } } includes/admin/meta-boxes/class-wc-meta-box-order-actions.php 0000644 00000013541 15132754524 0020230 0 ustar 00 <?php /** * Order Actions * * Functions for displaying the order actions meta box. * * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC_Meta_Box_Order_Actions Class. */ class WC_Meta_Box_Order_Actions { /** * Output the metabox. * * @param WP_Post $post Post object. */ public static function output( $post ) { global $theorder; // This is used by some callbacks attached to hooks such as woocommerce_order_actions which rely on the global to determine if actions should be displayed for certain orders. // Avoid using this global with the `woocommerce_order_actions` filter, instead use the $order filter arg. if ( ! is_object( $theorder ) ) { $theorder = wc_get_order( $post->ID ); } $theorder = $theorder instanceof WC_Order ? $theorder : null; $order_actions = self::get_available_order_actions_for_order( $theorder ); ?> <ul class="order_actions submitbox"> <?php do_action( 'woocommerce_order_actions_start', $post->ID ); ?> <li class="wide" id="actions"> <select name="wc_order_action"> <option value=""><?php esc_html_e( 'Choose an action...', 'woocommerce' ); ?></option> <?php foreach ( $order_actions as $action => $title ) { ?> <option value="<?php echo esc_attr( $action ); ?>"><?php echo esc_html( $title ); ?></option> <?php } ?> </select> <button class="button wc-reload"><span><?php esc_html_e( 'Apply', 'woocommerce' ); ?></span></button> </li> <li class="wide"> <div id="delete-action"> <?php if ( current_user_can( 'delete_post', $post->ID ) ) { if ( ! EMPTY_TRASH_DAYS ) { $delete_text = __( 'Delete permanently', 'woocommerce' ); } else { $delete_text = __( 'Move to Trash', 'woocommerce' ); } ?> <a class="submitdelete deletion" href="<?php echo esc_url( get_delete_post_link( $post->ID ) ); ?>"><?php echo esc_html( $delete_text ); ?></a> <?php } ?> </div> <button type="submit" class="button save_order button-primary" name="save" value="<?php echo 'auto-draft' === $post->post_status ? esc_attr__( 'Create', 'woocommerce' ) : esc_attr__( 'Update', 'woocommerce' ); ?>"><?php echo 'auto-draft' === $post->post_status ? esc_html__( 'Create', 'woocommerce' ) : esc_html__( 'Update', 'woocommerce' ); ?></button> </li> <?php do_action( 'woocommerce_order_actions_end', $post->ID ); ?> </ul> <?php } /** * Save meta box data. * * @param int $post_id Post ID. * @param WP_Post $post Post Object. */ public static function save( $post_id, $post ) { // Order data saved, now get it so we can manipulate status. $order = wc_get_order( $post_id ); // Handle button actions. if ( ! empty( $_POST['wc_order_action'] ) ) { // @codingStandardsIgnoreLine $action = wc_clean( wp_unslash( $_POST['wc_order_action'] ) ); // @codingStandardsIgnoreLine if ( 'send_order_details' === $action ) { do_action( 'woocommerce_before_resend_order_emails', $order, 'customer_invoice' ); // Send the customer invoice email. WC()->payment_gateways(); WC()->shipping(); WC()->mailer()->customer_invoice( $order ); // Note the event. $order->add_order_note( __( 'Order details manually sent to customer.', 'woocommerce' ), false, true ); do_action( 'woocommerce_after_resend_order_email', $order, 'customer_invoice' ); // Change the post saved message. add_filter( 'redirect_post_location', array( __CLASS__, 'set_email_sent_message' ) ); } elseif ( 'send_order_details_admin' === $action ) { do_action( 'woocommerce_before_resend_order_emails', $order, 'new_order' ); WC()->payment_gateways(); WC()->shipping(); add_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' ); WC()->mailer()->emails['WC_Email_New_Order']->trigger( $order->get_id(), $order, true ); remove_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' ); do_action( 'woocommerce_after_resend_order_email', $order, 'new_order' ); // Change the post saved message. add_filter( 'redirect_post_location', array( __CLASS__, 'set_email_sent_message' ) ); } elseif ( 'regenerate_download_permissions' === $action ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->delete_by_order_id( $post_id ); wc_downloadable_product_permissions( $post_id, true ); } else { if ( ! did_action( 'woocommerce_order_action_' . sanitize_title( $action ) ) ) { do_action( 'woocommerce_order_action_' . sanitize_title( $action ), $order ); } } } } /** * Set the correct message ID. * * @param string $location Location. * @since 2.3.0 * @static * @return string */ public static function set_email_sent_message( $location ) { return add_query_arg( 'message', 11, $location ); } /** * Get the available order actions for a given order. * * @since 5.8.0 * * @param WC_Order|null $order The order object or null if no order is available. * * @return array */ private static function get_available_order_actions_for_order( $order ) { $actions = array( 'send_order_details' => __( 'Email invoice / order details to customer', 'woocommerce' ), 'send_order_details_admin' => __( 'Resend new order notification', 'woocommerce' ), 'regenerate_download_permissions' => __( 'Regenerate download permissions', 'woocommerce' ), ); /** * Filter: woocommerce_order_actions * Allows filtering of the available order actions for an order. * * @since 2.1.0 Filter was added. * @since 5.8.0 The $order param was added. * * @param array $actions The available order actions for the order. * @param WC_Order|null $order The order object or null if no order is available. */ return apply_filters( 'woocommerce_order_actions', $actions, $order ); } } includes/admin/meta-boxes/class-wc-meta-box-product-images.php 0000644 00000006675 15132754524 0020414 0 ustar 00 <?php /** * Product Images * * Display the product images meta box. * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Meta_Box_Product_Images Class. */ class WC_Meta_Box_Product_Images { /** * Output the metabox. * * @param WP_Post $post */ public static function output( $post ) { global $thepostid, $product_object; $thepostid = $post->ID; $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); ?> <div id="product_images_container"> <ul class="product_images"> <?php $product_image_gallery = $product_object->get_gallery_image_ids( 'edit' ); $attachments = array_filter( $product_image_gallery ); $update_meta = false; $updated_gallery_ids = array(); if ( ! empty( $attachments ) ) { foreach ( $attachments as $attachment_id ) { $attachment = wp_get_attachment_image( $attachment_id, 'thumbnail' ); // if attachment is empty skip. if ( empty( $attachment ) ) { $update_meta = true; continue; } ?> <li class="image" data-attachment_id="<?php echo esc_attr( $attachment_id ); ?>"> <?php echo $attachment; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <ul class="actions"> <li><a href="#" class="delete tips" data-tip="<?php esc_attr_e( 'Delete image', 'woocommerce' ); ?>"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></li> </ul> <?php // Allow for extra info to be exposed or extra action to be executed for this attachment. do_action( 'woocommerce_admin_after_product_gallery_item', $thepostid, $attachment_id ); ?> </li> <?php // rebuild ids to be saved. $updated_gallery_ids[] = $attachment_id; } // need to update product meta to set new gallery ids if ( $update_meta ) { update_post_meta( $post->ID, '_product_image_gallery', implode( ',', $updated_gallery_ids ) ); } } ?> </ul> <input type="hidden" id="product_image_gallery" name="product_image_gallery" value="<?php echo esc_attr( implode( ',', $updated_gallery_ids ) ); ?>" /> </div> <p class="add_product_images hide-if-no-js"> <a href="#" data-choose="<?php esc_attr_e( 'Add images to product gallery', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Add to gallery', 'woocommerce' ); ?>" data-delete="<?php esc_attr_e( 'Delete image', 'woocommerce' ); ?>" data-text="<?php esc_attr_e( 'Delete', 'woocommerce' ); ?>"><?php esc_html_e( 'Add product gallery images', 'woocommerce' ); ?></a> </p> <?php } /** * Save meta box data. * * @param int $post_id * @param WP_Post $post */ public static function save( $post_id, $post ) { $product_type = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( stripslashes( $_POST['product-type'] ) ); $classname = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : 'simple' ); $product = new $classname( $post_id ); $attachment_ids = isset( $_POST['product_image_gallery'] ) ? array_filter( explode( ',', wc_clean( $_POST['product_image_gallery'] ) ) ) : array(); $product->set_gallery_image_ids( $attachment_ids ); $product->save(); } } includes/admin/meta-boxes/class-wc-meta-box-order-notes.php 0000644 00000002527 15132754524 0017722 0 ustar 00 <?php /** * Order Notes * * @package WooCommerce\Admin\Meta Boxes */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Meta_Box_Order_Notes Class. */ class WC_Meta_Box_Order_Notes { /** * Output the metabox. * * @param WP_Post $post Post object. */ public static function output( $post ) { global $post; $args = array( 'order_id' => $post->ID, ); $notes = wc_get_order_notes( $args ); include __DIR__ . '/views/html-order-notes.php'; ?> <div class="add_note"> <p> <label for="add_order_note"><?php esc_html_e( 'Add note', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Add a note for your reference, or add a customer note (the user will be notified).', 'woocommerce' ) ); ?></label> <textarea type="text" name="order_note" id="add_order_note" class="input-text" cols="20" rows="5"></textarea> </p> <p> <label for="order_note_type" class="screen-reader-text"><?php esc_html_e( 'Note type', 'woocommerce' ); ?></label> <select name="order_note_type" id="order_note_type"> <option value=""><?php esc_html_e( 'Private note', 'woocommerce' ); ?></option> <option value="customer"><?php esc_html_e( 'Note to customer', 'woocommerce' ); ?></option> </select> <button type="button" class="add_note button"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button> </p> </div> <?php } } includes/admin/meta-boxes/class-wc-meta-box-product-short-description.php 0000644 00000002114 15132754524 0022607 0 ustar 00 <?php /** * Product Short Description * * Replaces the standard excerpt box. * * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Meta_Box_Product_Short_Description Class. */ class WC_Meta_Box_Product_Short_Description { /** * Output the metabox. * * @param WP_Post $post Post object. */ public static function output( $post ) { $settings = array( 'textarea_name' => 'excerpt', 'quicktags' => array( 'buttons' => 'em,strong,link' ), 'tinymce' => array( 'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator', 'theme_advanced_buttons2' => '', ), 'editor_css' => '<style>#wp-excerpt-editor-container .wp-editor-area{height:175px; width:100%;}</style>', ); wp_editor( htmlspecialchars_decode( $post->post_excerpt, ENT_QUOTES ), 'excerpt', apply_filters( 'woocommerce_product_short_description_editor_settings', $settings ) ); } } includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php 0000644 00000006062 15132754524 0020562 0 ustar 00 <?php /** * Order Downloads * * @author WooThemes * @category Admin * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * WC_Meta_Box_Order_Downloads Class. */ class WC_Meta_Box_Order_Downloads { /** * Output the metabox. * * @param WP_Post $post */ public static function output( $post ) { ?> <div class="order_download_permissions wc-metaboxes-wrapper"> <div class="wc-metaboxes"> <?php $data_store = WC_Data_Store::load( 'customer-download' ); $download_permissions = $data_store->get_downloads( array( 'order_id' => $post->ID, 'orderby' => 'product_id', ) ); $product = null; $loop = 0; $file_counter = 1; if ( $download_permissions && sizeof( $download_permissions ) > 0 ) { foreach ( $download_permissions as $download ) { if ( ! $product || $product->get_id() !== $download->get_product_id() ) { $product = wc_get_product( $download->get_product_id() ); $file_counter = 1; } // don't show permissions to files that have since been removed. if ( ! $product || ! $product->exists() || ! $product->has_file( $download->get_download_id() ) ) { continue; } // Show file title instead of count if set. $file = $product->get_file( $download->get_download_id() ); $file_count = isset( $file['name'] ) ? $file['name'] : sprintf( __( 'File %d', 'woocommerce' ), $file_counter ); include __DIR__ . '/views/html-order-download-permission.php'; $loop++; $file_counter++; } } ?> </div> <div class="toolbar"> <p class="buttons"> <select id="grant_access_id" class="wc-product-search" name="grant_access_id[]" multiple="multiple" style="width: 400px;" data-placeholder="<?php esc_attr_e( 'Search for a downloadable product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_downloadable_products_and_variations"></select> <button type="button" class="button grant_access"> <?php _e( 'Grant access', 'woocommerce' ); ?> </button> </p> <div class="clear"></div> </div> </div> <?php } /** * Save meta box data. * * @param int $post_id * @param WP_Post $post */ public static function save( $post_id, $post ) { if ( isset( $_POST['permission_id'] ) ) { $permission_ids = $_POST['permission_id']; $downloads_remaining = $_POST['downloads_remaining']; $access_expires = $_POST['access_expires']; $max = max( array_keys( $permission_ids ) ); for ( $i = 0; $i <= $max; $i ++ ) { if ( ! isset( $permission_ids[ $i ] ) ) { continue; } $download = new WC_Customer_Download( $permission_ids[ $i ] ); $download->set_downloads_remaining( wc_clean( $downloads_remaining[ $i ] ) ); $download->set_access_expires( array_key_exists( $i, $access_expires ) && '' !== $access_expires[ $i ] ? strtotime( $access_expires[ $i ] ) : '' ); $download->save(); } } } } includes/class-wc-cli.php 0000644 00000002246 15132754524 0011360 0 ustar 00 <?php /** * Enables WooCommerce, via the the command line. * * @package WooCommerce\CLI * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * CLI class. */ class WC_CLI { /** * Load required files and hooks to make the CLI work. */ public function __construct() { $this->includes(); $this->hooks(); } /** * Load command files. */ private function includes() { require_once dirname( __FILE__ ) . '/cli/class-wc-cli-runner.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-rest-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tool-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-update-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tracker-command.php'; } /** * Sets up and hooks WP CLI to our CLI code. */ private function hooks() { WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Runner::after_wp_load' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' ); } } new WC_CLI(); includes/class-wc-checkout.php 0000644 00000130007 15132754524 0012413 0 ustar 00 <?php /** * Checkout functionality * * The WooCommerce checkout class handles the checkout process, collecting user data and processing the payment. * * @package WooCommerce\Classes * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * Checkout class. */ class WC_Checkout { /** * The single instance of the class. * * @var WC_Checkout|null */ protected static $instance = null; /** * Checkout fields are stored here. * * @var array|null */ protected $fields = null; /** * Holds posted data for backwards compatibility. * * @var array */ protected $legacy_posted_data = array(); /** * Caches customer object. @see get_value. * * @var WC_Customer */ private $logged_in_customer = null; /** * Gets the main WC_Checkout Instance. * * @since 2.1 * @static * @return WC_Checkout Main instance */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); // Hook in actions once. add_action( 'woocommerce_checkout_billing', array( self::$instance, 'checkout_form_billing' ) ); add_action( 'woocommerce_checkout_shipping', array( self::$instance, 'checkout_form_shipping' ) ); // woocommerce_checkout_init action is ran once when the class is first constructed. do_action( 'woocommerce_checkout_init', self::$instance ); } return self::$instance; } /** * See if variable is set. Used to support legacy public variables which are no longer defined. * * @param string $key Key. * @return bool */ public function __isset( $key ) { return in_array( $key, array( 'enable_signup', 'enable_guest_checkout', 'must_create_account', 'checkout_fields', 'posted', 'shipping_method', 'payment_method', 'customer_id', 'shipping_methods', ), true ); } /** * Sets the legacy public variables for backwards compatibility. * * @param string $key Key. * @param mixed $value Value. */ public function __set( $key, $value ) { switch ( $key ) { case 'enable_signup': $bool_value = wc_string_to_bool( $value ); if ( $bool_value !== $this->is_registration_enabled() ) { remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true', 0 ); remove_filter( 'woocommerce_checkout_registration_enabled', '__return_false', 0 ); add_filter( 'woocommerce_checkout_registration_enabled', $bool_value ? '__return_true' : '__return_false', 0 ); } break; case 'enable_guest_checkout': $bool_value = wc_string_to_bool( $value ); if ( $bool_value === $this->is_registration_required() ) { remove_filter( 'woocommerce_checkout_registration_required', '__return_true', 0 ); remove_filter( 'woocommerce_checkout_registration_required', '__return_false', 0 ); add_filter( 'woocommerce_checkout_registration_required', $bool_value ? '__return_false' : '__return_true', 0 ); } break; case 'checkout_fields': $this->fields = $value; break; case 'shipping_methods': WC()->session->set( 'chosen_shipping_methods', $value ); break; case 'posted': $this->legacy_posted_data = $value; break; } } /** * Gets the legacy public variables for backwards compatibility. * * @param string $key Key. * @return array|string */ public function __get( $key ) { if ( in_array( $key, array( 'posted', 'shipping_method', 'payment_method' ), true ) && empty( $this->legacy_posted_data ) ) { $this->legacy_posted_data = $this->get_posted_data(); } switch ( $key ) { case 'enable_signup': return $this->is_registration_enabled(); case 'enable_guest_checkout': return ! $this->is_registration_required(); case 'must_create_account': return $this->is_registration_required() && ! is_user_logged_in(); case 'checkout_fields': return $this->get_checkout_fields(); case 'posted': wc_doing_it_wrong( 'WC_Checkout->posted', 'Use $_POST directly.', '3.0.0' ); return $this->legacy_posted_data; case 'shipping_method': return $this->legacy_posted_data['shipping_method']; case 'payment_method': return $this->legacy_posted_data['payment_method']; case 'customer_id': return apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); case 'shipping_methods': return (array) WC()->session->get( 'chosen_shipping_methods' ); } } /** * Cloning is forbidden. */ public function __clone() { wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. */ public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' ); } /** * Is registration required to checkout? * * @since 3.0.0 * @return boolean */ public function is_registration_required() { return apply_filters( 'woocommerce_checkout_registration_required', 'yes' !== get_option( 'woocommerce_enable_guest_checkout' ) ); } /** * Is registration enabled on the checkout page? * * @since 3.0.0 * @return boolean */ public function is_registration_enabled() { return apply_filters( 'woocommerce_checkout_registration_enabled', 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) ); } /** * Get an array of checkout fields. * * @param string $fieldset to get. * @return array */ public function get_checkout_fields( $fieldset = '' ) { if ( ! is_null( $this->fields ) ) { return $fieldset ? $this->fields[ $fieldset ] : $this->fields; } // Fields are based on billing/shipping country. Grab those values but ensure they are valid for the store before using. $billing_country = $this->get_value( 'billing_country' ); $billing_country = empty( $billing_country ) ? WC()->countries->get_base_country() : $billing_country; $allowed_countries = WC()->countries->get_allowed_countries(); if ( ! array_key_exists( $billing_country, $allowed_countries ) ) { $billing_country = current( array_keys( $allowed_countries ) ); } $shipping_country = $this->get_value( 'shipping_country' ); $shipping_country = empty( $shipping_country ) ? WC()->countries->get_base_country() : $shipping_country; $allowed_countries = WC()->countries->get_shipping_countries(); if ( ! array_key_exists( $shipping_country, $allowed_countries ) ) { $shipping_country = current( array_keys( $allowed_countries ) ); } $this->fields = array( 'billing' => WC()->countries->get_address_fields( $billing_country, 'billing_' ), 'shipping' => WC()->countries->get_address_fields( $shipping_country, 'shipping_' ), 'account' => array(), 'order' => array( 'order_comments' => array( 'type' => 'textarea', 'class' => array( 'notes' ), 'label' => __( 'Order notes', 'woocommerce' ), 'placeholder' => esc_attr__( 'Notes about your order, e.g. special notes for delivery.', 'woocommerce' ), ), ), ); if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) { $this->fields['account']['account_username'] = array( 'type' => 'text', 'label' => __( 'Account username', 'woocommerce' ), 'required' => true, 'placeholder' => esc_attr__( 'Username', 'woocommerce' ), ); } if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { $this->fields['account']['account_password'] = array( 'type' => 'password', 'label' => __( 'Create account password', 'woocommerce' ), 'required' => true, 'placeholder' => esc_attr__( 'Password', 'woocommerce' ), ); } $this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields ); foreach ( $this->fields as $field_type => $fields ) { // Sort each of the checkout field sections based on priority. uasort( $this->fields[ $field_type ], 'wc_checkout_fields_uasort_comparison' ); // Add accessibility labels to fields that have placeholders. foreach ( $fields as $single_field_type => $field ) { if ( empty( $field['label'] ) && ! empty( $field['placeholder'] ) ) { $this->fields[ $field_type ][ $single_field_type ]['label'] = $field['placeholder']; $this->fields[ $field_type ][ $single_field_type ]['label_class'] = array( 'screen-reader-text' ); } } } return $fieldset ? $this->fields[ $fieldset ] : $this->fields; } /** * When we process the checkout, lets ensure cart items are rechecked to prevent checkout. */ public function check_cart_items() { do_action( 'woocommerce_check_cart_items' ); } /** * Output the billing form. */ public function checkout_form_billing() { wc_get_template( 'checkout/form-billing.php', array( 'checkout' => $this ) ); } /** * Output the shipping form. */ public function checkout_form_shipping() { wc_get_template( 'checkout/form-shipping.php', array( 'checkout' => $this ) ); } /** * Create an order. Error codes: * 520 - Cannot insert order into the database. * 521 - Cannot get order after creation. * 522 - Cannot update order. * 525 - Cannot create line item. * 526 - Cannot create fee item. * 527 - Cannot create shipping item. * 528 - Cannot create tax item. * 529 - Cannot create coupon item. * * @throws Exception When checkout validation fails. * @param array $data Posted data. * @return int|WP_ERROR */ public function create_order( $data ) { // Give plugins the opportunity to create an order themselves. $order_id = apply_filters( 'woocommerce_create_order', null, $this ); if ( $order_id ) { return $order_id; } try { $order_id = absint( WC()->session->get( 'order_awaiting_payment' ) ); $cart_hash = WC()->cart->get_cart_hash(); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); $order = $order_id ? wc_get_order( $order_id ) : null; /** * If there is an order pending payment, we can resume it here so * long as it has not changed. If the order has changed, i.e. * different items or cost, create a new order. We use a hash to * detect changes which is based on cart items + order total. */ if ( $order && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) { // Action for 3rd parties. do_action( 'woocommerce_resume_order', $order_id ); // Remove all items - we will re-add them later. $order->remove_order_items(); } else { $order = new WC_Order(); } $fields_prefix = array( 'shipping' => true, 'billing' => true, ); $shipping_fields = array( 'shipping_method' => true, 'shipping_total' => true, 'shipping_tax' => true, ); foreach ( $data as $key => $value ) { if ( is_callable( array( $order, "set_{$key}" ) ) ) { $order->{"set_{$key}"}( $value ); // Store custom fields prefixed with wither shipping_ or billing_. This is for backwards compatibility with 2.6.x. } elseif ( isset( $fields_prefix[ current( explode( '_', $key ) ) ] ) ) { if ( ! isset( $shipping_fields[ $key ] ) ) { $order->update_meta_data( '_' . $key, $value ); } } } $order->hold_applied_coupons( $data['billing_email'] ); $order->set_created_via( 'checkout' ); $order->set_cart_hash( $cart_hash ); $order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) ); $order->set_currency( get_woocommerce_currency() ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); $order->set_customer_user_agent( wc_get_user_agent() ); $order->set_customer_note( isset( $data['order_comments'] ) ? $data['order_comments'] : '' ); $order->set_payment_method( isset( $available_gateways[ $data['payment_method'] ] ) ? $available_gateways[ $data['payment_method'] ] : $data['payment_method'] ); $this->set_data_from_cart( $order ); /** * Action hook to adjust order before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order', $order, $data ); // Save the order. $order_id = $order->save(); /** * Action hook fired after an order is created used to add custom meta to the order. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data ); /** * Action hook fired after an order is created. * * @since 4.3.0 */ do_action( 'woocommerce_checkout_order_created', $order ); return $order_id; } catch ( Exception $e ) { if ( $order && $order instanceof WC_Order ) { $order->get_data_store()->release_held_coupons( $order ); /** * Action hook fired when an order is discarded due to Exception. * * @since 4.3.0 */ do_action( 'woocommerce_checkout_order_exception', $order ); } return new WP_Error( 'checkout-error', $e->getMessage() ); } } /** * Copy line items, tax, totals data from cart to order. * * @param WC_Order $order Order object. * * @throws Exception When unable to create order. */ public function set_data_from_cart( &$order ) { $order_vat_exempt = WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no'; $order->add_meta_data( 'is_vat_exempt', $order_vat_exempt, true ); $order->set_shipping_total( WC()->cart->get_shipping_total() ); $order->set_discount_total( WC()->cart->get_discount_total() ); $order->set_discount_tax( WC()->cart->get_discount_tax() ); $order->set_cart_tax( WC()->cart->get_cart_contents_tax() + WC()->cart->get_fee_tax() ); $order->set_shipping_tax( WC()->cart->get_shipping_tax() ); $order->set_total( WC()->cart->get_total( 'edit' ) ); $this->create_order_line_items( $order, WC()->cart ); $this->create_order_fee_lines( $order, WC()->cart ); $this->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() ); $this->create_order_tax_lines( $order, WC()->cart ); $this->create_order_coupon_lines( $order, WC()->cart ); } /** * Add line items to the order. * * @param WC_Order $order Order instance. * @param WC_Cart $cart Cart instance. */ public function create_order_line_items( &$order, $cart ) { foreach ( $cart->get_cart() as $cart_item_key => $values ) { /** * Filter hook to get initial item object. * * @since 3.1.0 */ $item = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order ); $product = $values['data']; $item->legacy_values = $values; // @deprecated 4.4.0 For legacy actions. $item->legacy_cart_item_key = $cart_item_key; // @deprecated 4.4.0 For legacy actions. $item->set_props( array( 'quantity' => $values['quantity'], 'variation' => $values['variation'], 'subtotal' => $values['line_subtotal'], 'total' => $values['line_total'], 'subtotal_tax' => $values['line_subtotal_tax'], 'total_tax' => $values['line_tax'], 'taxes' => $values['line_tax_data'], ) ); if ( $product ) { $item->set_props( array( 'name' => $product->get_name(), 'tax_class' => $product->get_tax_class(), 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, ) ); } $item->set_backorder_meta(); /** * Action hook to adjust item before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order_line_item', $item, $cart_item_key, $values, $order ); // Add item to order and save. $order->add_item( $item ); } } /** * Add fees to the order. * * @param WC_Order $order Order instance. * @param WC_Cart $cart Cart instance. */ public function create_order_fee_lines( &$order, $cart ) { foreach ( $cart->get_fees() as $fee_key => $fee ) { $item = new WC_Order_Item_Fee(); $item->legacy_fee = $fee; // @deprecated 4.4.0 For legacy actions. $item->legacy_fee_key = $fee_key; // @deprecated 4.4.0 For legacy actions. $item->set_props( array( 'name' => $fee->name, 'tax_class' => $fee->taxable ? $fee->tax_class : 0, 'amount' => $fee->amount, 'total' => $fee->total, 'total_tax' => $fee->tax, 'taxes' => array( 'total' => $fee->tax_data, ), ) ); /** * Action hook to adjust item before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order_fee_item', $item, $fee_key, $fee, $order ); // Add item to order and save. $order->add_item( $item ); } } /** * Add shipping lines to the order. * * @param WC_Order $order Order Instance. * @param array $chosen_shipping_methods Chosen shipping methods. * @param array $packages Packages. */ public function create_order_shipping_lines( &$order, $chosen_shipping_methods, $packages ) { foreach ( $packages as $package_key => $package ) { if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) { $shipping_rate = $package['rates'][ $chosen_shipping_methods[ $package_key ] ]; $item = new WC_Order_Item_Shipping(); $item->legacy_package_key = $package_key; // @deprecated 4.4.0 For legacy actions. $item->set_props( array( 'method_title' => $shipping_rate->label, 'method_id' => $shipping_rate->method_id, 'instance_id' => $shipping_rate->instance_id, 'total' => wc_format_decimal( $shipping_rate->cost ), 'taxes' => array( 'total' => $shipping_rate->taxes, ), ) ); foreach ( $shipping_rate->get_meta_data() as $key => $value ) { $item->add_meta_data( $key, $value, true ); } /** * Action hook to adjust item before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order ); // Add item to order and save. $order->add_item( $item ); } } } /** * Add tax lines to the order. * * @param WC_Order $order Order instance. * @param WC_Cart $cart Cart instance. */ public function create_order_tax_lines( &$order, $cart ) { foreach ( array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() ) as $tax_rate_id ) { if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) { $item = new WC_Order_Item_Tax(); $item->set_props( array( 'rate_id' => $tax_rate_id, 'tax_total' => $cart->get_tax_amount( $tax_rate_id ), 'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ), 'rate_code' => WC_Tax::get_rate_code( $tax_rate_id ), 'label' => WC_Tax::get_rate_label( $tax_rate_id ), 'compound' => WC_Tax::is_compound( $tax_rate_id ), 'rate_percent' => WC_Tax::get_rate_percent_value( $tax_rate_id ), ) ); /** * Action hook to adjust item before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order_tax_item', $item, $tax_rate_id, $order ); // Add item to order and save. $order->add_item( $item ); } } } /** * Add coupon lines to the order. * * @param WC_Order $order Order instance. * @param WC_Cart $cart Cart instance. */ public function create_order_coupon_lines( &$order, $cart ) { foreach ( $cart->get_coupons() as $code => $coupon ) { $item = new WC_Order_Item_Coupon(); $item->set_props( array( 'code' => $code, 'discount' => $cart->get_coupon_discount_amount( $code ), 'discount_tax' => $cart->get_coupon_discount_tax_amount( $code ), ) ); // Avoid storing used_by - it's not needed and can get large. $coupon_data = $coupon->get_data(); unset( $coupon_data['used_by'] ); $item->add_meta_data( 'coupon_data', $coupon_data ); /** * Action hook to adjust item before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_create_order_coupon_item', $item, $code, $coupon, $order ); // Add item to order and save. $order->add_item( $item ); } } /** * See if a fieldset should be skipped. * * @since 3.0.0 * @param string $fieldset_key Fieldset key. * @param array $data Posted data. * @return bool */ protected function maybe_skip_fieldset( $fieldset_key, $data ) { if ( 'shipping' === $fieldset_key && ( ! $data['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) { return true; } if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( ! $this->is_registration_required() && empty( $data['createaccount'] ) ) ) ) { return true; } return false; } /** * Get posted data from the checkout form. * * @since 3.1.0 * @return array of data. */ public function get_posted_data() { // phpcs:disable WordPress.Security.NonceVerification.Missing $data = array( 'terms' => (int) isset( $_POST['terms'] ), 'createaccount' => (int) ( $this->is_registration_enabled() ? ! empty( $_POST['createaccount'] ) : false ), 'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', 'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', 'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), 'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), ); // phpcs:enable WordPress.Security.NonceVerification.Missing $skipped = array(); $form_was_shown = isset( $_POST['woocommerce-process-checkout-nonce'] ); // phpcs:disable WordPress.Security.NonceVerification.Missing foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { $skipped[] = $fieldset_key; continue; } foreach ( $fieldset as $key => $field ) { $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' ); if ( isset( $_POST[ $key ] ) && '' !== $_POST[ $key ] ) { // phpcs:disable WordPress.Security.NonceVerification.Missing $value = wp_unslash( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( isset( $field['default'] ) && 'checkbox' !== $type && ! $form_was_shown ) { $value = $field['default']; } else { $value = ''; } if ( '' !== $value ) { switch ( $type ) { case 'checkbox': $value = 1; break; case 'multiselect': $value = implode( ', ', wc_clean( $value ) ); break; case 'textarea': $value = wc_sanitize_textarea( $value ); break; case 'password': break; default: $value = wc_clean( $value ); break; } } $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) ); } } if ( in_array( 'shipping', $skipped, true ) && ( WC()->cart->needs_shipping_address() || wc_ship_to_billing_address_only() ) ) { foreach ( $this->get_checkout_fields( 'shipping' ) as $key => $field ) { $data[ $key ] = isset( $data[ 'billing_' . substr( $key, 9 ) ] ) ? $data[ 'billing_' . substr( $key, 9 ) ] : ''; } } // BW compatibility. $this->legacy_posted_data = $data; return apply_filters( 'woocommerce_checkout_posted_data', $data ); } /** * Validates the posted checkout data based on field properties. * * @since 3.0.0 * @param array $data An array of posted data. * @param WP_Error $errors Validation error. */ protected function validate_posted_data( &$data, &$errors ) { foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { $validate_fieldset = true; if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { $validate_fieldset = false; } foreach ( $fieldset as $key => $field ) { if ( ! isset( $data[ $key ] ) ) { continue; } $required = ! empty( $field['required'] ); $format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() ); $field_label = isset( $field['label'] ) ? $field['label'] : ''; if ( $validate_fieldset && ( isset( $field['type'] ) && 'country' === $field['type'] && '' !== $data[ $key ] ) && ! WC()->countries->country_exists( $data[ $key ] ) ) { /* translators: ISO 3166-1 alpha-2 country code */ $errors->add( $key . '_validation', sprintf( __( "'%s' is not a valid country code.", 'woocommerce' ), $data[ $key ] ) ); } switch ( $fieldset_key ) { case 'shipping': /* translators: %s: field name */ $field_label = sprintf( _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), $field_label ); break; case 'billing': /* translators: %s: field name */ $field_label = sprintf( _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), $field_label ); break; } if ( in_array( 'postcode', $format, true ) ) { $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); $data[ $key ] = wc_format_postcode( $data[ $key ], $country ); if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_postcode( $data[ $key ], $country ) ) { switch ( $country ) { case 'IE': /* translators: %1$s: field name, %2$s finder.eircode.ie URL */ $postcode_validation_notice = sprintf( __( '%1$s is not valid. You can look up the correct Eircode <a target="_blank" href="%2$s">here</a>.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', 'https://finder.eircode.ie' ); break; default: /* translators: %s: field name */ $postcode_validation_notice = sprintf( __( '%s is not a valid postcode / ZIP.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ); } $errors->add( $key . '_validation', apply_filters( 'woocommerce_checkout_postcode_validation_notice', $postcode_validation_notice, $country, $data[ $key ] ), array( 'id' => $key ) ); } } if ( in_array( 'phone', $format, true ) ) { if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) { /* translators: %s: phone number */ $errors->add( $key . '_validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) ); } } if ( in_array( 'email', $format, true ) && '' !== $data[ $key ] ) { $email_is_valid = is_email( $data[ $key ] ); $data[ $key ] = sanitize_email( $data[ $key ] ); if ( $validate_fieldset && ! $email_is_valid ) { /* translators: %s: email address */ $errors->add( $key . '_validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) ); continue; } } if ( '' !== $data[ $key ] && in_array( 'state', $format, true ) ) { $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); $valid_states = WC()->countries->get_states( $country ); if ( ! empty( $valid_states ) && is_array( $valid_states ) && count( $valid_states ) > 0 ) { $valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) ); $data[ $key ] = wc_strtoupper( $data[ $key ] ); if ( isset( $valid_state_values[ $data[ $key ] ] ) ) { // With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below. $data[ $key ] = $valid_state_values[ $data[ $key ] ]; } if ( $validate_fieldset && ! in_array( $data[ $key ], $valid_state_values, true ) ) { /* translators: 1: state field 2: valid states */ $errors->add( $key . '_validation', sprintf( __( '%1$s is not valid. Please enter one of the following: %2$s', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', implode( ', ', $valid_states ) ), array( 'id' => $key ) ); } } } if ( $validate_fieldset && $required && '' === $data[ $key ] ) { /* translators: %s: field name */ $errors->add( $key . '_required', apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), $field_label ), array( 'id' => $key ) ); } } } } /** * Validates that the checkout has enough info to proceed. * * @since 3.0.0 * @param array $data An array of posted data. * @param WP_Error $errors Validation errors. */ protected function validate_checkout( &$data, &$errors ) { $this->validate_posted_data( $data, $errors ); $this->check_cart_items(); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) { $errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) ); } if ( WC()->cart->needs_shipping() ) { $shipping_country = isset( $data['shipping_country'] ) ? $data['shipping_country'] : WC()->customer->get_shipping_country(); if ( empty( $shipping_country ) ) { $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) ); } elseif ( ! in_array( $shipping_country, array_keys( WC()->countries->get_shipping_countries() ), true ) ) { if ( WC()->countries->country_exists( $shipping_country ) ) { /* translators: %s: shipping location (prefix e.g. 'to' + ISO 3166-1 alpha-2 country code) */ $errors->add( 'shipping', sprintf( __( 'Unfortunately <strong>we do not ship %s</strong>. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . $shipping_country ) ); } } else { $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); foreach ( WC()->shipping()->get_packages() as $i => $package ) { if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) { $errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ) ); } } } } if ( WC()->cart->needs_payment() ) { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) { $errors->add( 'payment', __( 'Invalid payment method.', 'woocommerce' ) ); } else { $available_gateways[ $data['payment_method'] ]->validate_fields(); } } do_action( 'woocommerce_after_checkout_validation', $data, $errors ); } /** * Set address field for customer. * * @since 3.0.7 * @param string $field String to update. * @param string $key Field key. * @param array $data Array of data to get the value from. */ protected function set_customer_address_fields( $field, $key, $data ) { $billing_value = null; $shipping_value = null; if ( isset( $data[ "billing_{$field}" ] ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) { $billing_value = $data[ "billing_{$field}" ]; $shipping_value = $data[ "billing_{$field}" ]; } if ( isset( $data[ "shipping_{$field}" ] ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) { $shipping_value = $data[ "shipping_{$field}" ]; } if ( ! is_null( $billing_value ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) { WC()->customer->{"set_billing_{$field}"}( $billing_value ); } if ( ! is_null( $shipping_value ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) { WC()->customer->{"set_shipping_{$field}"}( $shipping_value ); } } /** * Update customer and session data from the posted checkout data. * * @since 3.0.0 * @param array $data Posted data. */ protected function update_session( $data ) { // Update both shipping and billing to the passed billing address first if set. $address_fields = array( 'first_name', 'last_name', 'company', 'email', 'phone', 'address_1', 'address_2', 'city', 'postcode', 'state', 'country', ); array_walk( $address_fields, array( $this, 'set_customer_address_fields' ), $data ); WC()->customer->save(); // Update customer shipping and payment method to posted method. $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); if ( is_array( $data['shipping_method'] ) ) { foreach ( $data['shipping_method'] as $i => $value ) { $chosen_shipping_methods[ $i ] = $value; } } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); WC()->session->set( 'chosen_payment_method', $data['payment_method'] ); // Update cart totals now we have customer address. WC()->cart->calculate_totals(); } /** * Process an order that does require payment. * * @since 3.0.0 * @param int $order_id Order ID. * @param string $payment_method Payment method. */ protected function process_order_payment( $order_id, $payment_method ) { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $payment_method ] ) ) { return; } // Store Order ID in session so it can be re-used after payment failure. WC()->session->set( 'order_awaiting_payment', $order_id ); // Process Payment. $result = $available_gateways[ $payment_method ]->process_payment( $order_id ); // Redirect to success/confirmation/payment page. if ( isset( $result['result'] ) && 'success' === $result['result'] ) { $result['order_id'] = $order_id; $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); if ( ! is_ajax() ) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect wp_redirect( $result['redirect'] ); exit; } wp_send_json( $result ); } } /** * Process an order that doesn't require payment. * * @since 3.0.0 * @param int $order_id Order ID. */ protected function process_order_without_payment( $order_id ) { $order = wc_get_order( $order_id ); $order->payment_complete(); wc_empty_cart(); if ( ! is_ajax() ) { wp_safe_redirect( apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ) ); exit; } wp_send_json( array( 'result' => 'success', 'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ), ) ); } /** * Create a new customer account if needed. * * @throws Exception When not able to create customer. * @param array $data Posted data. */ protected function process_customer( $data ) { $customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); if ( ! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) ) ) { $username = ! empty( $data['account_username'] ) ? $data['account_username'] : ''; $password = ! empty( $data['account_password'] ) ? $data['account_password'] : ''; $customer_id = wc_create_new_customer( $data['billing_email'], $username, $password, array( 'first_name' => ! empty( $data['billing_first_name'] ) ? $data['billing_first_name'] : '', 'last_name' => ! empty( $data['billing_last_name'] ) ? $data['billing_last_name'] : '', ) ); if ( is_wp_error( $customer_id ) ) { throw new Exception( $customer_id->get_error_message() ); } wc_set_customer_auth_cookie( $customer_id ); // As we are now logged in, checkout will need to refresh to show logged in data. WC()->session->set( 'reload_checkout', true ); // Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering. WC()->cart->calculate_totals(); } // On multisite, ensure user exists on current site, if not add them before allowing login. if ( $customer_id && is_multisite() && is_user_logged_in() && ! is_user_member_of_blog() ) { add_user_to_blog( get_current_blog_id(), $customer_id, 'customer' ); } // Add customer info from other fields. if ( $customer_id && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) { $customer = new WC_Customer( $customer_id ); if ( ! empty( $data['billing_first_name'] ) && '' === $customer->get_first_name() ) { $customer->set_first_name( $data['billing_first_name'] ); } if ( ! empty( $data['billing_last_name'] ) && '' === $customer->get_last_name() ) { $customer->set_last_name( $data['billing_last_name'] ); } // If the display name is an email, update to the user's full name. if ( is_email( $customer->get_display_name() ) ) { $customer->set_display_name( $customer->get_first_name() . ' ' . $customer->get_last_name() ); } foreach ( $data as $key => $value ) { // Use setters where available. if ( is_callable( array( $customer, "set_{$key}" ) ) ) { $customer->{"set_{$key}"}( $value ); // Store custom fields prefixed with wither shipping_ or billing_. } elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) { $customer->update_meta_data( $key, $value ); } } /** * Action hook to adjust customer before save. * * @since 3.0.0 */ do_action( 'woocommerce_checkout_update_customer', $customer, $data ); $customer->save(); } do_action( 'woocommerce_checkout_update_user_meta', $customer_id, $data ); } /** * If checkout failed during an AJAX call, send failure response. */ protected function send_ajax_failure_response() { if ( is_ajax() ) { // Only print notices if not reloading the checkout, otherwise they're lost in the page reload. if ( ! isset( WC()->session->reload_checkout ) ) { $messages = wc_print_notices( true ); } $response = array( 'result' => 'failure', 'messages' => isset( $messages ) ? $messages : '', 'refresh' => isset( WC()->session->refresh_totals ), 'reload' => isset( WC()->session->reload_checkout ), ); unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); wp_send_json( $response ); } } /** * Process the checkout after the confirm order button is pressed. * * @throws Exception When validation fails. */ public function process_checkout() { try { $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) { WC()->session->set( 'refresh_totals', true ); throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) ); } wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); wc_set_time_limit( 0 ); do_action( 'woocommerce_before_checkout_process' ); if ( WC()->cart->is_empty() ) { /* translators: %s: shop cart url */ throw new Exception( sprintf( __( 'Sorry, your session has expired. <a href="%s" class="wc-backward">Return to shop</a>', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) ); } do_action( 'woocommerce_checkout_process' ); $errors = new WP_Error(); $posted_data = $this->get_posted_data(); // Update session for customer and totals. $this->update_session( $posted_data ); // Validate posted data and cart items before proceeding. $this->validate_checkout( $posted_data, $errors ); foreach ( $errors->errors as $code => $messages ) { $data = $errors->get_error_data( $code ); foreach ( $messages as $message ) { wc_add_notice( $message, 'error', $data ); } } if ( empty( $posted_data['woocommerce_checkout_update_totals'] ) && 0 === wc_notice_count( 'error' ) ) { $this->process_customer( $posted_data ); $order_id = $this->create_order( $posted_data ); $order = wc_get_order( $order_id ); if ( is_wp_error( $order_id ) ) { throw new Exception( $order_id->get_error_message() ); } if ( ! $order ) { throw new Exception( __( 'Unable to create order.', 'woocommerce' ) ); } do_action( 'woocommerce_checkout_order_processed', $order_id, $posted_data, $order ); /** * Note that woocommerce_cart_needs_payment is only used in * WC_Checkout::process_checkout() to keep backwards compatibility. * Use woocommerce_order_needs_payment instead. * * Note that at this point you can't rely on the Cart Object anymore, * since it could be empty see: * https://github.com/woocommerce/woocommerce/issues/24631 */ if ( apply_filters( 'woocommerce_cart_needs_payment', $order->needs_payment(), WC()->cart ) ) { $this->process_order_payment( $order_id, $posted_data['payment_method'] ); } else { $this->process_order_without_payment( $order_id ); } } } catch ( Exception $e ) { wc_add_notice( $e->getMessage(), 'error' ); } $this->send_ajax_failure_response(); } /** * Get a posted address field after sanitization and validation. * * @param string $key Field key. * @param string $type Type of address. Available options: 'billing' or 'shipping'. * @return string */ public function get_posted_address_data( $key, $type = 'billing' ) { if ( 'billing' === $type || false === $this->legacy_posted_data['ship_to_different_address'] ) { $return = isset( $this->legacy_posted_data[ 'billing_' . $key ] ) ? $this->legacy_posted_data[ 'billing_' . $key ] : ''; } else { $return = isset( $this->legacy_posted_data[ 'shipping_' . $key ] ) ? $this->legacy_posted_data[ 'shipping_' . $key ] : ''; } return $return; } /** * Gets the value either from POST, or from the customer object. Sets the default values in checkout fields. * * @param string $input Name of the input we want to grab data for. e.g. billing_country. * @return string The default value. */ public function get_value( $input ) { // If the form was posted, get the posted value. This will only tend to happen when JavaScript is disabled client side. if ( ! empty( $_POST[ $input ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return wc_clean( wp_unslash( $_POST[ $input ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } // Allow 3rd parties to short circuit the logic and return their own default value. $value = apply_filters( 'woocommerce_checkout_get_value', null, $input ); if ( ! is_null( $value ) ) { return $value; } /** * For logged in customers, pull data from their account rather than the session which may contain incomplete data. * Another reason is that WC sets shipping address to the billing address on the checkout updates unless the * "ship to another address" box is checked. @see issue #20975. */ $customer_object = false; if ( is_user_logged_in() ) { // Load customer object, but keep it cached to avoid reloading it multiple times. if ( is_null( $this->logged_in_customer ) ) { $this->logged_in_customer = new WC_Customer( get_current_user_id(), true ); } $customer_object = $this->logged_in_customer; } if ( ! $customer_object ) { $customer_object = WC()->customer; } if ( is_callable( array( $customer_object, "get_$input" ) ) ) { $value = $customer_object->{"get_$input"}(); } elseif ( $customer_object->meta_exists( $input ) ) { $value = $customer_object->get_meta( $input, true ); } if ( '' === $value ) { $value = null; } return apply_filters( 'default_checkout_' . $input, $value, $input ); } } includes/class-wc-session-handler.php 0000644 00000027277 15132754524 0013722 0 ustar 00 <?php /** * Handle data for the current customers session. * Implements the WC_Session abstract class. * * From 2.5 this uses a custom table for session storage. Based on https://github.com/kloon/woocommerce-large-sessions. * * @class WC_Session_Handler * @version 2.5.0 * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Session handler class. */ class WC_Session_Handler extends WC_Session { /** * Cookie name used for the session. * * @var string cookie name */ protected $_cookie; /** * Stores session expiry. * * @var string session due to expire timestamp */ protected $_session_expiring; /** * Stores session due to expire timestamp. * * @var string session expiration timestamp */ protected $_session_expiration; /** * True when the cookie exists. * * @var bool Based on whether a cookie exists. */ protected $_has_cookie = false; /** * Table name for session data. * * @var string Custom session table name */ protected $_table; /** * Constructor for the session class. */ public function __construct() { $this->_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; } /** * Init hooks and session data. * * @since 3.3.0 */ public function init() { $this->init_session_cookie(); add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); add_action( 'shutdown', array( $this, 'save_data' ), 20 ); add_action( 'wp_logout', array( $this, 'destroy_session' ) ); if ( ! is_user_logged_in() ) { add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 ); } } /** * Setup cookie and customer ID. * * @since 3.6.0 */ public function init_session_cookie() { $cookie = $this->get_session_cookie(); if ( $cookie ) { $this->_customer_id = $cookie[0]; $this->_session_expiration = $cookie[1]; $this->_session_expiring = $cookie[2]; $this->_has_cookie = true; $this->_data = $this->get_session_data(); // If the user logs in, update session. if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { $guest_session_id = $this->_customer_id; $this->_customer_id = strval( get_current_user_id() ); $this->_dirty = true; $this->save_data( $guest_session_id ); $this->set_customer_session_cookie( true ); } // Update session if its close to expiring. if ( time() > $this->_session_expiring ) { $this->set_session_expiration(); $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration ); } } else { $this->set_session_expiration(); $this->_customer_id = $this->generate_customer_id(); $this->_data = $this->get_session_data(); } } /** * Sets the session cookie on-demand (usually after adding an item to the cart). * * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. * * Warning: Cookies will only be set if this is called before the headers are sent. * * @param bool $set Should the session cookie be set. */ public function set_customer_session_cookie( $set ) { if ( $set ) { $to_hash = $this->_customer_id . '|' . $this->_session_expiration; $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; $this->_has_cookie = true; if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) { wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true ); } } } /** * Should the session cookie be secure? * * @since 3.6.0 * @return bool */ protected function use_secure_cookie() { return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() ); } /** * Return true if the current user has an active session, i.e. a cookie to retrieve values. * * @return bool */ public function has_session() { return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine. } /** * Set session expiration. */ public function set_session_expiration() { $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours. $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours. } /** * Generate a unique customer ID for guests, or return user ID if logged in. * * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. * * @return string */ public function generate_customer_id() { $customer_id = ''; if ( is_user_logged_in() ) { $customer_id = strval( get_current_user_id() ); } if ( empty( $customer_id ) ) { require_once ABSPATH . 'wp-includes/class-phpass.php'; $hasher = new PasswordHash( 8, false ); $customer_id = md5( $hasher->get_random_bytes( 32 ) ); } return $customer_id; } /** * Get session unique ID for requests if session is initialized or user ID if logged in. * Introduced to help with unit tests. * * @since 5.3.0 * @return string */ public function get_customer_unique_id() { $customer_id = ''; if ( $this->has_session() && $this->_customer_id ) { $customer_id = $this->_customer_id; } elseif ( is_user_logged_in() ) { $customer_id = (string) get_current_user_id(); } return $customer_id; } /** * Get the session cookie, if set. Otherwise return false. * * Session cookies without a customer ID are invalid. * * @return bool|array */ public function get_session_cookie() { $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine. if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) { return false; } list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value ); if ( empty( $customer_id ) ) { return false; } // Validate hash. $to_hash = $customer_id . '|' . $session_expiration; $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { return false; } return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ); } /** * Get session data. * * @return array */ public function get_session_data() { return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); } /** * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. * * @return string */ private function get_cache_prefix() { return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); } /** * Save data and delete guest session. * * @param int $old_session_key session ID before user logs in. */ public function save_data( $old_session_key = 0 ) { // Dirty if something changed - prevents saving nothing new. if ( $this->_dirty && $this->has_session() ) { global $wpdb; $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", $this->_customer_id, maybe_serialize( $this->_data ), $this->_session_expiration ) ); wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); $this->_dirty = false; if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) { $this->delete_session( $old_session_key ); } } } /** * Destroy all session data. */ public function destroy_session() { $this->delete_session( $this->_customer_id ); $this->forget_session(); } /** * Forget all session data without destroying it. */ public function forget_session() { wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true ); wc_empty_cart(); $this->_data = array(); $this->_dirty = false; $this->_customer_id = $this->generate_customer_id(); } /** * When a user is logged out, ensure they have a unique nonce by using the customer/session ID. * * @deprecated 5.3.0 * @param int $uid User ID. * @return int|string */ public function nonce_user_logged_out( $uid ) { wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' ); return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; } /** * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID. * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called. * * @since 5.3.0 * @param int $uid User ID. * @param string $action The nonce action. * @return int|string */ public function maybe_update_nonce_user_logged_out( $uid, $action ) { if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) { return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; } return $uid; } /** * Cleanup session data from the database and clear caches. */ public function cleanup_sessions() { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine. if ( class_exists( 'WC_Cache_Helper' ) ) { WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP ); } } /** * Returns the session. * * @param string $customer_id Custo ID. * @param mixed $default Default session value. * @return string|array */ public function get_session( $customer_id, $default = false ) { global $wpdb; if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) { return false; } // Try to get it from the cache, it will return false if not present or if object cache not in use. $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); if ( false === $value ) { $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine. if ( is_null( $value ) ) { $value = $default; } $cache_duration = $this->_session_expiration - time(); if ( 0 < $cache_duration ) { wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration ); } } return maybe_unserialize( $value ); } /** * Delete the session from the cache and database. * * @param int $customer_id Customer ID. */ public function delete_session( $customer_id ) { global $wpdb; wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); $wpdb->delete( $this->_table, array( 'session_key' => $customer_id, ) ); } /** * Update the session expiry timestamp. * * @param string $customer_id Customer ID. * @param int $timestamp Timestamp to expire the cookie. */ public function update_session_timestamp( $customer_id, $timestamp ) { global $wpdb; $wpdb->update( $this->_table, array( 'session_expiry' => $timestamp, ), array( 'session_key' => $customer_id, ), array( '%d', ) ); } } includes/class-wc-embed.php 0000644 00000010274 15132754524 0011665 0 ustar 00 <?php /** * WooCommerce product embed * * @version 2.4.11 * @package WooCommerce\Classes\Embed */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Embed Class which handles any WooCommerce Products that are embedded on this site or another site. */ class WC_Embed { /** * Init embed class. * * @since 2.4.11 */ public static function init() { // Filter all of the content that's going to be embedded. add_filter( 'the_excerpt_embed', array( __CLASS__, 'the_excerpt' ), 10 ); // Make sure no comments display. Doesn't make sense for products. add_action( 'embed_content_meta', array( __CLASS__, 'remove_comments_button' ), 5 ); // In the comments place let's display the product rating. add_action( 'embed_content_meta', array( __CLASS__, 'get_ratings' ), 5 ); // Add some basic styles. add_action( 'embed_head', array( __CLASS__, 'print_embed_styles' ) ); } /** * Remove comments button on product embeds. * * @since 2.6.0 */ public static function remove_comments_button() { if ( self::is_embedded_product() ) { remove_action( 'embed_content_meta', 'print_embed_comments_button' ); } } /** * Check if this is an embedded product - to make sure we don't mess up regular posts. * * @since 2.4.11 * @return bool */ public static function is_embedded_product() { if ( function_exists( 'is_embed' ) && is_embed() && is_product() ) { return true; } return false; } /** * Create the excerpt for embedded products - we want to add the buy button to it. * * @since 2.4.11 * @param string $excerpt Embed short description. * @return string */ public static function the_excerpt( $excerpt ) { global $post; // Get product. $_product = wc_get_product( get_the_ID() ); // Make sure we're only affecting embedded products. if ( self::is_embedded_product() ) { echo '<p><span class="wc-embed-price">' . $_product->get_price_html() . '</span></p>'; // WPCS: XSS ok. if ( ! empty( $post->post_excerpt ) ) { ob_start(); woocommerce_template_single_excerpt(); $excerpt = ob_get_clean(); } // Add the button. $excerpt .= self::product_buttons(); } return $excerpt; } /** * Create the button to go to the product page for embedded products. * * @since 2.4.11 * @return string */ public static function product_buttons() { $_product = wc_get_product( get_the_ID() ); $buttons = array(); $button = '<a href="%s" class="wp-embed-more wc-embed-button">%s</a>'; if ( $_product->is_type( 'simple' ) && $_product->is_purchasable() && $_product->is_in_stock() ) { $buttons[] = sprintf( $button, esc_url( add_query_arg( 'add-to-cart', get_the_ID(), wc_get_cart_url() ) ), esc_html__( 'Buy now', 'woocommerce' ) ); } $buttons[] = sprintf( $button, get_the_permalink(), esc_html__( 'Read more', 'woocommerce' ) ); return '<p>' . implode( ' ', $buttons ) . '</p>'; } /** * Prints the markup for the rating stars. * * @since 2.4.11 */ public static function get_ratings() { // Make sure we're only affecting embedded products. if ( ! self::is_embedded_product() ) { return; } $_product = wc_get_product( get_the_ID() ); if ( $_product && $_product->get_average_rating() > 0 ) { ?> <div class="wc-embed-rating"> <?php printf( /* translators: %s: average rating */ esc_html__( 'Rated %s out of 5', 'woocommerce' ), esc_html( $_product->get_average_rating() ) ); ?> </div> <?php } } /** * Basic styling. */ public static function print_embed_styles() { if ( ! self::is_embedded_product() ) { return; } ?> <style type="text/css"> a.wc-embed-button { border-radius: 4px; border: 1px solid #ddd; box-shadow: 0px 1px 0 0px rgba(0, 0, 0, 0.05); display:inline-block; padding: .5em; } a.wc-embed-button:hover, a.wc-embed-button:focus { border: 1px solid #ccc; box-shadow: 0px 1px 0 0px rgba(0, 0, 0, 0.1); color: #999; text-decoration: none; } .wp-embed-excerpt p { margin: 0 0 1em; } .wc-embed-price { display: block; opacity: .75; font-weight: 700; margin-top: -.75em; } .wc-embed-rating { display: inline-block; } </style> <?php } } WC_Embed::init(); includes/export/abstract-wc-csv-exporter.php 0000644 00000027043 15132754524 0015273 0 ustar 00 <?php /** * Handles CSV export. * * @package WooCommerce\Export * @version 3.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_CSV_Exporter Class. */ abstract class WC_CSV_Exporter { /** * Type of export used in filter names. * * @var string */ protected $export_type = ''; /** * Filename to export to. * * @var string */ protected $filename = 'wc-export.csv'; /** * Batch limit. * * @var integer */ protected $limit = 50; /** * Number exported. * * @var integer */ protected $exported_row_count = 0; /** * Raw data to export. * * @var array */ protected $row_data = array(); /** * Total rows to export. * * @var integer */ protected $total_rows = 0; /** * Columns ids and names. * * @var array */ protected $column_names = array(); /** * List of columns to export, or empty for all. * * @var array */ protected $columns_to_export = array(); /** * The delimiter parameter sets the field delimiter (one character only). * * @var string */ protected $delimiter = ','; /** * Prepare data that will be exported. */ abstract public function prepare_data_to_export(); /** * Return an array of supported column names and ids. * * @since 3.1.0 * @return array */ public function get_column_names() { return apply_filters( "woocommerce_{$this->export_type}_export_column_names", $this->column_names, $this ); } /** * Set column names. * * @since 3.1.0 * @param array $column_names Column names array. */ public function set_column_names( $column_names ) { $this->column_names = array(); foreach ( $column_names as $column_id => $column_name ) { $this->column_names[ wc_clean( $column_id ) ] = wc_clean( $column_name ); } } /** * Return an array of columns to export. * * @since 3.1.0 * @return array */ public function get_columns_to_export() { return $this->columns_to_export; } /** * Return the delimiter to use in CSV file * * @since 3.9.0 * @return string */ public function get_delimiter() { return apply_filters( "woocommerce_{$this->export_type}_export_delimiter", $this->delimiter ); } /** * Set columns to export. * * @since 3.1.0 * @param array $columns Columns array. */ public function set_columns_to_export( $columns ) { $this->columns_to_export = array_map( 'wc_clean', $columns ); } /** * See if a column is to be exported or not. * * @since 3.1.0 * @param string $column_id ID of the column being exported. * @return boolean */ public function is_column_exporting( $column_id ) { $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; $columns_to_export = $this->get_columns_to_export(); if ( empty( $columns_to_export ) ) { return true; } if ( in_array( $column_id, $columns_to_export, true ) || 'meta' === $column_id ) { return true; } return false; } /** * Return default columns. * * @since 3.1.0 * @return array */ public function get_default_column_names() { return array(); } /** * Do the export. * * @since 3.1.0 */ public function export() { $this->prepare_data_to_export(); $this->send_headers(); $this->send_content( chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $this->get_csv_data() ); die(); } /** * Set the export headers. * * @since 3.1.0 */ public function send_headers() { if ( function_exists( 'gc_enable' ) ) { gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound } if ( function_exists( 'apache_setenv' ) ) { @apache_setenv( 'no-gzip', 1 ); // @codingStandardsIgnoreLine } @ini_set( 'zlib.output_compression', 'Off' ); // @codingStandardsIgnoreLine @ini_set( 'output_buffering', 'Off' ); // @codingStandardsIgnoreLine @ini_set( 'output_handler', '' ); // @codingStandardsIgnoreLine ignore_user_abort( true ); wc_set_time_limit( 0 ); wc_nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=' . $this->get_filename() ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); } /** * Set filename to export to. * * @param string $filename Filename to export to. */ public function set_filename( $filename ) { $this->filename = sanitize_file_name( str_replace( '.csv', '', $filename ) . '.csv' ); } /** * Generate and return a filename. * * @return string */ public function get_filename() { return sanitize_file_name( apply_filters( "woocommerce_{$this->export_type}_export_get_filename", $this->filename ) ); } /** * Set the export content. * * @since 3.1.0 * @param string $csv_data All CSV content. */ public function send_content( $csv_data ) { echo $csv_data; // @codingStandardsIgnoreLine } /** * Get CSV data for this export. * * @since 3.1.0 * @return string */ protected function get_csv_data() { return $this->export_rows(); } /** * Export column headers in CSV format. * * @since 3.1.0 * @return string */ protected function export_column_headers() { $columns = $this->get_column_names(); $export_row = array(); $buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen ob_start(); foreach ( $columns as $column_id => $column_name ) { if ( ! $this->is_column_exporting( $column_id ) ) { continue; } $export_row[] = $this->format_data( $column_name ); } $this->fputcsv( $buffer, $export_row ); return ob_get_clean(); } /** * Get data that will be exported. * * @since 3.1.0 * @return array */ protected function get_data_to_export() { return $this->row_data; } /** * Export rows in CSV format. * * @since 3.1.0 * @return string */ protected function export_rows() { $data = $this->get_data_to_export(); $buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen ob_start(); array_walk( $data, array( $this, 'export_row' ), $buffer ); return apply_filters( "woocommerce_{$this->export_type}_export_rows", ob_get_clean(), $this ); } /** * Export rows to an array ready for the CSV. * * @since 3.1.0 * @param array $row_data Data to export. * @param string $key Column being exported. * @param resource $buffer Output buffer. */ protected function export_row( $row_data, $key, $buffer ) { $columns = $this->get_column_names(); $export_row = array(); foreach ( $columns as $column_id => $column_name ) { if ( ! $this->is_column_exporting( $column_id ) ) { continue; } if ( isset( $row_data[ $column_id ] ) ) { $export_row[] = $this->format_data( $row_data[ $column_id ] ); } else { $export_row[] = ''; } } $this->fputcsv( $buffer, $export_row ); ++ $this->exported_row_count; } /** * Get batch limit. * * @since 3.1.0 * @return int */ public function get_limit() { return apply_filters( "woocommerce_{$this->export_type}_export_batch_limit", $this->limit, $this ); } /** * Set batch limit. * * @since 3.1.0 * @param int $limit Limit to export. */ public function set_limit( $limit ) { $this->limit = absint( $limit ); } /** * Get count of records exported. * * @since 3.1.0 * @return int */ public function get_total_exported() { return $this->exported_row_count; } /** * Escape a string to be used in a CSV context * * Malicious input can inject formulas into CSV files, opening up the possibility * for phishing attacks and disclosure of sensitive information. * * Additionally, Excel exposes the ability to launch arbitrary commands through * the DDE protocol. * * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/ * @see https://hackerone.com/reports/72785 * * @since 3.1.0 * @param string $data CSV field to escape. * @return string */ public function escape_data( $data ) { $active_content_triggers = array( '=', '+', '-', '@' ); if ( in_array( mb_substr( $data, 0, 1 ), $active_content_triggers, true ) ) { $data = "'" . $data; } return $data; } /** * Format and escape data ready for the CSV file. * * @since 3.1.0 * @param string $data Data to format. * @return string */ public function format_data( $data ) { if ( ! is_scalar( $data ) ) { if ( is_a( $data, 'WC_Datetime' ) ) { $data = $data->date( 'Y-m-d G:i:s' ); } else { $data = ''; // Not supported. } } elseif ( is_bool( $data ) ) { $data = $data ? 1 : 0; } $use_mb = function_exists( 'mb_convert_encoding' ); if ( $use_mb ) { $encoding = mb_detect_encoding( $data, 'UTF-8, ISO-8859-1', true ); $data = 'UTF-8' === $encoding ? $data : utf8_encode( $data ); } return $this->escape_data( $data ); } /** * Format term ids to names. * * @since 3.1.0 * @param array $term_ids Term IDs to format. * @param string $taxonomy Taxonomy name. * @return string */ public function format_term_ids( $term_ids, $taxonomy ) { $term_ids = wp_parse_id_list( $term_ids ); if ( ! count( $term_ids ) ) { return ''; } $formatted_terms = array(); if ( is_taxonomy_hierarchical( $taxonomy ) ) { foreach ( $term_ids as $term_id ) { $formatted_term = array(); $ancestor_ids = array_reverse( get_ancestors( $term_id, $taxonomy ) ); foreach ( $ancestor_ids as $ancestor_id ) { $term = get_term( $ancestor_id, $taxonomy ); if ( $term && ! is_wp_error( $term ) ) { $formatted_term[] = $term->name; } } $term = get_term( $term_id, $taxonomy ); if ( $term && ! is_wp_error( $term ) ) { $formatted_term[] = $term->name; } $formatted_terms[] = implode( ' > ', $formatted_term ); } } else { foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( $term && ! is_wp_error( $term ) ) { $formatted_terms[] = $term->name; } } } return $this->implode_values( $formatted_terms ); } /** * Implode CSV cell values using commas by default, and wrapping values * which contain the separator. * * @since 3.2.0 * @param array $values Values to implode. * @return string */ protected function implode_values( $values ) { $values_to_implode = array(); foreach ( $values as $value ) { $value = (string) is_scalar( $value ) ? $value : ''; $values_to_implode[] = str_replace( ',', '\\,', $value ); } return implode( ', ', $values_to_implode ); } /** * Write to the CSV file, ensuring escaping works across versions of * PHP. * * PHP 5.5.4 uses '\' as the default escape character. This is not RFC-4180 compliant. * \0 disables the escape character. * * @see https://bugs.php.net/bug.php?id=43225 * @see https://bugs.php.net/bug.php?id=50686 * @see https://github.com/woocommerce/woocommerce/issues/19514 * @since 3.4.0 * @see https://github.com/woocommerce/woocommerce/issues/24579 * @since 3.9.0 * @param resource $buffer Resource we are writing to. * @param array $export_row Row to export. */ protected function fputcsv( $buffer, $export_row ) { if ( version_compare( PHP_VERSION, '5.5.4', '<' ) ) { ob_start(); $temp = fopen( 'php://output', 'w' ); // @codingStandardsIgnoreLine fputcsv( $temp, $export_row, $this->get_delimiter(), '"' ); // @codingStandardsIgnoreLine fclose( $temp ); // @codingStandardsIgnoreLine $row = ob_get_clean(); $row = str_replace( '\\"', '\\""', $row ); fwrite( $buffer, $row ); // @codingStandardsIgnoreLine } else { fputcsv( $buffer, $export_row, $this->get_delimiter(), '"', "\0" ); // @codingStandardsIgnoreLine } } } includes/export/abstract-wc-csv-batch-exporter.php 0000644 00000012275 15132754524 0016353 0 ustar 00 <?php /** * Handles Batch CSV export. * * Based on https://pippinsplugins.com/batch-processing-for-big-data/ * * @package WooCommerce\Export * @version 3.1.0 */ defined( 'ABSPATH' ) || exit; /** * Include dependencies. */ if ( ! class_exists( 'WC_CSV_Exporter', false ) ) { require_once WC_ABSPATH . 'includes/export/abstract-wc-csv-exporter.php'; } /** * WC_CSV_Exporter Class. */ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter { /** * Page being exported * * @var integer */ protected $page = 1; /** * Constructor. */ public function __construct() { $this->column_names = $this->get_default_column_names(); } /** * Get file path to export to. * * @return string */ protected function get_file_path() { $upload_dir = wp_upload_dir(); return trailingslashit( $upload_dir['basedir'] ) . $this->get_filename(); } /** * Get CSV headers row file path to export to. * * @return string */ protected function get_headers_row_file_path() { return $this->get_file_path() . '.headers'; } /** * Get the contents of the CSV headers row file. Defaults to the original known headers. * * @since 3.1.0 * @return string */ public function get_headers_row_file() { $file = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers(); if ( @file_exists( $this->get_headers_row_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged $file = @file_get_contents( $this->get_headers_row_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents } return $file; } /** * Get the file contents. * * @since 3.1.0 * @return string */ public function get_file() { $file = ''; if ( @file_exists( $this->get_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged $file = @file_get_contents( $this->get_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents } else { @file_put_contents( $this->get_file_path(), '' ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents @chmod( $this->get_file_path(), 0664 ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.chmod_chmod, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged } return $file; } /** * Serve the file and remove once sent to the client. * * @since 3.1.0 */ public function export() { $this->send_headers(); $this->send_content( $this->get_headers_row_file() . $this->get_file() ); @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged @unlink( $this->get_headers_row_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged die(); } /** * Generate the CSV file. * * @since 3.1.0 */ public function generate_file() { if ( 1 === $this->get_page() ) { @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged, // We need to initialize the file here. $this->get_file(); } $this->prepare_data_to_export(); $this->write_csv_data( $this->get_csv_data() ); } /** * Write data to the file. * * @since 3.1.0 * @param string $data Data. */ protected function write_csv_data( $data ) { if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) { return false; } $fp = fopen( $this->get_file_path(), 'a+' ); if ( $fp ) { fwrite( $fp, $data ); fclose( $fp ); } // Add all columns when finished. if ( 100 === $this->get_percent_complete() ) { $header = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers(); // We need to use a temporary file to store headers, this will make our life so much easier. @file_put_contents( $this->get_headers_row_file_path(), $header ); //phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents } } /** * Get page. * * @since 3.1.0 * @return int */ public function get_page() { return $this->page; } /** * Set page. * * @since 3.1.0 * @param int $page Page Nr. */ public function set_page( $page ) { $this->page = absint( $page ); } /** * Get count of records exported. * * @since 3.1.0 * @return int */ public function get_total_exported() { return ( ( $this->get_page() - 1 ) * $this->get_limit() ) + $this->exported_row_count; } /** * Get total % complete. * * @since 3.1.0 * @return int */ public function get_percent_complete() { return $this->total_rows ? floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100; } } includes/export/class-wc-product-csv-exporter.php 0000644 00000053551 15132754524 0016256 0 ustar 00 <?php /** * Handles product CSV export. * * @package WooCommerce\Export * @version 3.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Include dependencies. */ if ( ! class_exists( 'WC_CSV_Batch_Exporter', false ) ) { include_once WC_ABSPATH . 'includes/export/abstract-wc-csv-batch-exporter.php'; } /** * WC_Product_CSV_Exporter Class. */ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { /** * Type of export used in filter names. * * @var string */ protected $export_type = 'product'; /** * Should meta be exported? * * @var boolean */ protected $enable_meta_export = false; /** * Which product types are being exported. * * @var array */ protected $product_types_to_export = array(); /** * Products belonging to what category should be exported. * * @var string */ protected $product_category_to_export = array(); /** * Constructor. */ public function __construct() { parent::__construct(); $this->set_product_types_to_export( array_keys( WC_Admin_Exporters::get_product_types() ) ); } /** * Should meta be exported? * * @param bool $enable_meta_export Should meta be exported. * * @since 3.1.0 */ public function enable_meta_export( $enable_meta_export ) { $this->enable_meta_export = (bool) $enable_meta_export; } /** * Product types to export. * * @param array $product_types_to_export List of types to export. * * @since 3.1.0 */ public function set_product_types_to_export( $product_types_to_export ) { $this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export ); } /** * Product category to export * * @param string $product_category_to_export Product category slug to export, empty string exports all. * * @since 3.5.0 * @return void */ public function set_product_category_to_export( $product_category_to_export ) { $this->product_category_to_export = array_map( 'sanitize_title_with_dashes', $product_category_to_export ); } /** * Return an array of columns to export. * * @since 3.1.0 * @return array */ public function get_default_column_names() { return apply_filters( "woocommerce_product_export_{$this->export_type}_default_columns", array( 'id' => __( 'ID', 'woocommerce' ), 'type' => __( 'Type', 'woocommerce' ), 'sku' => __( 'SKU', 'woocommerce' ), 'name' => __( 'Name', 'woocommerce' ), 'published' => __( 'Published', 'woocommerce' ), 'featured' => __( 'Is featured?', 'woocommerce' ), 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), 'short_description' => __( 'Short description', 'woocommerce' ), 'description' => __( 'Description', 'woocommerce' ), 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), 'tax_status' => __( 'Tax status', 'woocommerce' ), 'tax_class' => __( 'Tax class', 'woocommerce' ), 'stock_status' => __( 'In stock?', 'woocommerce' ), 'stock' => __( 'Stock', 'woocommerce' ), 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), /* translators: %s: weight */ 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), /* translators: %s: length */ 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), /* translators: %s: width */ 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), /* translators: %s: Height */ 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), 'purchase_note' => __( 'Purchase note', 'woocommerce' ), 'sale_price' => __( 'Sale price', 'woocommerce' ), 'regular_price' => __( 'Regular price', 'woocommerce' ), 'category_ids' => __( 'Categories', 'woocommerce' ), 'tag_ids' => __( 'Tags', 'woocommerce' ), 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), 'images' => __( 'Images', 'woocommerce' ), 'download_limit' => __( 'Download limit', 'woocommerce' ), 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), 'parent_id' => __( 'Parent', 'woocommerce' ), 'grouped_products' => __( 'Grouped products', 'woocommerce' ), 'upsell_ids' => __( 'Upsells', 'woocommerce' ), 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), 'product_url' => __( 'External URL', 'woocommerce' ), 'button_text' => __( 'Button text', 'woocommerce' ), 'menu_order' => __( 'Position', 'woocommerce' ), ) ); } /** * Prepare data for export. * * @since 3.1.0 */ public function prepare_data_to_export() { $args = array( 'status' => array( 'private', 'publish', 'draft', 'future', 'pending' ), 'type' => $this->product_types_to_export, 'limit' => $this->get_limit(), 'page' => $this->get_page(), 'orderby' => array( 'ID' => 'ASC', ), 'return' => 'objects', 'paginate' => true, ); if ( ! empty( $this->product_category_to_export ) ) { $args['category'] = $this->product_category_to_export; } $products = wc_get_products( apply_filters( "woocommerce_product_export_{$this->export_type}_query_args", $args ) ); $this->total_rows = $products->total; $this->row_data = array(); $variable_products = array(); foreach ( $products->products as $product ) { // Check if the category is set, this means we need to fetch variations seperately as they are not tied to a category. if ( ! empty( $args['category'] ) && $product->is_type( 'variable' ) ) { $variable_products[] = $product->get_id(); } $this->row_data[] = $this->generate_row_data( $product ); } // If a category was selected we loop through the variations as they are not tied to a category so will be excluded by default. if ( ! empty( $variable_products ) ) { foreach ( $variable_products as $parent_id ) { $products = wc_get_products( array( 'parent' => $parent_id, 'type' => array( 'variation' ), 'return' => 'objects', 'limit' => -1, ) ); if ( ! $products ) { continue; } foreach ( $products as $product ) { $this->row_data[] = $this->generate_row_data( $product ); } } } } /** * Take a product and generate row data from it for export. * * @param WC_Product $product WC_Product object. * * @return array */ protected function generate_row_data( $product ) { $columns = $this->get_column_names(); $row = array(); foreach ( $columns as $column_id => $column_name ) { $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; $value = ''; // Skip some columns if dynamically handled later or if we're being selective. if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) { continue; } if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) { // Filter for 3rd parties. $value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product, $column_id ); } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) { // Handle special columns which don't map 1:1 to product data. $value = $this->{"get_column_value_{$column_id}"}( $product ); } elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) { // Default and custom handling. $value = $product->{"get_{$column_id}"}( 'edit' ); } if ( 'description' === $column_id || 'short_description' === $column_id ) { $value = $this->filter_description_field( $value ); } $row[ $column_id ] = $value; } $this->prepare_downloads_for_export( $product, $row ); $this->prepare_attributes_for_export( $product, $row ); $this->prepare_meta_for_export( $product, $row ); return apply_filters( 'woocommerce_product_export_row_data', $row, $product ); } /** * Get published value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return int */ protected function get_column_value_published( $product ) { $statuses = array( 'draft' => -1, 'private' => 0, 'publish' => 1, ); // Fix display for variations when parent product is a draft. if ( 'variation' === $product->get_type() ) { $parent = $product->get_parent_data(); $status = 'draft' === $parent['status'] ? $parent['status'] : $product->get_status( 'edit' ); } else { $status = $product->get_status( 'edit' ); } return isset( $statuses[ $status ] ) ? $statuses[ $status ] : -1; } /** * Get formatted sale price. * * @param WC_Product $product Product being exported. * * @return string */ protected function get_column_value_sale_price( $product ) { return wc_format_localized_price( $product->get_sale_price( 'view' ) ); } /** * Get formatted regular price. * * @param WC_Product $product Product being exported. * * @return string */ protected function get_column_value_regular_price( $product ) { return wc_format_localized_price( $product->get_regular_price() ); } /** * Get product_cat value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_category_ids( $product ) { $term_ids = $product->get_category_ids( 'edit' ); return $this->format_term_ids( $term_ids, 'product_cat' ); } /** * Get product_tag value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_tag_ids( $product ) { $term_ids = $product->get_tag_ids( 'edit' ); return $this->format_term_ids( $term_ids, 'product_tag' ); } /** * Get product_shipping_class value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_shipping_class_id( $product ) { $term_ids = $product->get_shipping_class_id( 'edit' ); return $this->format_term_ids( $term_ids, 'product_shipping_class' ); } /** * Get images value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_images( $product ) { $image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) ); $images = array(); foreach ( $image_ids as $image_id ) { $image = wp_get_attachment_image_src( $image_id, 'full' ); if ( $image ) { $images[] = $image[0]; } } return $this->implode_values( $images ); } /** * Prepare linked products for export. * * @param int[] $linked_products Array of linked product ids. * * @since 3.1.0 * @return string */ protected function prepare_linked_products_for_export( $linked_products ) { $product_list = array(); foreach ( $linked_products as $linked_product ) { if ( $linked_product->get_sku() ) { $product_list[] = $linked_product->get_sku(); } else { $product_list[] = 'id:' . $linked_product->get_id(); } } return $this->implode_values( $product_list ); } /** * Get cross_sell_ids value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_cross_sell_ids( $product ) { return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) ); } /** * Get upsell_ids value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_upsell_ids( $product ) { return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) ); } /** * Get parent_id value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_parent_id( $product ) { if ( $product->get_parent_id( 'edit' ) ) { $parent = wc_get_product( $product->get_parent_id( 'edit' ) ); if ( ! $parent ) { return ''; } return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id(); } return ''; } /** * Get grouped_products value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_grouped_products( $product ) { if ( 'grouped' !== $product->get_type() ) { return ''; } $grouped_products = array(); $child_ids = $product->get_children( 'edit' ); foreach ( $child_ids as $child_id ) { $child = wc_get_product( $child_id ); if ( ! $child ) { continue; } $grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id; } return $this->implode_values( $grouped_products ); } /** * Get download_limit value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_download_limit( $product ) { return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : ''; } /** * Get download_expiry value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_download_expiry( $product ) { return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : ''; } /** * Get stock value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_stock( $product ) { $manage_stock = $product->get_manage_stock( 'edit' ); $stock_quantity = $product->get_stock_quantity( 'edit' ); if ( $product->is_type( 'variation' ) && 'parent' === $manage_stock ) { return 'parent'; } elseif ( $manage_stock ) { return $stock_quantity; } else { return ''; } } /** * Get stock status value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_stock_status( $product ) { $status = $product->get_stock_status( 'edit' ); if ( 'onbackorder' === $status ) { return 'backorder'; } return 'instock' === $status ? 1 : 0; } /** * Get backorders. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_backorders( $product ) { $backorders = $product->get_backorders( 'edit' ); switch ( $backorders ) { case 'notify': return 'notify'; default: return wc_string_to_bool( $backorders ) ? 1 : 0; } } /** * Get low stock amount value. * * @param WC_Product $product Product being exported. * * @since 3.5.0 * @return int|string Empty string if value not set */ protected function get_column_value_low_stock_amount( $product ) { return $product->managing_stock() && $product->get_low_stock_amount( 'edit' ) ? $product->get_low_stock_amount( 'edit' ) : ''; } /** * Get type value. * * @param WC_Product $product Product being exported. * * @since 3.1.0 * @return string */ protected function get_column_value_type( $product ) { $types = array(); $types[] = $product->get_type(); if ( $product->is_downloadable() ) { $types[] = 'downloadable'; } if ( $product->is_virtual() ) { $types[] = 'virtual'; } return $this->implode_values( $types ); } /** * Filter description field for export. * Convert newlines to '\n'. * * @param string $description Product description text to filter. * * @since 3.5.4 * @return string */ protected function filter_description_field( $description ) { $description = str_replace( '\n', "\\\\n", $description ); $description = str_replace( "\n", '\n', $description ); return $description; } /** * Export downloads. * * @param WC_Product $product Product being exported. * @param array $row Row being exported. * * @since 3.1.0 */ protected function prepare_downloads_for_export( $product, &$row ) { if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) { $downloads = $product->get_downloads( 'edit' ); if ( $downloads ) { $i = 1; foreach ( $downloads as $download ) { /* translators: %s: download number */ $this->column_names[ 'downloads:id' . $i ] = sprintf( __( 'Download %d ID', 'woocommerce' ), $i ); /* translators: %s: download number */ $this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i ); /* translators: %s: download number */ $this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i ); $row[ 'downloads:id' . $i ] = $download->get_id(); $row[ 'downloads:name' . $i ] = $download->get_name(); $row[ 'downloads:url' . $i ] = $download->get_file(); $i++; } } } } /** * Export attributes data. * * @param WC_Product $product Product being exported. * @param array $row Row being exported. * * @since 3.1.0 */ protected function prepare_attributes_for_export( $product, &$row ) { if ( $this->is_column_exporting( 'attributes' ) ) { $attributes = $product->get_attributes(); $default_attributes = $product->get_default_attributes(); if ( count( $attributes ) ) { $i = 1; foreach ( $attributes as $attribute_name => $attribute ) { /* translators: %s: attribute number */ $this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i ); /* translators: %s: attribute number */ $this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i ); /* translators: %s: attribute number */ $this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i ); /* translators: %s: attribute number */ $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i ); if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product ); if ( $attribute->is_taxonomy() ) { $terms = $attribute->get_terms(); $values = array(); foreach ( $terms as $term ) { $values[] = $term->name; } $row[ 'attributes:value' . $i ] = $this->implode_values( $values ); $row[ 'attributes:taxonomy' . $i ] = 1; } else { $row[ 'attributes:value' . $i ] = $this->implode_values( $attribute->get_options() ); $row[ 'attributes:taxonomy' . $i ] = 0; } $row[ 'attributes:visible' . $i ] = $attribute->get_visible(); } else { $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product ); if ( 0 === strpos( $attribute_name, 'pa_' ) ) { $option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine. $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : str_replace( ',', '\\,', $attribute ); $row[ 'attributes:taxonomy' . $i ] = 1; } else { $row[ 'attributes:value' . $i ] = str_replace( ',', '\\,', $attribute ); $row[ 'attributes:taxonomy' . $i ] = 0; } $row[ 'attributes:visible' . $i ] = ''; } if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) { /* translators: %s: attribute number */ $this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i ); $default_value = $default_attributes[ sanitize_title( $attribute_name ) ]; if ( 0 === strpos( $attribute_name, 'pa_' ) ) { $option_term = get_term_by( 'slug', $default_value, $attribute_name ); // @codingStandardsIgnoreLine. $row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value; } else { $row[ 'attributes:default' . $i ] = $default_value; } } $i++; } } } } /** * Export meta data. * * @param WC_Product $product Product being exported. * @param array $row Row data. * * @since 3.1.0 */ protected function prepare_meta_for_export( $product, &$row ) { if ( $this->enable_meta_export ) { $meta_data = $product->get_meta_data(); if ( count( $meta_data ) ) { $meta_keys_to_skip = apply_filters( 'woocommerce_product_export_skip_meta_keys', array(), $product ); $i = 1; foreach ( $meta_data as $meta ) { if ( in_array( $meta->key, $meta_keys_to_skip, true ) ) { continue; } // Allow 3rd parties to process the meta, e.g. to transform non-scalar values to scalar. $meta_value = apply_filters( 'woocommerce_product_export_meta_value', $meta->value, $meta, $product, $row ); if ( ! is_scalar( $meta_value ) ) { continue; } $column_key = 'meta:' . esc_attr( $meta->key ); /* translators: %s: meta data name */ $this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key ); $row[ $column_key ] = $meta_value; $i ++; } } } } } includes/class-wc-emails.php 0000644 00000054750 15132754524 0012072 0 ustar 00 <?php /** * Transactional Emails Controller * * WooCommerce Emails Class which handles the sending on transactional emails and email templates. This class loads in available emails. * * @package WooCommerce\Classes\Emails * @version 2.3.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Emails class. */ class WC_Emails { /** * Array of email notification classes * * @var WC_Email[] */ public $emails = array(); /** * The single instance of the class * * @var WC_Emails */ protected static $_instance = null; /** * Background emailer class. * * @var WC_Background_Emailer */ protected static $background_emailer = null; /** * Main WC_Emails Instance. * * Ensures only one instance of WC_Emails is loaded or can be loaded. * * @since 2.1 * @static * @return WC_Emails Main instance */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Cloning is forbidden. * * @since 2.1 */ public function __clone() { wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. * * @since 2.1 */ public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' ); } /** * Hook in all transactional emails. */ public static function init_transactional_emails() { $email_actions = apply_filters( 'woocommerce_email_actions', array( 'woocommerce_low_stock', 'woocommerce_no_stock', 'woocommerce_product_on_backorder', 'woocommerce_order_status_pending_to_processing', 'woocommerce_order_status_pending_to_completed', 'woocommerce_order_status_processing_to_cancelled', 'woocommerce_order_status_pending_to_failed', 'woocommerce_order_status_pending_to_on-hold', 'woocommerce_order_status_failed_to_processing', 'woocommerce_order_status_failed_to_completed', 'woocommerce_order_status_failed_to_on-hold', 'woocommerce_order_status_cancelled_to_processing', 'woocommerce_order_status_cancelled_to_completed', 'woocommerce_order_status_cancelled_to_on-hold', 'woocommerce_order_status_on-hold_to_processing', 'woocommerce_order_status_on-hold_to_cancelled', 'woocommerce_order_status_on-hold_to_failed', 'woocommerce_order_status_completed', 'woocommerce_order_fully_refunded', 'woocommerce_order_partially_refunded', 'woocommerce_new_customer_note', 'woocommerce_created_customer', ) ); if ( apply_filters( 'woocommerce_defer_transactional_emails', false ) ) { self::$background_emailer = new WC_Background_Emailer(); foreach ( $email_actions as $action ) { add_action( $action, array( __CLASS__, 'queue_transactional_email' ), 10, 10 ); } } else { foreach ( $email_actions as $action ) { add_action( $action, array( __CLASS__, 'send_transactional_email' ), 10, 10 ); } } } /** * Queues transactional email so it's not sent in current request if enabled, * otherwise falls back to send now. * * @param mixed ...$args Optional arguments. */ public static function queue_transactional_email( ...$args ) { if ( is_a( self::$background_emailer, 'WC_Background_Emailer' ) ) { self::$background_emailer->push_to_queue( array( 'filter' => current_filter(), 'args' => func_get_args(), ) ); } else { self::send_transactional_email( ...$args ); } } /** * Init the mailer instance and call the notifications for the current filter. * * @internal * * @param string $filter Filter name. * @param array $args Email args (default: []). */ public static function send_queued_transactional_email( $filter = '', $args = array() ) { if ( apply_filters( 'woocommerce_allow_send_queued_transactional_email', true, $filter, $args ) ) { self::instance(); // Init self so emails exist. // Ensure gateways are loaded in case they need to insert data into the emails. WC()->payment_gateways(); WC()->shipping(); do_action_ref_array( $filter . '_notification', $args ); } } /** * Init the mailer instance and call the notifications for the current filter. * * @internal * * @param array $args Email args (default: []). */ public static function send_transactional_email( $args = array() ) { try { $args = func_get_args(); self::instance(); // Init self so emails exist. do_action_ref_array( current_filter() . '_notification', $args ); } catch ( Exception $e ) { $error = 'Transactional email triggered fatal error for callback ' . current_filter(); $logger = wc_get_logger(); $logger->critical( $error . PHP_EOL, array( 'source' => 'transactional-emails', ) ); if ( Constants::is_true( 'WP_DEBUG' ) ) { trigger_error( $error, E_USER_WARNING ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } } } /** * Constructor for the email class hooks in all emails that can be sent. */ public function __construct() { $this->init(); // Email Header, Footer and content hooks. add_action( 'woocommerce_email_header', array( $this, 'email_header' ) ); add_action( 'woocommerce_email_footer', array( $this, 'email_footer' ) ); add_action( 'woocommerce_email_order_details', array( $this, 'order_downloads' ), 10, 4 ); add_action( 'woocommerce_email_order_details', array( $this, 'order_details' ), 10, 4 ); add_action( 'woocommerce_email_order_meta', array( $this, 'order_meta' ), 10, 3 ); add_action( 'woocommerce_email_customer_details', array( $this, 'customer_details' ), 10, 3 ); add_action( 'woocommerce_email_customer_details', array( $this, 'email_addresses' ), 20, 3 ); // Hooks for sending emails during store events. add_action( 'woocommerce_low_stock_notification', array( $this, 'low_stock' ) ); add_action( 'woocommerce_no_stock_notification', array( $this, 'no_stock' ) ); add_action( 'woocommerce_product_on_backorder_notification', array( $this, 'backorder' ) ); add_action( 'woocommerce_created_customer_notification', array( $this, 'customer_new_account' ), 10, 3 ); // Hook for replacing {site_title} in email-footer. add_filter( 'woocommerce_email_footer_text', array( $this, 'replace_placeholders' ) ); // Let 3rd parties unhook the above via this hook. do_action( 'woocommerce_email', $this ); } /** * Init email classes. */ public function init() { // Include email classes. include_once dirname( __FILE__ ) . '/emails/class-wc-email.php'; $this->emails['WC_Email_New_Order'] = include __DIR__ . '/emails/class-wc-email-new-order.php'; $this->emails['WC_Email_Cancelled_Order'] = include __DIR__ . '/emails/class-wc-email-cancelled-order.php'; $this->emails['WC_Email_Failed_Order'] = include __DIR__ . '/emails/class-wc-email-failed-order.php'; $this->emails['WC_Email_Customer_On_Hold_Order'] = include __DIR__ . '/emails/class-wc-email-customer-on-hold-order.php'; $this->emails['WC_Email_Customer_Processing_Order'] = include __DIR__ . '/emails/class-wc-email-customer-processing-order.php'; $this->emails['WC_Email_Customer_Completed_Order'] = include __DIR__ . '/emails/class-wc-email-customer-completed-order.php'; $this->emails['WC_Email_Customer_Refunded_Order'] = include __DIR__ . '/emails/class-wc-email-customer-refunded-order.php'; $this->emails['WC_Email_Customer_Invoice'] = include __DIR__ . '/emails/class-wc-email-customer-invoice.php'; $this->emails['WC_Email_Customer_Note'] = include __DIR__ . '/emails/class-wc-email-customer-note.php'; $this->emails['WC_Email_Customer_Reset_Password'] = include __DIR__ . '/emails/class-wc-email-customer-reset-password.php'; $this->emails['WC_Email_Customer_New_Account'] = include __DIR__ . '/emails/class-wc-email-customer-new-account.php'; $this->emails = apply_filters( 'woocommerce_email_classes', $this->emails ); } /** * Return the email classes - used in admin to load settings. * * @return WC_Email[] */ public function get_emails() { return $this->emails; } /** * Get from name for email. * * @return string */ public function get_from_name() { return wp_specialchars_decode( get_option( 'woocommerce_email_from_name' ), ENT_QUOTES ); } /** * Get from email address. * * @return string */ public function get_from_address() { return sanitize_email( get_option( 'woocommerce_email_from_address' ) ); } /** * Get the email header. * * @param mixed $email_heading Heading for the email. */ public function email_header( $email_heading ) { wc_get_template( 'emails/email-header.php', array( 'email_heading' => $email_heading ) ); } /** * Get the email footer. */ public function email_footer() { wc_get_template( 'emails/email-footer.php' ); } /** * Replace placeholder text in strings. * * @since 3.7.0 * @param string $string Email footer text. * @return string Email footer text with any replacements done. */ public function replace_placeholders( $string ) { $domain = wp_parse_url( home_url(), PHP_URL_HOST ); return str_replace( array( '{site_title}', '{site_address}', '{site_url}', '{woocommerce}', '{WooCommerce}', ), array( $this->get_blogname(), $domain, $domain, '<a href="https://woocommerce.com">WooCommerce</a>', '<a href="https://woocommerce.com">WooCommerce</a>', ), $string ); } /** * Filter callback to replace {site_title} in email footer * * @since 3.3.0 * @deprecated 3.7.0 * @param string $string Email footer text. * @return string Email footer text with any replacements done. */ public function email_footer_replace_site_title( $string ) { wc_deprecated_function( 'WC_Emails::email_footer_replace_site_title', '3.7.0', 'WC_Emails::replace_placeholders' ); return $this->replace_placeholders( $string ); } /** * Wraps a message in the woocommerce mail template. * * @param string $email_heading Heading text. * @param string $message Email message. * @param bool $plain_text Set true to send as plain text. Default to false. * * @return string */ public function wrap_message( $email_heading, $message, $plain_text = false ) { // Buffer. ob_start(); do_action( 'woocommerce_email_header', $email_heading, null ); echo wpautop( wptexturize( $message ) ); // WPCS: XSS ok. do_action( 'woocommerce_email_footer', null ); // Get contents. $message = ob_get_clean(); return $message; } /** * Send the email. * * @param mixed $to Receiver. * @param mixed $subject Email subject. * @param mixed $message Message. * @param string $headers Email headers (default: "Content-Type: text/html\r\n"). * @param string $attachments Attachments (default: ""). * @return bool */ public function send( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { // Send. $email = new WC_Email(); return $email->send( $to, $subject, $message, $headers, $attachments ); } /** * Prepare and send the customer invoice email on demand. * * @param int|WC_Order $order Order instance or ID. */ public function customer_invoice( $order ) { $email = $this->emails['WC_Email_Customer_Invoice']; if ( ! is_object( $order ) ) { $order = wc_get_order( absint( $order ) ); } $email->trigger( $order->get_id(), $order ); } /** * Customer new account welcome email. * * @param int $customer_id Customer ID. * @param array $new_customer_data New customer data. * @param bool $password_generated If password is generated. */ public function customer_new_account( $customer_id, $new_customer_data = array(), $password_generated = false ) { if ( ! $customer_id ) { return; } $user_pass = ! empty( $new_customer_data['user_pass'] ) ? $new_customer_data['user_pass'] : ''; $email = $this->emails['WC_Email_Customer_New_Account']; $email->trigger( $customer_id, $user_pass, $password_generated ); } /** * Show the order details table * * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. * @param string $email Email address. */ public function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) { if ( $plain_text ) { wc_get_template( 'emails/plain/email-order-details.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email, ) ); } else { wc_get_template( 'emails/email-order-details.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email, ) ); } } /** * Show order downloads in a table. * * @since 3.2.0 * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. * @param string $email Email address. */ public function order_downloads( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) { $show_downloads = $order->has_downloadable_item() && $order->is_download_permitted() && ! $sent_to_admin && ! is_a( $email, 'WC_Email_Customer_Refunded_Order' ); if ( ! $show_downloads ) { return; } $downloads = $order->get_downloadable_items(); $columns = apply_filters( 'woocommerce_email_downloads_columns', array( 'download-product' => __( 'Product', 'woocommerce' ), 'download-expires' => __( 'Expires', 'woocommerce' ), 'download-file' => __( 'Download', 'woocommerce' ), ) ); if ( $plain_text ) { wc_get_template( 'emails/plain/email-downloads.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email, 'downloads' => $downloads, 'columns' => $columns, ) ); } else { wc_get_template( 'emails/email-downloads.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email, 'downloads' => $downloads, 'columns' => $columns, ) ); } } /** * Add order meta to email templates. * * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. */ public function order_meta( $order, $sent_to_admin = false, $plain_text = false ) { $fields = apply_filters( 'woocommerce_email_order_meta_fields', array(), $sent_to_admin, $order ); /** * Deprecated woocommerce_email_order_meta_keys filter. * * @since 2.3.0 */ $_fields = apply_filters( 'woocommerce_email_order_meta_keys', array(), $sent_to_admin ); if ( $_fields ) { foreach ( $_fields as $key => $field ) { if ( is_numeric( $key ) ) { $key = $field; } $fields[ $key ] = array( 'label' => wptexturize( $key ), 'value' => wptexturize( get_post_meta( $order->get_id(), $field, true ) ), ); } } if ( $fields ) { if ( $plain_text ) { foreach ( $fields as $field ) { if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { echo $field['label'] . ': ' . $field['value'] . "\n"; // WPCS: XSS ok. } } } else { foreach ( $fields as $field ) { if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { echo '<p><strong>' . $field['label'] . ':</strong> ' . $field['value'] . '</p>'; // WPCS: XSS ok. } } } } } /** * Is customer detail field valid? * * @param array $field Field data to check if is valid. * @return boolean */ public function customer_detail_field_is_valid( $field ) { return isset( $field['label'] ) && ! empty( $field['value'] ); } /** * Allows developers to add additional customer details to templates. * * In versions prior to 3.2 this was used for notes, phone and email but this data has moved. * * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. */ public function customer_details( $order, $sent_to_admin = false, $plain_text = false ) { if ( ! is_a( $order, 'WC_Order' ) ) { return; } $fields = array_filter( apply_filters( 'woocommerce_email_customer_details_fields', array(), $sent_to_admin, $order ), array( $this, 'customer_detail_field_is_valid' ) ); if ( ! empty( $fields ) ) { if ( $plain_text ) { wc_get_template( 'emails/plain/email-customer-details.php', array( 'fields' => $fields ) ); } else { wc_get_template( 'emails/email-customer-details.php', array( 'fields' => $fields ) ); } } } /** * Get the email addresses. * * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. */ public function email_addresses( $order, $sent_to_admin = false, $plain_text = false ) { if ( ! is_a( $order, 'WC_Order' ) ) { return; } if ( $plain_text ) { wc_get_template( 'emails/plain/email-addresses.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, ) ); } else { wc_get_template( 'emails/email-addresses.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, ) ); } } /** * Get blog name formatted for emails. * * @return string */ private function get_blogname() { return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); } /** * Low stock notification email. * * @param WC_Product $product Product instance. */ public function low_stock( $product ) { if ( 'no' === get_option( 'woocommerce_notify_low_stock', 'yes' ) ) { return; } /** * Determine if the current product should trigger a low stock notification * * @param int $product_id - The low stock product id * * @since 4.7.0 */ if ( false === apply_filters( 'woocommerce_should_send_low_stock_notification', true, $product->get_id() ) ) { return; } $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product low in stock', 'woocommerce' ) ); $message = sprintf( /* translators: 1: product name 2: items in stock */ __( '%1$s is low in stock. There are %2$d left.', 'woocommerce' ), html_entity_decode( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), html_entity_decode( wp_strip_all_tags( $product->get_stock_quantity() ) ) ); wp_mail( apply_filters( 'woocommerce_email_recipient_low_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ), apply_filters( 'woocommerce_email_subject_low_stock', $subject, $product, null ), apply_filters( 'woocommerce_email_content_low_stock', $message, $product ), apply_filters( 'woocommerce_email_headers', '', 'low_stock', $product, null ), apply_filters( 'woocommerce_email_attachments', array(), 'low_stock', $product, null ) ); } /** * No stock notification email. * * @param WC_Product $product Product instance. */ public function no_stock( $product ) { if ( 'no' === get_option( 'woocommerce_notify_no_stock', 'yes' ) ) { return; } /** * Determine if the current product should trigger a no stock notification * * @param int $product_id - The out of stock product id * * @since 4.6.0 */ if ( false === apply_filters( 'woocommerce_should_send_no_stock_notification', true, $product->get_id() ) ) { return; } $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product out of stock', 'woocommerce' ) ); /* translators: %s: product name */ $message = sprintf( __( '%s is out of stock.', 'woocommerce' ), html_entity_decode( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ); wp_mail( apply_filters( 'woocommerce_email_recipient_no_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ), apply_filters( 'woocommerce_email_subject_no_stock', $subject, $product, null ), apply_filters( 'woocommerce_email_content_no_stock', $message, $product ), apply_filters( 'woocommerce_email_headers', '', 'no_stock', $product, null ), apply_filters( 'woocommerce_email_attachments', array(), 'no_stock', $product, null ) ); } /** * Backorder notification email. * * @param array $args Arguments. */ public function backorder( $args ) { $args = wp_parse_args( $args, array( 'product' => '', 'quantity' => '', 'order_id' => '', ) ); $order = wc_get_order( $args['order_id'] ); if ( ! $args['product'] || ! is_object( $args['product'] ) || ! $args['quantity'] || ! $order ) { return; } $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product backorder', 'woocommerce' ) ); /* translators: 1: product quantity 2: product name 3: order number */ $message = sprintf( __( '%1$s units of %2$s have been backordered in order #%3$s.', 'woocommerce' ), $args['quantity'], html_entity_decode( wp_strip_all_tags( $args['product']->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), $order->get_order_number() ); wp_mail( apply_filters( 'woocommerce_email_recipient_backorder', get_option( 'woocommerce_stock_email_recipient' ), $args, null ), apply_filters( 'woocommerce_email_subject_backorder', $subject, $args, null ), apply_filters( 'woocommerce_email_content_backorder', $message, $args ), apply_filters( 'woocommerce_email_headers', '', 'backorder', $args, null ), apply_filters( 'woocommerce_email_attachments', array(), 'backorder', $args, null ) ); } /** * Adds Schema.org markup for order in JSON-LD format. * * @deprecated 3.0.0 * @see WC_Structured_Data::generate_order_data() * * @since 2.6.0 * @param WC_Order $order Order instance. * @param bool $sent_to_admin If should sent to admin. * @param bool $plain_text If is plain text email. */ public function order_schema_markup( $order, $sent_to_admin = false, $plain_text = false ) { wc_deprecated_function( 'WC_Emails::order_schema_markup', '3.0', 'WC_Structured_Data::generate_order_data' ); WC()->structured_data->generate_order_data( $order, $sent_to_admin, $plain_text ); WC()->structured_data->output_structured_data(); } } includes/wc-page-functions.php 0000644 00000015657 15132754524 0012442 0 ustar 00 <?php /** * WooCommerce Page Functions * * Functions related to pages and menus. * * @package WooCommerce\Functions * @version 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Replace a page title with the endpoint title. * * @param string $title Post title. * @return string */ function wc_page_endpoint_title( $title ) { global $wp_query; if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && is_wc_endpoint_url() ) { $endpoint = WC()->query->get_current_endpoint(); $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; $endpoint_title = WC()->query->get_endpoint_title( $endpoint, $action ); $title = $endpoint_title ? $endpoint_title : $title; remove_filter( 'the_title', 'wc_page_endpoint_title' ); } return $title; } add_filter( 'the_title', 'wc_page_endpoint_title' ); /** * Retrieve page ids - used for myaccount, edit_address, shop, cart, checkout, pay, view_order, terms. returns -1 if no page is found. * * @param string $page Page slug. * @return int */ function wc_get_page_id( $page ) { if ( 'pay' === $page || 'thanks' === $page ) { wc_deprecated_argument( __FUNCTION__, '2.1', 'The "pay" and "thanks" pages are no-longer used - an endpoint is added to the checkout instead. To get a valid link use the WC_Order::get_checkout_payment_url() or WC_Order::get_checkout_order_received_url() methods instead.' ); $page = 'checkout'; } if ( 'change_password' === $page || 'edit_address' === $page || 'lost_password' === $page ) { wc_deprecated_argument( __FUNCTION__, '2.1', 'The "change_password", "edit_address" and "lost_password" pages are no-longer used - an endpoint is added to the my-account instead. To get a valid link use the wc_customer_edit_account_url() function instead.' ); $page = 'myaccount'; } $page = apply_filters( 'woocommerce_get_' . $page . '_page_id', get_option( 'woocommerce_' . $page . '_page_id' ) ); return $page ? absint( $page ) : -1; } /** * Retrieve page permalink. * * @param string $page page slug. * @param string|bool $fallback Fallback URL if page is not set. Defaults to home URL. @since 3.4.0. * @return string */ function wc_get_page_permalink( $page, $fallback = null ) { $page_id = wc_get_page_id( $page ); $permalink = 0 < $page_id ? get_permalink( $page_id ) : ''; if ( ! $permalink ) { $permalink = is_null( $fallback ) ? get_home_url() : $fallback; } return apply_filters( 'woocommerce_get_' . $page . '_page_permalink', $permalink ); } /** * Get endpoint URL. * * Gets the URL for an endpoint, which varies depending on permalink settings. * * @param string $endpoint Endpoint slug. * @param string $value Query param value. * @param string $permalink Permalink. * * @return string */ function wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { if ( ! $permalink ) { $permalink = get_permalink(); } // Map endpoint to options. $query_vars = WC()->query->get_query_vars(); $endpoint = ! empty( $query_vars[ $endpoint ] ) ? $query_vars[ $endpoint ] : $endpoint; $value = ( get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ) === $endpoint ) ? wc_edit_address_i18n( $value ) : $value; if ( get_option( 'permalink_structure' ) ) { if ( strstr( $permalink, '?' ) ) { $query_string = '?' . wp_parse_url( $permalink, PHP_URL_QUERY ); $permalink = current( explode( '?', $permalink ) ); } else { $query_string = ''; } $url = trailingslashit( $permalink ); if ( $value ) { $url .= trailingslashit( $endpoint ) . user_trailingslashit( $value ); } else { $url .= user_trailingslashit( $endpoint ); } $url .= $query_string; } else { $url = add_query_arg( $endpoint, $value, $permalink ); } return apply_filters( 'woocommerce_get_endpoint_url', $url, $endpoint, $value, $permalink ); } /** * Hide menu items conditionally. * * @param array $items Navigation items. * @return array */ function wc_nav_menu_items( $items ) { if ( ! is_user_logged_in() ) { $customer_logout = get_option( 'woocommerce_logout_endpoint', 'customer-logout' ); if ( ! empty( $customer_logout ) && ! empty( $items ) && is_array( $items ) ) { foreach ( $items as $key => $item ) { if ( empty( $item->url ) ) { continue; } $path = wp_parse_url( $item->url, PHP_URL_PATH ); $query = wp_parse_url( $item->url, PHP_URL_QUERY ); if ( strstr( $path, $customer_logout ) || strstr( $query, $customer_logout ) ) { unset( $items[ $key ] ); } } } } return $items; } add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_items', 10 ); /** * Fix active class in nav for shop page. * * @param array $menu_items Menu items. * @return array */ function wc_nav_menu_item_classes( $menu_items ) { if ( ! is_woocommerce() ) { return $menu_items; } $shop_page = wc_get_page_id( 'shop' ); $page_for_posts = (int) get_option( 'page_for_posts' ); if ( ! empty( $menu_items ) && is_array( $menu_items ) ) { foreach ( $menu_items as $key => $menu_item ) { $classes = (array) $menu_item->classes; $menu_id = (int) $menu_item->object_id; // Unset active class for blog page. if ( $page_for_posts === $menu_id ) { $menu_items[ $key ]->current = false; if ( in_array( 'current_page_parent', $classes, true ) ) { unset( $classes[ array_search( 'current_page_parent', $classes, true ) ] ); } if ( in_array( 'current-menu-item', $classes, true ) ) { unset( $classes[ array_search( 'current-menu-item', $classes, true ) ] ); } } elseif ( is_shop() && $shop_page === $menu_id && 'page' === $menu_item->object ) { // Set active state if this is the shop page link. $menu_items[ $key ]->current = true; $classes[] = 'current-menu-item'; $classes[] = 'current_page_item'; } elseif ( is_singular( 'product' ) && $shop_page === $menu_id ) { // Set parent state if this is a product page. $classes[] = 'current_page_parent'; } $menu_items[ $key ]->classes = array_unique( $classes ); } } return $menu_items; } add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_item_classes', 2 ); /** * Fix active class in wp_list_pages for shop page. * * See details in https://github.com/woocommerce/woocommerce/issues/177. * * @param string $pages Pages list. * @return string */ function wc_list_pages( $pages ) { if ( ! is_woocommerce() ) { return $pages; } // Remove current_page_parent class from any item. $pages = str_replace( 'current_page_parent', '', $pages ); // Find shop_page_id through woocommerce options. $shop_page = 'page-item-' . wc_get_page_id( 'shop' ); if ( is_shop() ) { // Add current_page_item class to shop page. return str_replace( $shop_page, $shop_page . ' current_page_item', $pages ); } // Add current_page_parent class to shop page. return str_replace( $shop_page, $shop_page . ' current_page_parent', $pages ); } add_filter( 'wp_list_pages', 'wc_list_pages' ); includes/customizer/class-wc-shop-customizer.php 0000644 00000073257 15132754524 0016202 0 ustar 00 <?php /** * Adds options to the customizer for WooCommerce. * * @version 3.3.0 * @package WooCommerce */ defined( 'ABSPATH' ) || exit; /** * WC_Shop_Customizer class. */ class WC_Shop_Customizer { /** * Constructor. */ public function __construct() { add_action( 'customize_register', array( $this, 'add_sections' ) ); add_action( 'customize_controls_print_styles', array( $this, 'add_styles' ) ); add_action( 'customize_controls_print_scripts', array( $this, 'add_scripts' ), 30 ); add_action( 'wp_enqueue_scripts', array( $this, 'add_frontend_scripts' ) ); add_action( 'admin_menu', array( $this, 'add_fse_customize_link' ) ); } /** * Add settings to the customizer. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. */ public function add_sections( $wp_customize ) { $wp_customize->add_panel( 'woocommerce', array( 'priority' => 200, 'capability' => 'manage_woocommerce', 'theme_supports' => '', 'title' => __( 'WooCommerce', 'woocommerce' ), ) ); $this->add_store_notice_section( $wp_customize ); $this->add_product_catalog_section( $wp_customize ); $this->add_product_images_section( $wp_customize ); $this->add_checkout_section( $wp_customize ); } /** * Frontend CSS styles. */ public function add_frontend_scripts() { if ( ! is_customize_preview() || ! is_store_notice_showing() ) { return; } $css = '.woocommerce-store-notice, p.demo_store { display: block !important; }'; wp_add_inline_style( 'customize-preview', $css ); } /** * CSS styles to improve our form. */ public function add_styles() { ?> <style type="text/css"> .woocommerce-cropping-control { margin: 0 40px 1em 0; padding: 0; display:inline-block; vertical-align: top; } .woocommerce-cropping-control input[type=radio] { margin-top: 1px; } .woocommerce-cropping-control span.woocommerce-cropping-control-aspect-ratio { margin-top: .5em; display:block; } .woocommerce-cropping-control span.woocommerce-cropping-control-aspect-ratio input { width: auto; display: inline-block; } <?php // For FSE themes hide the back button so we only surface WooCommerce options. if ( function_exists( 'gutenberg_is_fse_theme' ) && gutenberg_is_fse_theme() ) { ?> #sub-accordion-panel-woocommerce .customize-panel-back{ display: none; } #customize-controls #sub-accordion-panel-woocommerce .panel-meta.customize-info .accordion-section-title { margin-left: 0; } <?php } ?> </style> <?php } /** * Scripts to improve our form. */ public function add_scripts() { $min_rows = wc_get_theme_support( 'product_grid::min_rows', 1 ); $max_rows = wc_get_theme_support( 'product_grid::max_rows', '' ); $min_columns = wc_get_theme_support( 'product_grid::min_columns', 1 ); $max_columns = wc_get_theme_support( 'product_grid::max_columns', '' ); /* translators: %d: Setting value */ $min_notice = __( 'The minimum allowed setting is %d', 'woocommerce' ); /* translators: %d: Setting value */ $max_notice = __( 'The maximum allowed setting is %d', 'woocommerce' ); ?> <script type="text/javascript"> jQuery( function( $ ) { $( document.body ).on( 'change', '.woocommerce-cropping-control input[type="radio"]', function() { var $wrapper = $( this ).closest( '.woocommerce-cropping-control' ), value = $wrapper.find( 'input:checked' ).val(); if ( 'custom' === value ) { $wrapper.find( '.woocommerce-cropping-control-aspect-ratio' ).slideDown( 200 ); } else { $wrapper.find( '.woocommerce-cropping-control-aspect-ratio' ).hide(); } return false; } ); wp.customize.bind( 'ready', function() { // Ready? $( '.woocommerce-cropping-control' ).find( 'input:checked' ).trigger( 'change' ); } ); wp.customize( 'woocommerce_demo_store', function( setting ) { setting.bind( function( value ) { var notice = wp.customize( 'woocommerce_demo_store_notice' ); if ( value && ! notice.callbacks.has( notice.preview ) ) { notice.bind( notice.preview ); } else if ( ! value ) { notice.unbind( notice.preview ); } } ); } ); wp.customize( 'woocommerce_demo_store_notice', function( setting ) { setting.bind( function( value ) { var checkbox = wp.customize( 'woocommerce_demo_store' ); if ( checkbox.get() ) { $( '.woocommerce-store-notice' ).text( value ); } } ); } ); wp.customize.section( 'woocommerce_store_notice', function( section ) { section.expanded.bind( function( isExpanded ) { if ( isExpanded ) { var notice = wp.customize( 'woocommerce_demo_store_notice' ), checkbox = wp.customize( 'woocommerce_demo_store' ); if ( checkbox.get() && ! notice.callbacks.has( notice.preview ) ) { notice.bind( notice.preview ); } else if ( ! checkbox.get() ) { notice.unbind( notice.preview ); } } } ); } ); wp.customize.section( 'woocommerce_product_catalog', function( section ) { section.expanded.bind( function( isExpanded ) { if ( isExpanded ) { wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'shop' ) ); ?>' ); } } ); } ); wp.customize.section( 'woocommerce_product_images', function( section ) { section.expanded.bind( function( isExpanded ) { if ( isExpanded ) { wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'shop' ) ); ?>' ); } } ); } ); wp.customize.section( 'woocommerce_checkout', function( section ) { section.expanded.bind( function( isExpanded ) { if ( isExpanded ) { wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'checkout' ) ); ?>' ); } } ); } ); wp.customize( 'woocommerce_catalog_columns', function( setting ) { setting.bind( function( value ) { var min = parseInt( '<?php echo esc_js( $min_columns ); ?>', 10 ); var max = parseInt( '<?php echo esc_js( $max_columns ); ?>', 10 ); value = parseInt( value, 10 ); if ( max && value > max ) { setting.notifications.add( 'max_columns_error', new wp.customize.Notification( 'max_columns_error', { type : 'error', message: '<?php echo esc_js( sprintf( $max_notice, $max_columns ) ); ?>' } ) ); } else { setting.notifications.remove( 'max_columns_error' ); } if ( min && value < min ) { setting.notifications.add( 'min_columns_error', new wp.customize.Notification( 'min_columns_error', { type : 'error', message: '<?php echo esc_js( sprintf( $min_notice, $min_columns ) ); ?>' } ) ); } else { setting.notifications.remove( 'min_columns_error' ); } } ); } ); wp.customize( 'woocommerce_catalog_rows', function( setting ) { setting.bind( function( value ) { var min = parseInt( '<?php echo esc_js( $min_rows ); ?>', 10 ); var max = parseInt( '<?php echo esc_js( $max_rows ); ?>', 10 ); value = parseInt( value, 10 ); if ( max && value > max ) { setting.notifications.add( 'max_rows_error', new wp.customize.Notification( 'max_rows_error', { type : 'error', message: '<?php echo esc_js( sprintf( $max_notice, $max_rows ) ); ?>' } ) ); } else { setting.notifications.remove( 'max_rows_error' ); } if ( min && value < min ) { setting.notifications.add( 'min_rows_error', new wp.customize.Notification( 'min_rows_error', { type : 'error', message: '<?php echo esc_js( sprintf( $min_notice, $min_rows ) ); ?>' } ) ); } else { setting.notifications.remove( 'min_rows_error' ); } } ); } ); } ); </script> <?php } /** * For FSE themes add a "Customize WooCommerce" link to the Appearance menu. * * FSE themes hide the "Customize" link in the Appearance menu. In WooCommerce we have several options that can currently * only be edited via the Customizer. For now, we are thus adding a new link for WooCommerce specific Customizer options. */ public function add_fse_customize_link() { // Exit early if the FSE theme feature isn't present or the current theme is not a FSE theme. if ( ! function_exists( 'gutenberg_is_fse_theme' ) || function_exists( 'gutenberg_is_fse_theme' ) && ! gutenberg_is_fse_theme() ) { return; } // Add a link to the WooCommerce panel in the Customizer. add_submenu_page( 'themes.php', __( 'Customize WooCommerce', 'woocommerce' ), __( 'Customize WooCommerce', 'woocommerce' ), 'edit_theme_options', admin_url( 'customize.php?autofocus[panel]=woocommerce' ) ); } /** * Sanitize the shop page & category display setting. * * @param string $value '', 'subcategories', or 'both'. * @return string */ public function sanitize_archive_display( $value ) { $options = array( '', 'subcategories', 'both' ); return in_array( $value, $options, true ) ? $value : ''; } /** * Sanitize the catalog orderby setting. * * @param string $value An array key from the below array. * @return string */ public function sanitize_default_catalog_orderby( $value ) { $options = apply_filters( 'woocommerce_default_catalog_orderby_options', array( 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), 'rating' => __( 'Average rating', 'woocommerce' ), 'date' => __( 'Sort by most recent', 'woocommerce' ), 'price' => __( 'Sort by price (asc)', 'woocommerce' ), 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), ) ); return array_key_exists( $value, $options ) ? $value : 'menu_order'; } /** * Store notice section. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. */ private function add_store_notice_section( $wp_customize ) { $wp_customize->add_section( 'woocommerce_store_notice', array( 'title' => __( 'Store Notice', 'woocommerce' ), 'priority' => 10, 'panel' => 'woocommerce', ) ); $wp_customize->add_setting( 'woocommerce_demo_store', array( 'default' => 'no', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wc_bool_to_string', 'sanitize_js_callback' => 'wc_string_to_bool', ) ); $wp_customize->add_setting( 'woocommerce_demo_store_notice', array( 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wp_kses_post', 'transport' => 'postMessage', ) ); $wp_customize->add_control( 'woocommerce_demo_store_notice', array( 'label' => __( 'Store notice', 'woocommerce' ), 'description' => __( 'If enabled, this text will be shown site-wide. You can use it to show events or promotions to visitors!', 'woocommerce' ), 'section' => 'woocommerce_store_notice', 'settings' => 'woocommerce_demo_store_notice', 'type' => 'textarea', ) ); $wp_customize->add_control( 'woocommerce_demo_store', array( 'label' => __( 'Enable store notice', 'woocommerce' ), 'section' => 'woocommerce_store_notice', 'settings' => 'woocommerce_demo_store', 'type' => 'checkbox', ) ); if ( isset( $wp_customize->selective_refresh ) ) { $wp_customize->selective_refresh->add_partial( 'woocommerce_demo_store_notice', array( 'selector' => '.woocommerce-store-notice', 'container_inclusive' => true, 'render_callback' => 'woocommerce_demo_store', ) ); } } /** * Product catalog section. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. */ public function add_product_catalog_section( $wp_customize ) { $wp_customize->add_section( 'woocommerce_product_catalog', array( 'title' => __( 'Product Catalog', 'woocommerce' ), 'priority' => 10, 'panel' => 'woocommerce', ) ); $wp_customize->add_setting( 'woocommerce_shop_page_display', array( 'default' => '', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), ) ); $wp_customize->add_control( 'woocommerce_shop_page_display', array( 'label' => __( 'Shop page display', 'woocommerce' ), 'description' => __( 'Choose what to display on the main shop page.', 'woocommerce' ), 'section' => 'woocommerce_product_catalog', 'settings' => 'woocommerce_shop_page_display', 'type' => 'select', 'choices' => array( '' => __( 'Show products', 'woocommerce' ), 'subcategories' => __( 'Show categories', 'woocommerce' ), 'both' => __( 'Show categories & products', 'woocommerce' ), ), ) ); $wp_customize->add_setting( 'woocommerce_category_archive_display', array( 'default' => '', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), ) ); $wp_customize->add_control( 'woocommerce_category_archive_display', array( 'label' => __( 'Category display', 'woocommerce' ), 'description' => __( 'Choose what to display on product category pages.', 'woocommerce' ), 'section' => 'woocommerce_product_catalog', 'settings' => 'woocommerce_category_archive_display', 'type' => 'select', 'choices' => array( '' => __( 'Show products', 'woocommerce' ), 'subcategories' => __( 'Show subcategories', 'woocommerce' ), 'both' => __( 'Show subcategories & products', 'woocommerce' ), ), ) ); $wp_customize->add_setting( 'woocommerce_default_catalog_orderby', array( 'default' => 'menu_order', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => array( $this, 'sanitize_default_catalog_orderby' ), ) ); $wp_customize->add_control( 'woocommerce_default_catalog_orderby', array( 'label' => __( 'Default product sorting', 'woocommerce' ), 'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ), 'section' => 'woocommerce_product_catalog', 'settings' => 'woocommerce_default_catalog_orderby', 'type' => 'select', 'choices' => apply_filters( 'woocommerce_default_catalog_orderby_options', array( 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), 'rating' => __( 'Average rating', 'woocommerce' ), 'date' => __( 'Sort by most recent', 'woocommerce' ), 'price' => __( 'Sort by price (asc)', 'woocommerce' ), 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), ) ), ) ); // The following settings should be hidden if the theme is declaring the values. if ( has_filter( 'loop_shop_columns' ) ) { return; } $wp_customize->add_setting( 'woocommerce_catalog_columns', array( 'default' => 4, 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); $wp_customize->add_control( 'woocommerce_catalog_columns', array( 'label' => __( 'Products per row', 'woocommerce' ), 'description' => __( 'How many products should be shown per row?', 'woocommerce' ), 'section' => 'woocommerce_product_catalog', 'settings' => 'woocommerce_catalog_columns', 'type' => 'number', 'input_attrs' => array( 'min' => wc_get_theme_support( 'product_grid::min_columns', 1 ), 'max' => wc_get_theme_support( 'product_grid::max_columns', '' ), 'step' => 1, ), ) ); // Only add this setting if something else isn't managing the number of products per page. if ( ! has_filter( 'loop_shop_per_page' ) ) { $wp_customize->add_setting( 'woocommerce_catalog_rows', array( 'default' => 4, 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); } $wp_customize->add_control( 'woocommerce_catalog_rows', array( 'label' => __( 'Rows per page', 'woocommerce' ), 'description' => __( 'How many rows of products should be shown per page?', 'woocommerce' ), 'section' => 'woocommerce_product_catalog', 'settings' => 'woocommerce_catalog_rows', 'type' => 'number', 'input_attrs' => array( 'min' => wc_get_theme_support( 'product_grid::min_rows', 1 ), 'max' => wc_get_theme_support( 'product_grid::max_rows', '' ), 'step' => 1, ), ) ); } /** * Product images section. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. */ private function add_product_images_section( $wp_customize ) { if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) { $regen_description = ''; // Nothing to report; Jetpack will handle magically. } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && ! is_multisite() ) { $regen_description = __( 'After publishing your changes, new image sizes will be generated automatically.', 'woocommerce' ); } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && is_multisite() ) { /* translators: 1: tools URL 2: regen thumbs url */ $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you regenerate thumbnails. You can do this from the <a href="%1$s" target="_blank">tools section in WooCommerce</a> or by using a plugin such as <a href="%2$s" target="_blank">Regenerate Thumbnails</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status&tab=tools' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); } else { /* translators: %s: regen thumbs url */ $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you <a href="%s" target="_blank">Regenerate Thumbnails</a>.', 'woocommerce' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); } $wp_customize->add_section( 'woocommerce_product_images', array( 'title' => __( 'Product Images', 'woocommerce' ), 'description' => $regen_description, 'priority' => 20, 'panel' => 'woocommerce', ) ); if ( ! wc_get_theme_support( 'single_image_width' ) ) { $wp_customize->add_setting( 'woocommerce_single_image_width', array( 'default' => 600, 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); $wp_customize->add_control( 'woocommerce_single_image_width', array( 'label' => __( 'Main image width', 'woocommerce' ), 'description' => __( 'Image size used for the main image on single product pages. These images will remain uncropped.', 'woocommerce' ), 'section' => 'woocommerce_product_images', 'settings' => 'woocommerce_single_image_width', 'type' => 'number', 'input_attrs' => array( 'min' => 0, 'step' => 1, ), ) ); } if ( ! wc_get_theme_support( 'thumbnail_image_width' ) ) { $wp_customize->add_setting( 'woocommerce_thumbnail_image_width', array( 'default' => 300, 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); $wp_customize->add_control( 'woocommerce_thumbnail_image_width', array( 'label' => __( 'Thumbnail width', 'woocommerce' ), 'description' => __( 'Image size used for products in the catalog.', 'woocommerce' ), 'section' => 'woocommerce_product_images', 'settings' => 'woocommerce_thumbnail_image_width', 'type' => 'number', 'input_attrs' => array( 'min' => 0, 'step' => 1, ), ) ); } include_once WC_ABSPATH . 'includes/customizer/class-wc-customizer-control-cropping.php'; $wp_customize->add_setting( 'woocommerce_thumbnail_cropping', array( 'default' => '1:1', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wc_clean', ) ); $wp_customize->add_setting( 'woocommerce_thumbnail_cropping_custom_width', array( 'default' => '4', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); $wp_customize->add_setting( 'woocommerce_thumbnail_cropping_custom_height', array( 'default' => '3', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'absint', 'sanitize_js_callback' => 'absint', ) ); $wp_customize->add_control( new WC_Customizer_Control_Cropping( $wp_customize, 'woocommerce_thumbnail_cropping', array( 'section' => 'woocommerce_product_images', 'settings' => array( 'cropping' => 'woocommerce_thumbnail_cropping', 'custom_width' => 'woocommerce_thumbnail_cropping_custom_width', 'custom_height' => 'woocommerce_thumbnail_cropping_custom_height', ), 'label' => __( 'Thumbnail cropping', 'woocommerce' ), 'choices' => array( '1:1' => array( 'label' => __( '1:1', 'woocommerce' ), 'description' => __( 'Images will be cropped into a square', 'woocommerce' ), ), 'custom' => array( 'label' => __( 'Custom', 'woocommerce' ), 'description' => __( 'Images will be cropped to a custom aspect ratio', 'woocommerce' ), ), 'uncropped' => array( 'label' => __( 'Uncropped', 'woocommerce' ), 'description' => __( 'Images will display using the aspect ratio in which they were uploaded', 'woocommerce' ), ), ), ) ) ); } /** * Checkout section. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. */ public function add_checkout_section( $wp_customize ) { $wp_customize->add_section( 'woocommerce_checkout', array( 'title' => __( 'Checkout', 'woocommerce' ), 'priority' => 20, 'panel' => 'woocommerce', 'description' => __( 'These options let you change the appearance of the WooCommerce checkout.', 'woocommerce' ), ) ); // Checkout field controls. $fields = array( 'company' => __( 'Company name', 'woocommerce' ), 'address_2' => __( 'Address line 2', 'woocommerce' ), 'phone' => __( 'Phone', 'woocommerce' ), ); foreach ( $fields as $field => $label ) { $wp_customize->add_setting( 'woocommerce_checkout_' . $field . '_field', array( 'default' => 'phone' === $field ? 'required' : 'optional', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => array( $this, 'sanitize_checkout_field_display' ), ) ); $wp_customize->add_control( 'woocommerce_checkout_' . $field . '_field', array( /* Translators: %s field name. */ 'label' => sprintf( __( '%s field', 'woocommerce' ), $label ), 'section' => 'woocommerce_checkout', 'settings' => 'woocommerce_checkout_' . $field . '_field', 'type' => 'select', 'choices' => array( 'hidden' => __( 'Hidden', 'woocommerce' ), 'optional' => __( 'Optional', 'woocommerce' ), 'required' => __( 'Required', 'woocommerce' ), ), ) ); } // Register settings. $wp_customize->add_setting( 'woocommerce_checkout_highlight_required_fields', array( 'default' => 'yes', 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wc_bool_to_string', 'sanitize_js_callback' => 'wc_string_to_bool', ) ); $wp_customize->add_setting( 'woocommerce_checkout_terms_and_conditions_checkbox_text', array( /* translators: %s terms and conditions page name and link */ 'default' => sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ), 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wp_kses_post', 'transport' => 'postMessage', ) ); $wp_customize->add_setting( 'woocommerce_checkout_privacy_policy_text', array( /* translators: %s privacy policy page name and link */ 'default' => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), 'type' => 'option', 'capability' => 'manage_woocommerce', 'sanitize_callback' => 'wp_kses_post', 'transport' => 'postMessage', ) ); // Register controls. $wp_customize->add_control( 'woocommerce_checkout_highlight_required_fields', array( 'label' => __( 'Highlight required fields with an asterisk', 'woocommerce' ), 'section' => 'woocommerce_checkout', 'settings' => 'woocommerce_checkout_highlight_required_fields', 'type' => 'checkbox', ) ); if ( current_user_can( 'manage_privacy_options' ) ) { $choose_pages = array( 'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ), 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), ); } else { $choose_pages = array( 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), ); } $pages = get_pages( array( 'post_type' => 'page', 'post_status' => 'publish,private,draft', 'child_of' => 0, 'parent' => -1, 'exclude' => array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ), ), 'sort_order' => 'asc', 'sort_column' => 'post_title', ) ); $page_choices = array( '' => __( 'No page set', 'woocommerce' ) ) + array_combine( array_map( 'strval', wp_list_pluck( $pages, 'ID' ) ), wp_list_pluck( $pages, 'post_title' ) ); foreach ( $choose_pages as $id => $name ) { $wp_customize->add_setting( $id, array( 'default' => '', 'type' => 'option', 'capability' => 'manage_woocommerce', ) ); $wp_customize->add_control( $id, array( /* Translators: %s: page name. */ 'label' => sprintf( __( '%s page', 'woocommerce' ), $name ), 'section' => 'woocommerce_checkout', 'settings' => $id, 'type' => 'select', 'choices' => $page_choices, ) ); } $wp_customize->add_control( 'woocommerce_checkout_privacy_policy_text', array( 'label' => __( 'Privacy policy', 'woocommerce' ), 'description' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ), 'section' => 'woocommerce_checkout', 'settings' => 'woocommerce_checkout_privacy_policy_text', 'active_callback' => array( $this, 'has_privacy_policy_page_id' ), 'type' => 'textarea', ) ); $wp_customize->add_control( 'woocommerce_checkout_terms_and_conditions_checkbox_text', array( 'label' => __( 'Terms and conditions', 'woocommerce' ), 'description' => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ), 'section' => 'woocommerce_checkout', 'settings' => 'woocommerce_checkout_terms_and_conditions_checkbox_text', 'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ), 'type' => 'text', ) ); if ( isset( $wp_customize->selective_refresh ) ) { $wp_customize->selective_refresh->add_partial( 'woocommerce_checkout_privacy_policy_text', array( 'selector' => '.woocommerce-privacy-policy-text', 'container_inclusive' => true, 'render_callback' => 'wc_checkout_privacy_policy_text', ) ); $wp_customize->selective_refresh->add_partial( 'woocommerce_checkout_terms_and_conditions_checkbox_text', array( 'selector' => '.woocommerce-terms-and-conditions-checkbox-text', 'container_inclusive' => false, 'render_callback' => 'wc_terms_and_conditions_checkbox_text', ) ); } } /** * Sanitize field display. * * @param string $value '', 'subcategories', or 'both'. * @return string */ public function sanitize_checkout_field_display( $value ) { $options = array( 'hidden', 'optional', 'required' ); return in_array( $value, $options, true ) ? $value : ''; } /** * Whether or not a page has been chose for the privacy policy. * * @return bool */ public function has_privacy_policy_page_id() { return wc_privacy_policy_page_id() > 0; } /** * Whether or not a page has been chose for the terms and conditions. * * @return bool */ public function has_terms_and_conditions_page_id() { return wc_terms_and_conditions_page_id() > 0; } } new WC_Shop_Customizer(); includes/customizer/class-wc-customizer-control-cropping.php 0000644 00000003773 15132754524 0020524 0 ustar 00 <?php /** * Custom control for radio buttons with nested options. * * Used for our image cropping settings. * * @version 3.3.0 * @package WooCommerce */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Customizer_Control_Cropping class. */ class WC_Customizer_Control_Cropping extends WP_Customize_Control { /** * Declare the control type. * * @var string */ public $type = 'woocommerce-cropping-control'; /** * Render control. */ public function render_content() { if ( empty( $this->choices ) ) { return; } $value = $this->value( 'cropping' ); $custom_width = $this->value( 'custom_width' ); $custom_height = $this->value( 'custom_height' ); ?> <span class="customize-control-title"> <?php echo esc_html( $this->label ); ?> </span> <?php if ( ! empty( $this->description ) ) : ?> <span class="description customize-control-description"><?php echo esc_html( $this->description ); ?></span> <?php endif; ?> <ul id="input_<?php echo esc_attr( $this->id ); ?>" class="woocommerce-cropping-control"> <?php foreach ( $this->choices as $key => $radio ) : ?> <li> <input type="radio" name="<?php echo esc_attr( $this->id ); ?>" value="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $this->id . $key ); ?>" <?php $this->link( 'cropping' ); ?> <?php checked( $value, $key ); ?> /> <label for="<?php echo esc_attr( $this->id . $key ); ?>"><?php echo esc_html( $radio['label'] ); ?><br/><span class="description"><?php echo esc_html( $radio['description'] ); ?></span></label> <?php if ( 'custom' === $key ) : ?> <span class="woocommerce-cropping-control-aspect-ratio"> <input type="text" pattern="\d*" size="3" value="<?php echo esc_attr( $custom_width ); ?>" <?php $this->link( 'custom_width' ); ?> /> : <input type="text" pattern="\d*" size="3" value="<?php echo esc_attr( $custom_height ); ?>" <?php $this->link( 'custom_height' ); ?> /> </span> <?php endif; ?> </li> <?php endforeach; ?> </ul> <?php } } includes/wc-widget-functions.php 0000644 00000004017 15132754524 0012775 0 ustar 00 <?php /** * WooCommerce Widget Functions * * Widget related functions and widget registration. * * @package WooCommerce\Functions * @version 2.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } // Include widget classes. require_once dirname( __FILE__ ) . '/abstracts/abstract-wc-widget.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-cart.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-layered-nav-filters.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-layered-nav.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-price-filter.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-categories.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-search.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-tag-cloud.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-products.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-rating-filter.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-recent-reviews.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-recently-viewed.php'; require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-top-rated-products.php'; /** * Register Widgets. * * @since 2.3.0 */ function wc_register_widgets() { register_widget( 'WC_Widget_Cart' ); register_widget( 'WC_Widget_Layered_Nav_Filters' ); register_widget( 'WC_Widget_Layered_Nav' ); register_widget( 'WC_Widget_Price_Filter' ); register_widget( 'WC_Widget_Product_Categories' ); register_widget( 'WC_Widget_Product_Search' ); register_widget( 'WC_Widget_Product_Tag_Cloud' ); register_widget( 'WC_Widget_Products' ); register_widget( 'WC_Widget_Recently_Viewed' ); if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) { register_widget( 'WC_Widget_Top_Rated_Products' ); register_widget( 'WC_Widget_Recent_Reviews' ); register_widget( 'WC_Widget_Rating_Filter' ); } } add_action( 'widgets_init', 'wc_register_widgets' ); includes/wc-template-hooks.php 0000644 00000030611 15132754524 0012437 0 ustar 00 <?php /** * WooCommerce Template Hooks * * Action/filter hooks used for WooCommerce functions/templates. * * @package WooCommerce\Templates * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; add_filter( 'body_class', 'wc_body_class' ); add_filter( 'post_class', 'wc_product_post_class', 20, 3 ); /** * WP Header. * * @see wc_generator_tag() */ add_filter( 'get_the_generator_html', 'wc_generator_tag', 10, 2 ); add_filter( 'get_the_generator_xhtml', 'wc_generator_tag', 10, 2 ); /** * Content Wrappers. * * @see woocommerce_output_content_wrapper() * @see woocommerce_output_content_wrapper_end() */ add_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); add_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); /** * Sale flashes. * * @see woocommerce_show_product_loop_sale_flash() * @see woocommerce_show_product_sale_flash() */ add_action( 'woocommerce_before_shop_loop_item_title', 'woocommerce_show_product_loop_sale_flash', 10 ); add_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_sale_flash', 10 ); /** * Breadcrumbs. * * @see woocommerce_breadcrumb() */ add_action( 'woocommerce_before_main_content', 'woocommerce_breadcrumb', 20, 0 ); /** * Sidebar. * * @see woocommerce_get_sidebar() */ add_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 ); /** * Archive descriptions. * * @see woocommerce_taxonomy_archive_description() * @see woocommerce_product_archive_description() */ add_action( 'woocommerce_archive_description', 'woocommerce_taxonomy_archive_description', 10 ); add_action( 'woocommerce_archive_description', 'woocommerce_product_archive_description', 10 ); /** * Product loop start. */ add_filter( 'woocommerce_product_loop_start', 'woocommerce_maybe_show_product_subcategories' ); /** * Products Loop. * * @see woocommerce_result_count() * @see woocommerce_catalog_ordering() */ add_action( 'woocommerce_before_shop_loop', 'woocommerce_result_count', 20 ); add_action( 'woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30 ); add_action( 'woocommerce_no_products_found', 'wc_no_products_found' ); /** * Product Loop Items. * * @see woocommerce_template_loop_product_link_open() * @see woocommerce_template_loop_product_link_close() * @see woocommerce_template_loop_add_to_cart() * @see woocommerce_template_loop_product_thumbnail() * @see woocommerce_template_loop_product_title() * @see woocommerce_template_loop_category_link_open() * @see woocommerce_template_loop_category_title() * @see woocommerce_template_loop_category_link_close() * @see woocommerce_template_loop_price() * @see woocommerce_template_loop_rating() */ add_action( 'woocommerce_before_shop_loop_item', 'woocommerce_template_loop_product_link_open', 10 ); add_action( 'woocommerce_after_shop_loop_item', 'woocommerce_template_loop_product_link_close', 5 ); add_action( 'woocommerce_after_shop_loop_item', 'woocommerce_template_loop_add_to_cart', 10 ); add_action( 'woocommerce_before_shop_loop_item_title', 'woocommerce_template_loop_product_thumbnail', 10 ); add_action( 'woocommerce_shop_loop_item_title', 'woocommerce_template_loop_product_title', 10 ); add_action( 'woocommerce_before_subcategory', 'woocommerce_template_loop_category_link_open', 10 ); add_action( 'woocommerce_shop_loop_subcategory_title', 'woocommerce_template_loop_category_title', 10 ); add_action( 'woocommerce_after_subcategory', 'woocommerce_template_loop_category_link_close', 10 ); add_action( 'woocommerce_after_shop_loop_item_title', 'woocommerce_template_loop_price', 10 ); add_action( 'woocommerce_after_shop_loop_item_title', 'woocommerce_template_loop_rating', 5 ); /** * Subcategories. * * @see woocommerce_subcategory_thumbnail() */ add_action( 'woocommerce_before_subcategory_title', 'woocommerce_subcategory_thumbnail', 10 ); /** * Before Single Products Summary Div. * * @see woocommerce_show_product_images() * @see woocommerce_show_product_thumbnails() */ add_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20 ); add_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 ); /** * After Single Products Summary Div. * * @see woocommerce_output_product_data_tabs() * @see woocommerce_upsell_display() * @see woocommerce_output_related_products() */ add_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_product_data_tabs', 10 ); add_action( 'woocommerce_after_single_product_summary', 'woocommerce_upsell_display', 15 ); add_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_related_products', 20 ); /** * Product Summary Box. * * @see woocommerce_template_single_title() * @see woocommerce_template_single_rating() * @see woocommerce_template_single_price() * @see woocommerce_template_single_excerpt() * @see woocommerce_template_single_meta() * @see woocommerce_template_single_sharing() */ add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 ); add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_rating', 10 ); add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_price', 10 ); add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_excerpt', 20 ); add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_meta', 40 ); add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_sharing', 50 ); /** * Reviews * * @see woocommerce_review_display_gravatar() * @see woocommerce_review_display_rating() * @see woocommerce_review_display_meta() * @see woocommerce_review_display_comment_text() */ add_action( 'woocommerce_review_before', 'woocommerce_review_display_gravatar', 10 ); add_action( 'woocommerce_review_before_comment_meta', 'woocommerce_review_display_rating', 10 ); add_action( 'woocommerce_review_meta', 'woocommerce_review_display_meta', 10 ); add_action( 'woocommerce_review_comment_text', 'woocommerce_review_display_comment_text', 10 ); /** * Product Add to cart. * * @see woocommerce_template_single_add_to_cart() * @see woocommerce_simple_add_to_cart() * @see woocommerce_grouped_add_to_cart() * @see woocommerce_variable_add_to_cart() * @see woocommerce_external_add_to_cart() * @see woocommerce_single_variation() * @see woocommerce_single_variation_add_to_cart_button() */ add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 ); add_action( 'woocommerce_simple_add_to_cart', 'woocommerce_simple_add_to_cart', 30 ); add_action( 'woocommerce_grouped_add_to_cart', 'woocommerce_grouped_add_to_cart', 30 ); add_action( 'woocommerce_variable_add_to_cart', 'woocommerce_variable_add_to_cart', 30 ); add_action( 'woocommerce_external_add_to_cart', 'woocommerce_external_add_to_cart', 30 ); add_action( 'woocommerce_single_variation', 'woocommerce_single_variation', 10 ); add_action( 'woocommerce_single_variation', 'woocommerce_single_variation_add_to_cart_button', 20 ); /** * Pagination after shop loops. * * @see woocommerce_pagination() */ add_action( 'woocommerce_after_shop_loop', 'woocommerce_pagination', 10 ); /** * Product page tabs. */ add_filter( 'woocommerce_product_tabs', 'woocommerce_default_product_tabs' ); add_filter( 'woocommerce_product_tabs', 'woocommerce_sort_product_tabs', 99 ); /** * Additional Information tab. * * @see wc_display_product_attributes() */ add_action( 'woocommerce_product_additional_information', 'wc_display_product_attributes', 10 ); /** * Checkout. * * @see woocommerce_checkout_login_form() * @see woocommerce_checkout_coupon_form() * @see woocommerce_order_review() * @see woocommerce_checkout_payment() * @see wc_checkout_privacy_policy_text() * @see wc_terms_and_conditions_page_content() * @see wc_get_pay_buttons() */ add_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_login_form', 10 ); add_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10 ); add_action( 'woocommerce_checkout_order_review', 'woocommerce_order_review', 10 ); add_action( 'woocommerce_checkout_order_review', 'woocommerce_checkout_payment', 20 ); add_action( 'woocommerce_checkout_terms_and_conditions', 'wc_checkout_privacy_policy_text', 20 ); add_action( 'woocommerce_checkout_terms_and_conditions', 'wc_terms_and_conditions_page_content', 30 ); add_action( 'woocommerce_checkout_before_customer_details', 'wc_get_pay_buttons', 30 ); /** * Cart widget */ add_action( 'woocommerce_widget_shopping_cart_buttons', 'woocommerce_widget_shopping_cart_button_view_cart', 10 ); add_action( 'woocommerce_widget_shopping_cart_buttons', 'woocommerce_widget_shopping_cart_proceed_to_checkout', 20 ); add_action( 'woocommerce_widget_shopping_cart_total', 'woocommerce_widget_shopping_cart_subtotal', 10 ); /** * Cart. * * @see woocommerce_cross_sell_display() * @see woocommerce_cart_totals() * @see wc_get_pay_buttons() * @see woocommerce_button_proceed_to_checkout() * @see wc_empty_cart_message() */ add_action( 'woocommerce_cart_collaterals', 'woocommerce_cross_sell_display' ); add_action( 'woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10 ); add_action( 'woocommerce_proceed_to_checkout', 'wc_get_pay_buttons', 10 ); add_action( 'woocommerce_proceed_to_checkout', 'woocommerce_button_proceed_to_checkout', 20 ); add_action( 'woocommerce_cart_is_empty', 'wc_empty_cart_message', 10 ); /** * Footer. * * @see wc_print_js() * @see woocommerce_demo_store() */ add_action( 'wp_footer', 'wc_print_js', 25 ); add_action( 'wp_footer', 'woocommerce_demo_store' ); /** * Order details. * * @see woocommerce_order_details_table() * @see woocommerce_order_again_button() */ add_action( 'woocommerce_view_order', 'woocommerce_order_details_table', 10 ); add_action( 'woocommerce_thankyou', 'woocommerce_order_details_table', 10 ); add_action( 'woocommerce_order_details_after_order_table', 'woocommerce_order_again_button' ); /** * Order downloads. * * @see woocommerce_order_downloads_table() */ add_action( 'woocommerce_available_downloads', 'woocommerce_order_downloads_table', 10 ); /** * Auth. * * @see woocommerce_output_auth_header() * @see woocommerce_output_auth_footer() */ add_action( 'woocommerce_auth_page_header', 'woocommerce_output_auth_header', 10 ); add_action( 'woocommerce_auth_page_footer', 'woocommerce_output_auth_footer', 10 ); /** * Comments. * * Disable Jetpack comments. */ add_filter( 'jetpack_comment_form_enabled_for_product', '__return_false' ); /** * My Account. */ add_action( 'woocommerce_account_navigation', 'woocommerce_account_navigation' ); add_action( 'woocommerce_account_content', 'woocommerce_account_content' ); add_action( 'woocommerce_account_orders_endpoint', 'woocommerce_account_orders' ); add_action( 'woocommerce_account_view-order_endpoint', 'woocommerce_account_view_order' ); add_action( 'woocommerce_account_downloads_endpoint', 'woocommerce_account_downloads' ); add_action( 'woocommerce_account_edit-address_endpoint', 'woocommerce_account_edit_address' ); add_action( 'woocommerce_account_payment-methods_endpoint', 'woocommerce_account_payment_methods' ); add_action( 'woocommerce_account_add-payment-method_endpoint', 'woocommerce_account_add_payment_method' ); add_action( 'woocommerce_account_edit-account_endpoint', 'woocommerce_account_edit_account' ); add_action( 'woocommerce_register_form', 'wc_registration_privacy_policy_text', 20 ); /** * Notices. */ add_action( 'woocommerce_cart_is_empty', 'woocommerce_output_all_notices', 5 ); add_action( 'woocommerce_shortcode_before_product_cat_loop', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_shop_loop', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_single_product', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_cart', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_checkout_form_cart_notices', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_checkout_form', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_account_content', 'woocommerce_output_all_notices', 5 ); add_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 ); add_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 ); includes/class-wc-rest-authentication.php 0000644 00000047024 15132754524 0014606 0 ustar 00 <?php /** * REST API Authentication * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * REST API authentication class. */ class WC_REST_Authentication { /** * Authentication error. * * @var WP_Error */ protected $error = null; /** * Logged in user data. * * @var stdClass */ protected $user = null; /** * Current auth method. * * @var string */ protected $auth_method = ''; /** * Initialize authentication actions. */ public function __construct() { add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 ); add_filter( 'rest_authentication_errors', array( $this, 'authentication_fallback' ) ); add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 ); add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 ); } /** * Check if is request to our REST API. * * @return bool */ protected function is_request_to_rest_api() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // Check if the request is to the WC API endpoints. $woocommerce = ( false !== strpos( $request_uri, $rest_prefix . 'wc/' ) ); // Allow third party plugins use our authentication methods. $third_party = ( false !== strpos( $request_uri, $rest_prefix . 'wc-' ) ); return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $woocommerce || $third_party ); } /** * Authenticate user. * * @param int|false $user_id User ID if one has been determined, false otherwise. * @return int|false */ public function authenticate( $user_id ) { // Do not authenticate twice and check if is a request to our endpoint in the WP REST API. if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) { return $user_id; } if ( is_ssl() ) { $user_id = $this->perform_basic_authentication(); } if ( $user_id ) { return $user_id; } return $this->perform_oauth_authentication(); } /** * Authenticate the user if authentication wasn't performed during the * determine_current_user action. * * Necessary in cases where wp_get_current_user() is called before WooCommerce is loaded. * * @see https://github.com/woocommerce/woocommerce/issues/26847 * * @param WP_Error|null|bool $error Error data. * @return WP_Error|null|bool */ public function authentication_fallback( $error ) { if ( ! empty( $error ) ) { // Another plugin has already declared a failure. return $error; } if ( empty( $this->error ) && empty( $this->auth_method ) && empty( $this->user ) && 0 === get_current_user_id() ) { // Authentication hasn't occurred during `determine_current_user`, so check auth. $user_id = $this->authenticate( false ); if ( $user_id ) { wp_set_current_user( $user_id ); return true; } } return $error; } /** * Check for authentication error. * * @param WP_Error|null|bool $error Error data. * @return WP_Error|null|bool */ public function check_authentication_error( $error ) { // Pass through other errors. if ( ! empty( $error ) ) { return $error; } return $this->get_error(); } /** * Set authentication error. * * @param WP_Error $error Authentication error data. */ protected function set_error( $error ) { // Reset user. $this->user = null; $this->error = $error; } /** * Get authentication error. * * @return WP_Error|null. */ protected function get_error() { return $this->error; } /** * Basic Authentication. * * SSL-encrypted requests are not subject to sniffing or man-in-the-middle * attacks, so the request can be authenticated by simply looking up the user * associated with the given consumer key and confirming the consumer secret * provided is valid. * * @return int|bool */ private function perform_basic_authentication() { $this->auth_method = 'basic_auth'; $consumer_key = ''; $consumer_secret = ''; // If the $_GET parameters are present, use those first. if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { // WPCS: CSRF ok. $consumer_key = $_GET['consumer_key']; // WPCS: CSRF ok, sanitization ok. $consumer_secret = $_GET['consumer_secret']; // WPCS: CSRF ok, sanitization ok. } // If the above is not present, we will do full basic auth. if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { $consumer_key = $_SERVER['PHP_AUTH_USER']; // WPCS: CSRF ok, sanitization ok. $consumer_secret = $_SERVER['PHP_AUTH_PW']; // WPCS: CSRF ok, sanitization ok. } // Stop if don't have any key. if ( ! $consumer_key || ! $consumer_secret ) { return false; } // Get user data. $this->user = $this->get_user_data_by_consumer_key( $consumer_key ); if ( empty( $this->user ) ) { return false; } // Validate user secret. if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine $this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) ); return false; } return $this->user->user_id; } /** * Parse the Authorization header into parameters. * * @since 3.0.0 * * @param string $header Authorization header value (not including "Authorization: " prefix). * * @return array Map of parameter values. */ public function parse_header( $header ) { if ( 'OAuth ' !== substr( $header, 0, 6 ) ) { return array(); } // From OAuth PHP library, used under MIT license. $params = array(); if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) { foreach ( $matches[1] as $i => $h ) { $params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] ); } if ( isset( $params['realm'] ) ) { unset( $params['realm'] ); } } return $params; } /** * Get the authorization header. * * On certain systems and configurations, the Authorization header will be * stripped out by the server or PHP. Typically this is then used to * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use * `getallheaders` here to try and grab it out instead. * * @since 3.0.0 * * @return string Authorization header if set. */ public function get_authorization_header() { if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) { return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // WPCS: sanitization ok. } if ( function_exists( 'getallheaders' ) ) { $headers = getallheaders(); // Check for the authoization header case-insensitively. foreach ( $headers as $key => $value ) { if ( 'authorization' === strtolower( $key ) ) { return $value; } } } return ''; } /** * Get oAuth parameters from $_GET, $_POST or request header. * * @since 3.0.0 * * @return array|WP_Error */ public function get_oauth_parameters() { $params = array_merge( $_GET, $_POST ); // WPCS: CSRF ok. $params = wp_unslash( $params ); $header = $this->get_authorization_header(); if ( ! empty( $header ) ) { // Trim leading spaces. $header = trim( $header ); $header_params = $this->parse_header( $header ); if ( ! empty( $header_params ) ) { $params = array_merge( $params, $header_params ); } } $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method', ); $errors = array(); $have_one = false; // Check for required OAuth parameters. foreach ( $param_names as $param_name ) { if ( empty( $params[ $param_name ] ) ) { $errors[] = $param_name; } else { $have_one = true; } } // All keys are missing, so we're probably not even trying to use OAuth. if ( ! $have_one ) { return array(); } // If we have at least one supplied piece of data, and we have an error, // then it's a failed authentication. if ( ! empty( $errors ) ) { $message = sprintf( /* translators: %s: amount of errors */ _n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'woocommerce' ), implode( ', ', $errors ) ); $this->set_error( new WP_Error( 'woocommerce_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) ); return array(); } return $params; } /** * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests. * * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP. * * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: * * 1) There is no token associated with request/responses, only consumer keys/secrets are used. * * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, * This is because there is no cross-OS function within PHP to get the raw Authorization header. * * @link http://tools.ietf.org/html/rfc5849 for the full spec. * * @return int|bool */ private function perform_oauth_authentication() { $this->auth_method = 'oauth1'; $params = $this->get_oauth_parameters(); if ( empty( $params ) ) { return false; } // Fetch WP user by consumer key. $this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] ); if ( empty( $this->user ) ) { $this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) ); return false; } // Perform OAuth validation. $signature = $this->check_oauth_signature( $this->user, $params ); if ( is_wp_error( $signature ) ) { $this->set_error( $signature ); return false; } $timestamp_and_nonce = $this->check_oauth_timestamp_and_nonce( $this->user, $params['oauth_timestamp'], $params['oauth_nonce'] ); if ( is_wp_error( $timestamp_and_nonce ) ) { $this->set_error( $timestamp_and_nonce ); return false; } return $this->user->user_id; } /** * Verify that the consumer-provided request signature matches our generated signature, * this ensures the consumer has a valid key/secret. * * @param stdClass $user User data. * @param array $params The request parameters. * @return true|WP_Error */ private function check_oauth_signature( $user, $params ) { $http_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; // WPCS: sanitization ok. $request_path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : ''; // WPCS: sanitization ok. $wp_base = get_home_url( null, '/', 'relative' ); if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { $request_path = substr( $request_path, strlen( $wp_base ) ); } $base_request_uri = rawurlencode( get_home_url( null, $request_path, is_ssl() ? 'https' : 'http' ) ); // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); unset( $params['oauth_signature'] ); // Sort parameters. if ( ! uksort( $params, 'strcmp' ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) ); } // Normalize parameter key/values. $params = $this->normalize_parameters( $params ); $query_string = implode( '%26', $this->join_with_equals_sign( $params ) ); // Join with ampersand. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); $secret = $user->consumer_secret . '&'; $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { // @codingStandardsIgnoreLine return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); } return true; } /** * Creates an array of urlencoded strings out of each array key/value pairs. * * @param array $params Array of parameters to convert. * @param array $query_params Array to extend. * @param string $key Optional Array key to append. * @return string Array of urlencoded strings. */ private function join_with_equals_sign( $params, $query_params = array(), $key = '' ) { foreach ( $params as $param_key => $param_value ) { if ( $key ) { $param_key = $key . '%5B' . $param_key . '%5D'; // Handle multi-dimensional array. } if ( is_array( $param_value ) ) { $query_params = $this->join_with_equals_sign( $param_value, $query_params, $param_key ); } else { $string = $param_key . '=' . $param_value; // Join with equals sign. $query_params[] = wc_rest_urlencode_rfc3986( $string ); } } return $query_params; } /** * Normalize each parameter by assuming each parameter may have already been * encoded, so attempt to decode, and then re-encode according to RFC 3986. * * Note both the key and value is normalized so a filter param like: * * 'filter[period]' => 'week' * * is encoded to: * * 'filter%255Bperiod%255D' => 'week' * * This conforms to the OAuth 1.0a spec which indicates the entire query string * should be URL encoded. * * @see rawurlencode() * @param array $parameters Un-normalized parameters. * @return array Normalized parameters. */ private function normalize_parameters( $parameters ) { $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); $parameters = array_combine( $keys, $values ); return $parameters; } /** * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where * an attacker could attempt to re-send an intercepted request at a later time. * * - A timestamp is valid if it is within 15 minutes of now. * - A nonce is valid if it has not been used within the last 15 minutes. * * @param stdClass $user User data. * @param int $timestamp The unix timestamp for when the request was made. * @param string $nonce A unique (for the given user) 32 alphanumeric string, consumer-generated. * @return bool|WP_Error */ private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { global $wpdb; $valid_window = 15 * 60; // 15 minute window. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); } $used_nonces = maybe_unserialize( $user->nonces ); if ( empty( $used_nonces ) ) { $used_nonces = array(); } if ( in_array( $nonce, $used_nonces, true ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) ); } $used_nonces[ $timestamp ] = $nonce; // Remove expired nonces. foreach ( $used_nonces as $nonce_timestamp => $nonce ) { if ( $nonce_timestamp < ( time() - $valid_window ) ) { unset( $used_nonces[ $nonce_timestamp ] ); } } $used_nonces = maybe_serialize( $used_nonces ); $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'nonces' => $used_nonces ), array( 'key_id' => $user->key_id ), array( '%s' ), array( '%d' ) ); return true; } /** * Return the user data for the given consumer_key. * * @param string $consumer_key Consumer key. * @return array */ private function get_user_data_by_consumer_key( $consumer_key ) { global $wpdb; $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); $user = $wpdb->get_row( $wpdb->prepare( " SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = %s ", $consumer_key ) ); return $user; } /** * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. * * @param string $method Request method. * @return bool|WP_Error */ private function check_permissions( $method ) { $permissions = $this->user->permissions; switch ( $method ) { case 'HEAD': case 'GET': if ( 'read' !== $permissions && 'read_write' !== $permissions ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); } break; case 'POST': case 'PUT': case 'PATCH': case 'DELETE': if ( 'write' !== $permissions && 'read_write' !== $permissions ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); } break; case 'OPTIONS': return true; default: return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Unknown request method.', 'woocommerce' ), array( 'status' => 401 ) ); } return true; } /** * Updated API Key last access datetime. */ private function update_last_access() { global $wpdb; $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', array( 'last_access' => current_time( 'mysql' ) ), array( 'key_id' => $this->user->key_id ), array( '%s' ), array( '%d' ) ); } /** * If the consumer_key and consumer_secret $_GET parameters are NOT provided * and the Basic auth headers are either not present or the consumer secret does not match the consumer * key provided, then return the correct Basic headers and an error message. * * @param WP_REST_Response $response Current response being served. * @return WP_REST_Response */ public function send_unauthorized_headers( $response ) { if ( is_wp_error( $this->get_error() ) && 'basic_auth' === $this->auth_method ) { $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); } return $response; } /** * Check for user permissions and register last access. * * @param mixed $result Response to replace the requested version with. * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. * @return mixed */ public function check_user_permissions( $result, $server, $request ) { if ( $this->user ) { // Check API Key permissions. $allowed = $this->check_permissions( $request->get_method() ); if ( is_wp_error( $allowed ) ) { return $allowed; } // Register last access. $this->update_last_access(); } return $result; } } new WC_REST_Authentication(); includes/class-wc-rate-limiter.php 0000644 00000004130 15132754524 0013201 0 ustar 00 <?php /** * Provide basic rate limiting functionality via WP Options API. * * Currently only provides a simple limit by delaying action by X seconds. * * Example usage: * * When an action runs, call set_rate_limit, e.g.: * * WC_Rate_Limiter::set_rate_limit( "{$my_action_name}_{$user_id}", $delay ); * * This sets a timestamp for future timestamp after which action can run again. * * * Then before running the action again, check if the action is allowed to run, e.g.: * * if ( WC_Rate_Limiter::retried_too_soon( "{$my_action_name}_{$user_id}" ) ) { * add_notice( 'Sorry, too soon!' ); * } * * @package WooCommerce\Classes * @version 3.9.0 * @since 3.9.0 */ defined( 'ABSPATH' ) || exit; /** * Rate limit class. */ class WC_Rate_Limiter { /** * Constructs Option name from action identifier. * * @param string $action_id Identifier of the action. * @return string */ public static function storage_id( $action_id ) { return 'woocommerce_rate_limit_' . $action_id; } /** * Returns true if the action is not allowed to be run by the rate limiter yet, false otherwise. * * @param string $action_id Identifier of the action. * @return bool */ public static function retried_too_soon( $action_id ) { $next_try_allowed_at = get_option( self::storage_id( $action_id ) ); // No record of action running, so action is allowed to run. if ( false === $next_try_allowed_at ) { return false; } // Before the next run is allowed, retry forbidden. if ( time() <= $next_try_allowed_at ) { return true; } // After the next run is allowed, retry allowed. return false; } /** * Sets the rate limit delay in seconds for action with identifier $id. * * @param string $action_id Identifier of the action. * @param int $delay Delay in seconds. * @return bool True if the option setting was successful, false otherwise. */ public static function set_rate_limit( $action_id, $delay ) { $option_name = self::storage_id( $action_id ); $next_try_allowed_at = time() + $delay; return update_option( $option_name, $next_try_allowed_at ); } } includes/class-wc-order-item.php 0000644 00000025264 15132754524 0012665 0 ustar 00 <?php /** * Order Item * * A class which represents an item within an order and handles CRUD. * Uses ArrayAccess to be BW compatible with WC_Orders::get_items(). * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item class. */ class WC_Order_Item extends WC_Data implements ArrayAccess { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $data = array( 'order_id' => 0, 'name' => '', ); /** * Stores meta in cache for future reads. * A group must be set to to enable caching. * * @var string */ protected $cache_group = 'order-items'; /** * Meta type. This should match up with * the types available at https://developer.wordpress.org/reference/functions/add_metadata/. * WP defines 'post', 'user', 'comment', and 'term'. * * @var string */ protected $meta_type = 'order_item'; /** * This is the name of this object type. * * @var string */ protected $object_type = 'order_item'; /** * Constructor. * * @param int|object|array $item ID to load from the DB, or WC_Order_Item object. */ public function __construct( $item = 0 ) { parent::__construct( $item ); if ( $item instanceof WC_Order_Item ) { $this->set_id( $item->get_id() ); } elseif ( is_numeric( $item ) && $item > 0 ) { $this->set_id( $item ); } else { $this->set_object_read( true ); } $type = 'line_item' === $this->get_type() ? 'product' : $this->get_type(); $this->data_store = WC_Data_Store::load( 'order-item-' . $type ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } /** * Merge changes with data and clear. * Overrides WC_Data::apply_changes. * array_replace_recursive does not work well for order items because it merges taxes instead * of replacing them. * * @since 3.2.0 */ public function apply_changes() { if ( function_exists( 'array_replace' ) ) { $this->data = array_replace( $this->data, $this->changes ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_replaceFound } else { // PHP 5.2 compatibility. foreach ( $this->changes as $key => $change ) { $this->data[ $key ] = $change; } } $this->changes = array(); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order ID this meta belongs to. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return int */ public function get_order_id( $context = 'view' ) { return $this->get_prop( 'order_id', $context ); } /** * Get order item name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { return $this->get_prop( 'name', $context ); } /** * Get order item type. Overridden by child classes. * * @return string */ public function get_type() { return ''; } /** * Get quantity. * * @return int */ public function get_quantity() { return 1; } /** * Get tax status. * * @return string */ public function get_tax_status() { return 'taxable'; } /** * Get tax class. * * @return string */ public function get_tax_class() { return ''; } /** * Get parent order object. * * @return WC_Order */ public function get_order() { return wc_get_order( $this->get_order_id() ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set order ID. * * @param int $value Order ID. */ public function set_order_id( $value ) { $this->set_prop( 'order_id', absint( $value ) ); } /** * Set order item name. * * @param string $value Item name. */ public function set_name( $value ) { $this->set_prop( 'name', wp_check_invalid_utf8( $value ) ); } /* |-------------------------------------------------------------------------- | Other Methods |-------------------------------------------------------------------------- */ /** * Type checking. * * @param string|array $type Type. * @return boolean */ public function is_type( $type ) { return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type(); } /** * Calculate item taxes. * * @since 3.2.0 * @param array $calculate_tax_for Location data to get taxes for. Required. * @return bool True if taxes were calculated. */ public function calculate_taxes( $calculate_tax_for = array() ) { if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { return false; } if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) { $calculate_tax_for['tax_class'] = $this->get_tax_class(); $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); if ( method_exists( $this, 'get_subtotal' ) ) { $subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ); $this->set_taxes( array( 'total' => $taxes, 'subtotal' => $subtotal_taxes, ) ); } else { $this->set_taxes( array( 'total' => $taxes ) ); } } else { $this->set_taxes( false ); } do_action( 'woocommerce_order_item_after_calculate_taxes', $this, $calculate_tax_for ); return true; } /* |-------------------------------------------------------------------------- | Meta Data Handling |-------------------------------------------------------------------------- */ /** * Expands things like term slugs before return. * * @param string $hideprefix Meta data prefix, (default: _). * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. * @return array */ public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) { $formatted_meta = array(); $meta_data = $this->get_meta_data(); $hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0; $product = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false; $order_item_name = $this->get_name(); foreach ( $meta_data as $meta ) { if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) { continue; } $meta->key = rawurldecode( (string) $meta->key ); $meta->value = rawurldecode( (string) $meta->value ); $attribute_key = str_replace( 'attribute_', '', $meta->key ); $display_key = wc_attribute_label( $attribute_key, $product ); $display_value = wp_kses_post( $meta->value ); if ( taxonomy_exists( $attribute_key ) ) { $term = get_term_by( 'slug', $meta->value, $attribute_key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $display_value = $term->name; } } // Skip items with values already in the product details area of the product name. if ( ! $include_all && $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { continue; } $formatted_meta[ $meta->id ] = (object) array( 'key' => $meta->key, 'value' => $meta->value, 'display_key' => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ), 'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ), ); } return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * OffsetSet for ArrayAccess. * * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { if ( 'item_meta_array' === $offset ) { foreach ( $value as $meta_id => $meta ) { $this->update_meta_data( $meta->key, $meta->value, $meta_id ); } return; } if ( array_key_exists( $offset, $this->data ) ) { $setter = "set_$offset"; if ( is_callable( array( $this, $setter ) ) ) { $this->$setter( $value ); } return; } $this->update_meta_data( $offset, $value ); } /** * OffsetUnset for ArrayAccess. * * @param string $offset Offset. */ public function offsetUnset( $offset ) { $this->maybe_read_meta_data(); if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) { $this->meta_data = array(); return; } if ( array_key_exists( $offset, $this->data ) ) { unset( $this->data[ $offset ] ); } if ( array_key_exists( $offset, $this->changes ) ) { unset( $this->changes[ $offset ] ); } $this->delete_meta_data( $offset ); } /** * OffsetExists for ArrayAccess. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { $this->maybe_read_meta_data(); if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) { return true; } return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ); } /** * OffsetGet for ArrayAccess. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { $this->maybe_read_meta_data(); if ( 'item_meta_array' === $offset ) { $return = array(); foreach ( $this->meta_data as $meta ) { $return[ $meta->id ] = $meta; } return $return; } $meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' ); if ( 'item_meta' === $offset ) { return $meta_values; } elseif ( 'type' === $offset ) { return $this->get_type(); } elseif ( array_key_exists( $offset, $this->data ) ) { $getter = "get_$offset"; if ( is_callable( array( $this, $getter ) ) ) { return $this->$getter(); } } elseif ( array_key_exists( '_' . $offset, $meta_values ) ) { // Item meta was expanded in previous versions, with prefixes removed. This maintains support. return $meta_values[ '_' . $offset ]; } elseif ( array_key_exists( $offset, $meta_values ) ) { return $meta_values[ $offset ]; } return null; } } includes/wc-deprecated-functions.php 0000644 00000101265 15132754524 0013615 0 ustar 00 <?php /** * Deprecated functions * * Where functions come to die. * * @author Automattic * @category Core * @package WooCommerce\Functions * @version 3.3.0 */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Runs a deprecated action with notice only if used. * * @since 3.0.0 * @param string $tag The name of the action hook. * @param array $args Array of additional function arguments to be passed to do_action(). * @param string $version The version of WooCommerce that deprecated the hook. * @param string $replacement The hook that should have been used. * @param string $message A message regarding the change. */ function wc_do_deprecated_action( $tag, $args, $version, $replacement = null, $message = null ) { if ( ! has_action( $tag ) ) { return; } wc_deprecated_hook( $tag, $version, $replacement, $message ); do_action_ref_array( $tag, $args ); } /** * Wrapper for deprecated functions so we can apply some extra logic. * * @since 3.0.0 * @param string $function Function used. * @param string $version Version the message was added in. * @param string $replacement Replacement for the called function. */ function wc_deprecated_function( $function, $version, $replacement = null ) { // @codingStandardsIgnoreStart if ( is_ajax() || WC()->is_rest_api_request() ) { do_action( 'deprecated_function_run', $function, $replacement, $version ); $log_string = "The {$function} function is deprecated since version {$version}."; $log_string .= $replacement ? " Replace with {$replacement}." : ''; error_log( $log_string ); } else { _deprecated_function( $function, $version, $replacement ); } // @codingStandardsIgnoreEnd } /** * Wrapper for deprecated hook so we can apply some extra logic. * * @since 3.3.0 * @param string $hook The hook that was used. * @param string $version The version of WordPress that deprecated the hook. * @param string $replacement The hook that should have been used. * @param string $message A message regarding the change. */ function wc_deprecated_hook( $hook, $version, $replacement = null, $message = null ) { // @codingStandardsIgnoreStart if ( is_ajax() || WC()->is_rest_api_request() ) { do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message ); $message = empty( $message ) ? '' : ' ' . $message; $log_string = "{$hook} is deprecated since version {$version}"; $log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.'; error_log( $log_string . $message ); } else { _deprecated_hook( $hook, $version, $replacement, $message ); } // @codingStandardsIgnoreEnd } /** * When catching an exception, this allows us to log it if unexpected. * * @since 3.3.0 * @param Exception $exception_object The exception object. * @param string $function The function which threw exception. * @param array $args The args passed to the function. */ function wc_caught_exception( $exception_object, $function = '', $args = array() ) { // @codingStandardsIgnoreStart $message = $exception_object->getMessage(); $message .= '. Args: ' . print_r( $args, true ) . '.'; do_action( 'woocommerce_caught_exception', $exception_object, $function, $args ); error_log( "Exception caught in {$function}. {$message}." ); // @codingStandardsIgnoreEnd } /** * Wrapper for _doing_it_wrong(). * * @since 3.0.0 * @param string $function Function used. * @param string $message Message to log. * @param string $version Version the message was added in. */ function wc_doing_it_wrong( $function, $message, $version ) { // @codingStandardsIgnoreStart $message .= ' Backtrace: ' . wp_debug_backtrace_summary(); if ( is_ajax() || WC()->is_rest_api_request() ) { do_action( 'doing_it_wrong_run', $function, $message, $version ); error_log( "{$function} was called incorrectly. {$message}. This message was added in version {$version}." ); } else { _doing_it_wrong( $function, $message, $version ); } // @codingStandardsIgnoreEnd } /** * Wrapper for deprecated arguments so we can apply some extra logic. * * @since 3.0.0 * @param string $argument * @param string $version * @param string $replacement */ function wc_deprecated_argument( $argument, $version, $message = null ) { if ( is_ajax() || WC()->is_rest_api_request() ) { do_action( 'deprecated_argument_run', $argument, $message, $version ); error_log( "The {$argument} argument is deprecated since version {$version}. {$message}" ); } else { _deprecated_argument( $argument, $version, $message ); } } /** * @deprecated 2.1 */ function woocommerce_show_messages() { wc_deprecated_function( 'woocommerce_show_messages', '2.1', 'wc_print_notices' ); wc_print_notices(); } /** * @deprecated 2.1 */ function woocommerce_weekend_area_js() { wc_deprecated_function( 'woocommerce_weekend_area_js', '2.1' ); } /** * @deprecated 2.1 */ function woocommerce_tooltip_js() { wc_deprecated_function( 'woocommerce_tooltip_js', '2.1' ); } /** * @deprecated 2.1 */ function woocommerce_datepicker_js() { wc_deprecated_function( 'woocommerce_datepicker_js', '2.1' ); } /** * @deprecated 2.1 */ function woocommerce_admin_scripts() { wc_deprecated_function( 'woocommerce_admin_scripts', '2.1' ); } /** * @deprecated 2.1 */ function woocommerce_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) { wc_deprecated_function( 'woocommerce_create_page', '2.1', 'wc_create_page' ); return wc_create_page( $slug, $option, $page_title, $page_content, $post_parent ); } /** * @deprecated 2.1 */ function woocommerce_readfile_chunked( $file, $retbytes = true ) { wc_deprecated_function( 'woocommerce_readfile_chunked', '2.1', 'WC_Download_Handler::readfile_chunked()' ); return WC_Download_Handler::readfile_chunked( $file ); } /** * Formal total costs - format to the number of decimal places for the base currency. * * @access public * @param mixed $number * @deprecated 2.1 * @return string */ function woocommerce_format_total( $number ) { wc_deprecated_function( __FUNCTION__, '2.1', 'wc_format_decimal()' ); return wc_format_decimal( $number, wc_get_price_decimals(), false ); } /** * Get product name with extra details such as SKU price and attributes. Used within admin. * * @access public * @param WC_Product $product * @deprecated 2.1 * @return string */ function woocommerce_get_formatted_product_name( $product ) { wc_deprecated_function( __FUNCTION__, '2.1', 'WC_Product::get_formatted_name()' ); return $product->get_formatted_name(); } /** * Handle IPN requests for the legacy paypal gateway by calling gateways manually if needed. * * @access public */ function woocommerce_legacy_paypal_ipn() { if ( ! empty( $_GET['paypalListener'] ) && 'paypal_standard_IPN' === $_GET['paypalListener'] ) { WC()->payment_gateways(); do_action( 'woocommerce_api_wc_gateway_paypal' ); } } add_action( 'init', 'woocommerce_legacy_paypal_ipn' ); /** * @deprecated 3.0 */ function get_product( $the_product = false, $args = array() ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product' ); return wc_get_product( $the_product, $args ); } /** * @deprecated 3.0 */ function woocommerce_protected_product_add_to_cart( $passed, $product_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_protected_product_add_to_cart' ); return wc_protected_product_add_to_cart( $passed, $product_id ); } /** * @deprecated 3.0 */ function woocommerce_empty_cart() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_empty_cart' ); wc_empty_cart(); } /** * @deprecated 3.0 */ function woocommerce_load_persistent_cart( $user_login, $user = 0 ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_load_persistent_cart' ); return wc_load_persistent_cart( $user_login, $user ); } /** * @deprecated 3.0 */ function woocommerce_add_to_cart_message( $product_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_to_cart_message' ); wc_add_to_cart_message( $product_id ); } /** * @deprecated 3.0 */ function woocommerce_clear_cart_after_payment() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clear_cart_after_payment' ); wc_clear_cart_after_payment(); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_subtotal_html() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_subtotal_html' ); wc_cart_totals_subtotal_html(); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_shipping_html() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_html' ); wc_cart_totals_shipping_html(); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_coupon_html( $coupon ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_coupon_html' ); wc_cart_totals_coupon_html( $coupon ); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_order_total_html() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_order_total_html' ); wc_cart_totals_order_total_html(); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_fee_html( $fee ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_fee_html' ); wc_cart_totals_fee_html( $fee ); } /** * @deprecated 3.0 */ function woocommerce_cart_totals_shipping_method_label( $method ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_method_label' ); return wc_cart_totals_shipping_method_label( $method ); } /** * @deprecated 3.0 */ function woocommerce_get_template_part( $slug, $name = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template_part' ); wc_get_template_part( $slug, $name ); } /** * @deprecated 3.0 */ function woocommerce_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template' ); wc_get_template( $template_name, $args, $template_path, $default_path ); } /** * @deprecated 3.0 */ function woocommerce_locate_template( $template_name, $template_path = '', $default_path = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_locate_template' ); return wc_locate_template( $template_name, $template_path, $default_path ); } /** * @deprecated 3.0 */ function woocommerce_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_mail' ); wc_mail( $to, $subject, $message, $headers, $attachments ); } /** * @deprecated 3.0 */ function woocommerce_disable_admin_bar( $show_admin_bar ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_disable_admin_bar' ); return wc_disable_admin_bar( $show_admin_bar ); } /** * @deprecated 3.0 */ function woocommerce_create_new_customer( $email, $username = '', $password = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_create_new_customer' ); return wc_create_new_customer( $email, $username, $password ); } /** * @deprecated 3.0 */ function woocommerce_set_customer_auth_cookie( $customer_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_customer_auth_cookie' ); wc_set_customer_auth_cookie( $customer_id ); } /** * @deprecated 3.0 */ function woocommerce_update_new_customer_past_orders( $customer_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_new_customer_past_orders' ); return wc_update_new_customer_past_orders( $customer_id ); } /** * @deprecated 3.0 */ function woocommerce_paying_customer( $order_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_paying_customer' ); wc_paying_customer( $order_id ); } /** * @deprecated 3.0 */ function woocommerce_customer_bought_product( $customer_email, $user_id, $product_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_bought_product' ); return wc_customer_bought_product( $customer_email, $user_id, $product_id ); } /** * @deprecated 3.0 */ function woocommerce_customer_has_capability( $allcaps, $caps, $args ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_has_capability' ); return wc_customer_has_capability( $allcaps, $caps, $args ); } /** * @deprecated 3.0 */ function woocommerce_sanitize_taxonomy_name( $taxonomy ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_sanitize_taxonomy_name' ); return wc_sanitize_taxonomy_name( $taxonomy ); } /** * @deprecated 3.0 */ function woocommerce_get_filename_from_url( $file_url ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_filename_from_url' ); return wc_get_filename_from_url( $file_url ); } /** * @deprecated 3.0 */ function woocommerce_get_dimension( $dim, $to_unit ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_dimension' ); return wc_get_dimension( $dim, $to_unit ); } /** * @deprecated 3.0 */ function woocommerce_get_weight( $weight, $to_unit ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_weight' ); return wc_get_weight( $weight, $to_unit ); } /** * @deprecated 3.0 */ function woocommerce_trim_zeros( $price ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_trim_zeros' ); return wc_trim_zeros( $price ); } /** * @deprecated 3.0 */ function woocommerce_round_tax_total( $tax ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_round_tax_total' ); return wc_round_tax_total( $tax ); } /** * @deprecated 3.0 */ function woocommerce_format_decimal( $number, $dp = false, $trim_zeros = false ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_decimal' ); return wc_format_decimal( $number, $dp, $trim_zeros ); } /** * @deprecated 3.0 */ function woocommerce_clean( $var ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clean' ); return wc_clean( $var ); } /** * @deprecated 3.0 */ function woocommerce_array_overlay( $a1, $a2 ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_array_overlay' ); return wc_array_overlay( $a1, $a2 ); } /** * @deprecated 3.0 */ function woocommerce_price( $price, $args = array() ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_price' ); return wc_price( $price, $args ); } /** * @deprecated 3.0 */ function woocommerce_let_to_num( $size ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_let_to_num' ); return wc_let_to_num( $size ); } /** * @deprecated 3.0 */ function woocommerce_date_format() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_date_format' ); return wc_date_format(); } /** * @deprecated 3.0 */ function woocommerce_time_format() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_time_format' ); return wc_time_format(); } /** * @deprecated 3.0 */ function woocommerce_timezone_string() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_timezone_string' ); return wc_timezone_string(); } if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { /** * @deprecated 3.0 */ function woocommerce_rgb_from_hex( $color ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_rgb_from_hex' ); return wc_rgb_from_hex( $color ); } } if ( ! function_exists( 'woocommerce_hex_darker' ) ) { /** * @deprecated 3.0 */ function woocommerce_hex_darker( $color, $factor = 30 ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_darker' ); return wc_hex_darker( $color, $factor ); } } if ( ! function_exists( 'woocommerce_hex_lighter' ) ) { /** * @deprecated 3.0 */ function woocommerce_hex_lighter( $color, $factor = 30 ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_lighter' ); return wc_hex_lighter( $color, $factor ); } } if ( ! function_exists( 'woocommerce_light_or_dark' ) ) { /** * @deprecated 3.0 */ function woocommerce_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_light_or_dark' ); return wc_light_or_dark( $color, $dark, $light ); } } if ( ! function_exists( 'woocommerce_format_hex' ) ) { /** * @deprecated 3.0 */ function woocommerce_format_hex( $hex ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_hex' ); return wc_format_hex( $hex ); } } /** * @deprecated 3.0 */ function woocommerce_get_order_id_by_order_key( $order_key ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_id_by_order_key' ); return wc_get_order_id_by_order_key( $order_key ); } /** * @deprecated 3.0 */ function woocommerce_downloadable_file_permission( $download_id, $product_id, $order ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_file_permission' ); return wc_downloadable_file_permission( $download_id, $product_id, $order ); } /** * @deprecated 3.0 */ function woocommerce_downloadable_product_permissions( $order_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_product_permissions' ); wc_downloadable_product_permissions( $order_id ); } /** * @deprecated 3.0 */ function woocommerce_add_order_item( $order_id, $item ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item' ); return wc_add_order_item( $order_id, $item ); } /** * @deprecated 3.0 */ function woocommerce_delete_order_item( $item_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item' ); return wc_delete_order_item( $item_id ); } /** * @deprecated 3.0 */ function woocommerce_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_order_item_meta' ); return wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value ); } /** * @deprecated 3.0 */ function woocommerce_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item_meta' ); return wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique ); } /** * @deprecated 3.0 */ function woocommerce_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item_meta' ); return wc_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all ); } /** * @deprecated 3.0 */ function woocommerce_get_order_item_meta( $item_id, $key, $single = true ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_item_meta' ); return wc_get_order_item_meta( $item_id, $key, $single ); } /** * @deprecated 3.0 */ function woocommerce_cancel_unpaid_orders() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cancel_unpaid_orders' ); wc_cancel_unpaid_orders(); } /** * @deprecated 3.0 */ function woocommerce_processing_order_count() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_processing_order_count' ); return wc_processing_order_count(); } /** * @deprecated 3.0 */ function woocommerce_get_page_id( $page ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_page_id' ); return wc_get_page_id( $page ); } /** * @deprecated 3.0 */ function woocommerce_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_endpoint_url' ); return wc_get_endpoint_url( $endpoint, $value, $permalink ); } /** * @deprecated 3.0 */ function woocommerce_lostpassword_url( $url ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_lostpassword_url' ); return wc_lostpassword_url( $url ); } /** * @deprecated 3.0 */ function woocommerce_customer_edit_account_url() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_edit_account_url' ); return wc_customer_edit_account_url(); } /** * @deprecated 3.0 */ function woocommerce_nav_menu_items( $items, $args ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_items' ); return wc_nav_menu_items( $items ); } /** * @deprecated 3.0 */ function woocommerce_nav_menu_item_classes( $menu_items, $args ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_item_classes' ); return wc_nav_menu_item_classes( $menu_items ); } /** * @deprecated 3.0 */ function woocommerce_list_pages( $pages ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_list_pages' ); return wc_list_pages( $pages ); } /** * @deprecated 3.0 */ function woocommerce_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_dropdown_categories' ); return wc_product_dropdown_categories( $args, $deprecated_hierarchical, $deprecated_show_uncategorized, $deprecated_orderby ); } /** * @deprecated 3.0 */ function woocommerce_walk_category_dropdown_tree( $a1 = '', $a2 = '', $a3 = '' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_walk_category_dropdown_tree' ); return wc_walk_category_dropdown_tree( $a1, $a2, $a3 ); } /** * @deprecated 3.0 */ function woocommerce_taxonomy_metadata_wpdbfix() { wc_deprecated_function( __FUNCTION__, '3.0' ); } /** * @deprecated 3.0 */ function wc_taxonomy_metadata_wpdbfix() { wc_deprecated_function( __FUNCTION__, '3.0' ); } /** * @deprecated 3.0 */ function woocommerce_order_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_reorder_terms' ); return wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $terms ); } /** * @deprecated 3.0 */ function woocommerce_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_term_order' ); return wc_set_term_order( $term_id, $index, $taxonomy, $recursive ); } /** * @deprecated 3.0 */ function woocommerce_terms_clauses( $clauses, $taxonomies, $args ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_terms_clauses' ); return wc_terms_clauses( $clauses, $taxonomies, $args ); } /** * @deprecated 3.0 */ function _woocommerce_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ) { wc_deprecated_function( __FUNCTION__, '3.0', '_wc_term_recount' ); return _wc_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ); } /** * @deprecated 3.0 */ function woocommerce_recount_after_stock_change( $product_id ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_recount_after_stock_change' ); return wc_recount_after_stock_change( $product_id ); } /** * @deprecated 3.0 */ function woocommerce_change_term_counts( $terms, $taxonomies, $args ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_change_term_counts' ); return wc_change_term_counts( $terms, $taxonomies ); } /** * @deprecated 3.0 */ function woocommerce_get_product_ids_on_sale() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_ids_on_sale' ); return wc_get_product_ids_on_sale(); } /** * @deprecated 3.0 */ function woocommerce_get_featured_product_ids() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_featured_product_ids' ); return wc_get_featured_product_ids(); } /** * @deprecated 3.0 */ function woocommerce_get_product_terms( $object_id, $taxonomy, $fields = 'all' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_terms' ); return wc_get_product_terms( $object_id, $taxonomy, array( 'fields' => $fields ) ); } /** * @deprecated 3.0 */ function woocommerce_product_post_type_link( $permalink, $post ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_post_type_link' ); return wc_product_post_type_link( $permalink, $post ); } /** * @deprecated 3.0 */ function woocommerce_placeholder_img_src() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img_src' ); return wc_placeholder_img_src(); } /** * @deprecated 3.0 */ function woocommerce_placeholder_img( $size = 'woocommerce_thumbnail' ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img' ); return wc_placeholder_img( $size ); } /** * @deprecated 3.0 */ function woocommerce_get_formatted_variation( $variation = '', $flat = false ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_formatted_variation' ); return wc_get_formatted_variation( $variation, $flat ); } /** * @deprecated 3.0 */ function woocommerce_scheduled_sales() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_scheduled_sales' ); return wc_scheduled_sales(); } /** * @deprecated 3.0 */ function woocommerce_get_attachment_image_attributes( $attr ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_attachment_image_attributes' ); return wc_get_attachment_image_attributes( $attr ); } /** * @deprecated 3.0 */ function woocommerce_prepare_attachment_for_js( $response ) { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_prepare_attachment_for_js' ); return wc_prepare_attachment_for_js( $response ); } /** * @deprecated 3.0 */ function woocommerce_track_product_view() { wc_deprecated_function( __FUNCTION__, '3.0', 'wc_track_product_view' ); return wc_track_product_view(); } /** * @deprecated 2.3 has no replacement */ function woocommerce_compile_less_styles() { wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' ); } /** * woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and * the woocommerce_ship_to_countries setting. * @deprecated 2.6.0 * @return string */ function woocommerce_calc_shipping_backwards_compatibility( $value ) { if ( Constants::is_defined( 'WC_UPDATING' ) ) { return $value; } return 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ? 'no' : 'yes'; } add_filter( 'pre_option_woocommerce_calc_shipping', 'woocommerce_calc_shipping_backwards_compatibility' ); /** * @deprecated 3.0.0 * @see WC_Structured_Data class * * @return string */ function woocommerce_get_product_schema() { wc_deprecated_function( 'woocommerce_get_product_schema', '3.0' ); global $product; $schema = "Product"; // Downloadable product schema handling if ( $product->is_downloadable() ) { switch ( $product->download_type ) { case 'application' : $schema = "SoftwareApplication"; break; case 'music' : $schema = "MusicAlbum"; break; default : $schema = "Product"; break; } } return 'http://schema.org/' . $schema; } /** * Save product price. * * This is a private function (internal use ONLY) used until a data manipulation api is built. * * @deprecated 3.0.0 * @param int $product_id * @param float $regular_price * @param float $sale_price * @param string $date_from * @param string $date_to */ function _wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) { wc_doing_it_wrong( '_wc_save_product_price()', 'This function is not for developer use and is deprecated.', '3.0' ); $product_id = absint( $product_id ); $regular_price = wc_format_decimal( $regular_price ); $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price ); $date_from = wc_clean( $date_from ); $date_to = wc_clean( $date_to ); update_post_meta( $product_id, '_regular_price', $regular_price ); update_post_meta( $product_id, '_sale_price', $sale_price ); // Save Dates update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' ); update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' ); if ( $date_to && ! $date_from ) { $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); } // Update price if on sale if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) { update_post_meta( $product_id, '_price', $sale_price ); } else { update_post_meta( $product_id, '_price', $regular_price ); } if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $product_id, '_price', $sale_price ); } if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $product_id, '_price', $regular_price ); update_post_meta( $product_id, '_sale_price_dates_from', '' ); update_post_meta( $product_id, '_sale_price_dates_to', '' ); } } /** * Return customer avatar URL. * * @deprecated 3.1.0 * @since 2.6.0 * @param string $email the customer's email. * @return string the URL to the customer's avatar. */ function wc_get_customer_avatar_url( $email ) { // Deprecated in favor of WordPress get_avatar_url() function. wc_deprecated_function( 'wc_get_customer_avatar_url()', '3.1', 'get_avatar_url()' ); return get_avatar_url( $email ); } /** * WooCommerce Core Supported Themes. * * @deprecated 3.3.0 * @since 2.2 * @return string[] */ function wc_get_core_supported_themes() { wc_deprecated_function( 'wc_get_core_supported_themes()', '3.3' ); return array( 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten' ); } /** * Get min/max price meta query args. * * @deprecated 3.6.0 * @since 3.0.0 * @param array $args Min price and max price arguments. * @return array */ function wc_get_min_max_price_meta_query( $args ) { wc_deprecated_function( 'wc_get_min_max_price_meta_query()', '3.6' ); $current_min_price = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; $current_max_price = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : PHP_INT_MAX; return apply_filters( 'woocommerce_get_min_max_price_meta_query', array( 'key' => '_price', 'value' => array( $current_min_price, $current_max_price ), 'compare' => 'BETWEEN', 'type' => 'DECIMAL(10,' . wc_get_price_decimals() . ')', ), $args ); } /** * When a term is split, ensure meta data maintained. * * @deprecated 3.6.0 * @param int $old_term_id Old term ID. * @param int $new_term_id New term ID. * @param string $term_taxonomy_id Term taxonomy ID. * @param string $taxonomy Taxonomy. */ function wc_taxonomy_metadata_update_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { wc_deprecated_function( 'wc_taxonomy_metadata_update_content_for_split_terms', '3.6' ); } /** * WooCommerce Term Meta API. * * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. * This function serves as a wrapper, using the new table if present, or falling back to the WC table. * * @deprecated 3.6.0 * @param int $term_id Term ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param string $prev_value Previous value. (default: ''). * @return bool */ function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { wc_deprecated_function( 'update_woocommerce_term_meta', '3.6', 'update_term_meta' ); return function_exists( 'update_term_meta' ) ? update_term_meta( $term_id, $meta_key, $meta_value, $prev_value ) : update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value ); } /** * WooCommerce Term Meta API. * * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. * This function serves as a wrapper, using the new table if present, or falling back to the WC table. * * @deprecated 3.6.0 * @param int $term_id Term ID. * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. * @param bool $unique Make meta key unique. (default: false). * @return bool */ function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { wc_deprecated_function( 'add_woocommerce_term_meta', '3.6', 'add_term_meta' ); return function_exists( 'add_term_meta' ) ? add_term_meta( $term_id, $meta_key, $meta_value, $unique ) : add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique ); } /** * WooCommerce Term Meta API * * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. * This function serves as a wrapper, using the new table if present, or falling back to the WC table. * * @deprecated 3.6.0 * @param int $term_id Term ID. * @param string $meta_key Meta key. * @param string $meta_value Meta value (default: ''). * @param bool $deprecated Deprecated param (default: false). * @return bool */ function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $deprecated = false ) { wc_deprecated_function( 'delete_woocommerce_term_meta', '3.6', 'delete_term_meta' ); return function_exists( 'delete_term_meta' ) ? delete_term_meta( $term_id, $meta_key, $meta_value ) : delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value ); } /** * WooCommerce Term Meta API * * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. * This function serves as a wrapper, using the new table if present, or falling back to the WC table. * * @deprecated 3.6.0 * @param int $term_id Term ID. * @param string $key Meta key. * @param bool $single Whether to return a single value. (default: true). * @return mixed */ function get_woocommerce_term_meta( $term_id, $key, $single = true ) { wc_deprecated_function( 'get_woocommerce_term_meta', '3.6', 'get_term_meta' ); return function_exists( 'get_term_meta' ) ? get_term_meta( $term_id, $key, $single ) : get_metadata( 'woocommerce_term', $term_id, $key, $single ); } includes/class-wc-product-download.php 0000644 00000016073 15132754524 0014101 0 ustar 00 <?php /** * Represents a file which can be downloaded. * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Product download class. */ class WC_Product_Download implements ArrayAccess { /** * Data array. * * @since 3.0.0 * @var array */ protected $data = array( 'id' => '', 'name' => '', 'file' => '', ); /** * Returns all data for this object. * * @return array */ public function get_data() { return $this->data; } /** * Get allowed mime types. * * @return array */ public function get_allowed_mime_types() { return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() ); } /** * Get type of file path set. * * @param string $file_path optional. * @return string absolute, relative, or shortcode. */ public function get_type_of_file_path( $file_path = '' ) { $file_path = $file_path ? $file_path : $this->get_file(); $parsed_url = parse_url( $file_path ); if ( $parsed_url && isset( $parsed_url['host'] ) && // Absolute url means that it has a host. ( // Theoretically we could permit any scheme (like ftp as well), but that has not been the case before. So we allow none or http(s). ! isset( $parsed_url['scheme'] ) || in_array( $parsed_url['scheme'], array( 'http', 'https' ) ) ) ) { return 'absolute'; } elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) { return 'shortcode'; } else { return 'relative'; } } /** * Get file type. * * @return string */ public function get_file_type() { $type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() ); return $type['type']; } /** * Get file extension. * * @return string */ public function get_file_extension() { $parsed_url = wp_parse_url( $this->get_file(), PHP_URL_PATH ); return pathinfo( $parsed_url, PATHINFO_EXTENSION ); } /** * Check if file is allowed. * * @return boolean */ public function is_allowed_filetype() { $file_path = $this->get_file(); // File types for URL-based files located on the server should get validated. $parsed_file_path = WC_Download_Handler::parse_file_path( $file_path ); $is_file_on_server = ! $parsed_file_path['remote_file']; $file_path_type = $this->get_type_of_file_path( $file_path ); // Shortcodes are allowed, validations should be done by the shortcode provider in this case. if ( 'shortcode' === $file_path_type ) { return true; } // Remote paths are allowed. if ( ! $is_file_on_server && 'relative' !== $file_path_type ) { return true; } // On windows system, local files ending with `.` are not allowed. // @link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions. if ( $is_file_on_server && ! $this->get_file_extension() && 'WIN' === strtoupper( substr( Constants::get_constant( 'PHP_OS' ), 0, 3 ) ) ) { if ( '.' === substr( $file_path, -1 ) ) { return false; } } return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true ); } /** * Validate file exists. * * @return boolean */ public function file_exists() { if ( 'relative' !== $this->get_type_of_file_path() ) { return true; } $file_url = $this->get_file(); if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) { $file_url = realpath( ABSPATH . $file_url ); } elseif ( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) === substr( $file_url, 0, strlen( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) ) ) ) { $file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) ); } return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set ID. * * @param string $value Download ID. */ public function set_id( $value ) { $this->data['id'] = wc_clean( $value ); } /** * Set name. * * @param string $value Download name. */ public function set_name( $value ) { $this->data['name'] = wc_clean( $value ); } /** * Set previous_hash. * * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. * @param string $value Previous hash. */ public function set_previous_hash( $value ) { wc_deprecated_function( __FUNCTION__, '3.3' ); $this->data['previous_hash'] = wc_clean( $value ); } /** * Set file. * * @param string $value File URL/Path. */ public function set_file( $value ) { // A `///` is recognized as an "absolute", but on the filesystem, so it bypasses the mime check in `self::is_allowed_filetype`. // This will strip extra prepending / to the maximum of 2. if ( preg_match( '#^//+(/[^/].+)$#i', $value, $matches ) ) { $value = $matches[1]; } switch ( $this->get_type_of_file_path( $value ) ) { case 'absolute': $this->data['file'] = esc_url_raw( $value ); break; default: $this->data['file'] = wc_clean( $value ); break; } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get id. * * @return string */ public function get_id() { return $this->data['id']; } /** * Get name. * * @return string */ public function get_name() { return $this->data['name']; } /** * Get previous_hash. * * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. * @return string */ public function get_previous_hash() { wc_deprecated_function( __FUNCTION__, '3.3' ); return $this->data['previous_hash']; } /** * Get file. * * @return string */ public function get_file() { return $this->data['file']; } /* |-------------------------------------------------------------------------- | ArrayAccess/Backwards compatibility. |-------------------------------------------------------------------------- */ /** * OffsetGet. * * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { switch ( $offset ) { default: if ( is_callable( array( $this, "get_$offset" ) ) ) { return $this->{"get_$offset"}(); } break; } return ''; } /** * OffsetSet. * * @param string $offset Offset. * @param mixed $value Offset value. */ public function offsetSet( $offset, $value ) { switch ( $offset ) { default: if ( is_callable( array( $this, "set_$offset" ) ) ) { return $this->{"set_$offset"}( $value ); } break; } } /** * OffsetUnset. * * @param string $offset Offset. */ public function offsetUnset( $offset ) {} /** * OffsetExists. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { return in_array( $offset, array_keys( $this->data ), true ); } } includes/class-wc-ajax.php 0000644 00000307370 15132754524 0011542 0 ustar 00 <?php /** * WooCommerce WC_AJAX. AJAX Event Handlers. * * @class WC_AJAX * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * WC_Ajax class. */ class WC_AJAX { /** * Hook in ajax handlers. */ public static function init() { add_action( 'init', array( __CLASS__, 'define_ajax' ), 0 ); add_action( 'template_redirect', array( __CLASS__, 'do_wc_ajax' ), 0 ); self::add_ajax_events(); } /** * Get WC Ajax Endpoint. * * @param string $request Optional. * * @return string */ public static function get_endpoint( $request = '' ) { return esc_url_raw( apply_filters( 'woocommerce_ajax_get_endpoint', add_query_arg( 'wc-ajax', $request, remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), home_url( '/', 'relative' ) ) ), $request ) ); } /** * Set WC AJAX constant and headers. */ public static function define_ajax() { // phpcs:disable if ( ! empty( $_GET['wc-ajax'] ) ) { wc_maybe_define_constant( 'DOING_AJAX', true ); wc_maybe_define_constant( 'WC_DOING_AJAX', true ); if ( ! WP_DEBUG || ( WP_DEBUG && ! WP_DEBUG_DISPLAY ) ) { @ini_set( 'display_errors', 0 ); // Turn off display_errors during AJAX events to prevent malformed JSON. } $GLOBALS['wpdb']->hide_errors(); } // phpcs:enable } /** * Send headers for WC Ajax Requests. * * @since 2.5.0 */ private static function wc_ajax_headers() { if ( ! headers_sent() ) { send_origin_headers(); send_nosniff_header(); wc_nocache_headers(); header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); header( 'X-Robots-Tag: noindex' ); status_header( 200 ); } elseif ( Constants::is_true( 'WP_DEBUG' ) ) { headers_sent( $file, $line ); trigger_error( "wc_ajax_headers cannot set headers - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine } } /** * Check for WC Ajax request and fire action. */ public static function do_wc_ajax() { global $wp_query; // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['wc-ajax'] ) ) { $wp_query->set( 'wc-ajax', sanitize_text_field( wp_unslash( $_GET['wc-ajax'] ) ) ); } $action = $wp_query->get( 'wc-ajax' ); if ( $action ) { self::wc_ajax_headers(); $action = sanitize_text_field( $action ); do_action( 'wc_ajax_' . $action ); wp_die(); } // phpcs:enable } /** * Hook in methods - uses WordPress ajax handlers (admin-ajax). */ public static function add_ajax_events() { $ajax_events_nopriv = array( 'get_refreshed_fragments', 'apply_coupon', 'remove_coupon', 'update_shipping_method', 'get_cart_totals', 'update_order_review', 'add_to_cart', 'remove_from_cart', 'checkout', 'get_variation', 'get_customer_location', ); foreach ( $ajax_events_nopriv as $ajax_event ) { add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) ); add_action( 'wp_ajax_nopriv_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) ); // WC AJAX can be used for frontend ajax requests. add_action( 'wc_ajax_' . $ajax_event, array( __CLASS__, $ajax_event ) ); } $ajax_events = array( 'feature_product', 'mark_order_status', 'get_order_details', 'add_attribute', 'add_new_attribute', 'remove_variations', 'save_attributes', 'add_variation', 'link_all_variations', 'revoke_access_to_download', 'grant_access_to_download', 'get_customer_details', 'add_order_item', 'add_order_fee', 'add_order_shipping', 'add_order_tax', 'add_coupon_discount', 'remove_order_coupon', 'remove_order_item', 'remove_order_tax', 'reduce_order_item_stock', 'increase_order_item_stock', 'add_order_item_meta', 'remove_order_item_meta', 'calc_line_taxes', 'save_order_items', 'load_order_items', 'add_order_note', 'delete_order_note', 'json_search_products', 'json_search_products_and_variations', 'json_search_downloadable_products_and_variations', 'json_search_customers', 'json_search_categories', 'json_search_pages', 'term_ordering', 'product_ordering', 'refund_line_items', 'delete_refund', 'rated', 'update_api_key', 'load_variations', 'save_variations', 'bulk_edit_variations', 'tax_rates_save_changes', 'shipping_zones_save_changes', 'shipping_zone_add_method', 'shipping_zone_methods_save_changes', 'shipping_zone_methods_save_settings', 'shipping_classes_save_changes', 'toggle_gateway_enabled', ); foreach ( $ajax_events as $ajax_event ) { add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) ); } } /** * Get a refreshed cart fragment, including the mini cart HTML. */ public static function get_refreshed_fragments() { ob_start(); woocommerce_mini_cart(); $mini_cart = ob_get_clean(); $data = array( 'fragments' => apply_filters( 'woocommerce_add_to_cart_fragments', array( 'div.widget_shopping_cart_content' => '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>', ) ), 'cart_hash' => WC()->cart->get_cart_hash(), ); wp_send_json( $data ); } /** * AJAX apply coupon on checkout page. */ public static function apply_coupon() { check_ajax_referer( 'apply-coupon', 'security' ); if ( ! empty( $_POST['coupon_code'] ) ) { WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } else { wc_add_notice( WC_Coupon::get_generic_coupon_error( WC_Coupon::E_WC_COUPON_PLEASE_ENTER ), 'error' ); } wc_print_notices(); wp_die(); } /** * AJAX remove coupon on cart and checkout page. */ public static function remove_coupon() { check_ajax_referer( 'remove-coupon', 'security' ); $coupon = isset( $_POST['coupon'] ) ? wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $coupon ) ) { wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' ); } else { WC()->cart->remove_coupon( $coupon ); wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) ); } wc_print_notices(); wp_die(); } /** * AJAX update shipping method on cart page. */ public static function update_shipping_method() { check_ajax_referer( 'update-shipping-method', 'security' ); wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); $posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array(); if ( is_array( $posted_shipping_methods ) ) { foreach ( $posted_shipping_methods as $i => $value ) { $chosen_shipping_methods[ $i ] = $value; } } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); self::get_cart_totals(); } /** * AJAX receive updated cart_totals div. */ public static function get_cart_totals() { wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); WC()->cart->calculate_totals(); woocommerce_cart_totals(); wp_die(); } /** * Session has expired. */ private static function update_order_review_expired() { wp_send_json( array( 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array( 'form.woocommerce-checkout' => '<div class="woocommerce-error">' . __( 'Sorry, your session has expired.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-backward">' . __( 'Return to shop', 'woocommerce' ) . '</a></div>', ) ), ) ); } /** * AJAX update order review on checkout. */ public static function update_order_review() { check_ajax_referer( 'update-order-review', 'security' ); wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_update_order_review_expired', true ) ) { self::update_order_review_expired(); } do_action( 'woocommerce_checkout_update_order_review', isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); $posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array(); if ( is_array( $posted_shipping_methods ) ) { foreach ( $posted_shipping_methods as $i => $value ) { $chosen_shipping_methods[ $i ] = $value; } } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); WC()->session->set( 'chosen_payment_method', empty( $_POST['payment_method'] ) ? '' : wc_clean( wp_unslash( $_POST['payment_method'] ) ) ); WC()->customer->set_props( array( 'billing_country' => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null, 'billing_state' => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null, 'billing_postcode' => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null, 'billing_city' => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null, 'billing_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null, 'billing_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null, ) ); if ( wc_ship_to_billing_address_only() ) { WC()->customer->set_props( array( 'shipping_country' => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null, 'shipping_state' => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null, 'shipping_postcode' => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null, 'shipping_city' => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null, 'shipping_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null, 'shipping_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null, ) ); } else { WC()->customer->set_props( array( 'shipping_country' => isset( $_POST['s_country'] ) ? wc_clean( wp_unslash( $_POST['s_country'] ) ) : null, 'shipping_state' => isset( $_POST['s_state'] ) ? wc_clean( wp_unslash( $_POST['s_state'] ) ) : null, 'shipping_postcode' => isset( $_POST['s_postcode'] ) ? wc_clean( wp_unslash( $_POST['s_postcode'] ) ) : null, 'shipping_city' => isset( $_POST['s_city'] ) ? wc_clean( wp_unslash( $_POST['s_city'] ) ) : null, 'shipping_address_1' => isset( $_POST['s_address'] ) ? wc_clean( wp_unslash( $_POST['s_address'] ) ) : null, 'shipping_address_2' => isset( $_POST['s_address_2'] ) ? wc_clean( wp_unslash( $_POST['s_address_2'] ) ) : null, ) ); } if ( isset( $_POST['has_full_address'] ) && wc_string_to_bool( wc_clean( wp_unslash( $_POST['has_full_address'] ) ) ) ) { WC()->customer->set_calculated_shipping( true ); } else { WC()->customer->set_calculated_shipping( false ); } WC()->customer->save(); // Calculate shipping before totals. This will ensure any shipping methods that affect things like taxes are chosen prior to final totals being calculated. Ref: #22708. WC()->cart->calculate_shipping(); WC()->cart->calculate_totals(); // Get order review fragment. ob_start(); woocommerce_order_review(); $woocommerce_order_review = ob_get_clean(); // Get checkout payment fragment. ob_start(); woocommerce_checkout_payment(); $woocommerce_checkout_payment = ob_get_clean(); // Get messages if reload checkout is not true. $reload_checkout = isset( WC()->session->reload_checkout ); if ( ! $reload_checkout ) { $messages = wc_print_notices( true ); } else { $messages = ''; } unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); wp_send_json( array( 'result' => empty( $messages ) ? 'success' : 'failure', 'messages' => $messages, 'reload' => $reload_checkout, 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array( '.woocommerce-checkout-review-order-table' => $woocommerce_order_review, '.woocommerce-checkout-payment' => $woocommerce_checkout_payment, ) ), ) ); } /** * AJAX add to cart. */ public static function add_to_cart() { ob_start(); // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['product_id'] ) ) { return; } $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_POST['product_id'] ) ); $product = wc_get_product( $product_id ); $quantity = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_POST['quantity'] ) ); $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); $product_status = get_post_status( $product_id ); $variation_id = 0; $variation = array(); if ( $product && 'variation' === $product->get_type() ) { $variation_id = $product_id; $product_id = $product->get_parent_id(); $variation = $product->get_variation_attributes(); } if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation ) && 'publish' === $product_status ) { do_action( 'woocommerce_ajax_added_to_cart', $product_id ); if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { wc_add_to_cart_message( array( $product_id => $quantity ), true ); } self::get_refreshed_fragments(); } else { // If there was an error adding to the cart, redirect to the product page to show any errors. $data = array( 'error' => true, 'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ), ); wp_send_json( $data ); } // phpcs:enable } /** * AJAX remove from cart. */ public static function remove_from_cart() { ob_start(); // phpcs:ignore WordPress.Security.NonceVerification.Missing $cart_item_key = wc_clean( isset( $_POST['cart_item_key'] ) ? wp_unslash( $_POST['cart_item_key'] ) : '' ); if ( $cart_item_key && false !== WC()->cart->remove_cart_item( $cart_item_key ) ) { self::get_refreshed_fragments(); } else { wp_send_json_error(); } } /** * Process ajax checkout form. */ public static function checkout() { wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); WC()->checkout()->process_checkout(); wp_die( 0 ); } /** * Get a matching variation based on posted attributes. */ public static function get_variation() { ob_start(); // phpcs:disable WordPress.Security.NonceVerification.Missing if ( empty( $_POST['product_id'] ) ) { wp_die(); } $variable_product = wc_get_product( absint( $_POST['product_id'] ) ); if ( ! $variable_product ) { wp_die(); } $data_store = WC_Data_Store::load( 'product' ); $variation_id = $data_store->find_matching_product_variation( $variable_product, wp_unslash( $_POST ) ); $variation = $variation_id ? $variable_product->get_available_variation( $variation_id ) : false; wp_send_json( $variation ); // phpcs:enable } /** * Locate user via AJAX. */ public static function get_customer_location() { $location_hash = WC_Cache_Helper::geolocation_ajax_get_location_hash(); wp_send_json_success( array( 'hash' => $location_hash ) ); } /** * Toggle Featured status of a product from admin. */ public static function feature_product() { if ( current_user_can( 'edit_products' ) && check_admin_referer( 'woocommerce-feature-product' ) && isset( $_GET['product_id'] ) ) { $product = wc_get_product( absint( $_GET['product_id'] ) ); if ( $product ) { $product->set_featured( ! $product->get_featured() ); $product->save(); } } wp_safe_redirect( wp_get_referer() ? remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) : admin_url( 'edit.php?post_type=product' ) ); exit; } /** * Mark an order with a status. */ public static function mark_order_status() { if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'woocommerce-mark-order-status' ) && isset( $_GET['status'], $_GET['order_id'] ) ) { $status = sanitize_text_field( wp_unslash( $_GET['status'] ) ); $order = wc_get_order( absint( wp_unslash( $_GET['order_id'] ) ) ); if ( wc_is_order_status( 'wc-' . $status ) && $order ) { // Initialize payment gateways in case order has hooked status transition actions. WC()->payment_gateways(); $order->update_status( $status, '', true ); do_action( 'woocommerce_order_edit_status', $order->get_id(), $status ); } } wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=shop_order' ) ); exit; } /** * Get order details. */ public static function get_order_details() { check_admin_referer( 'woocommerce-preview-order', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_GET['order_id'] ) ) { wp_die( -1 ); } $order = wc_get_order( absint( $_GET['order_id'] ) ); if ( $order ) { include_once __DIR__ . '/admin/list-tables/class-wc-admin-list-table-orders.php'; wp_send_json_success( WC_Admin_List_Table_Orders::order_preview_get_order_details( $order ) ); } wp_die(); } /** * Add an attribute row. */ public static function add_attribute() { ob_start(); check_ajax_referer( 'add-attribute', 'security' ); if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['taxonomy'], $_POST['i'] ) ) { wp_die( -1 ); } $i = absint( $_POST['i'] ); $metabox_class = array(); $attribute = new WC_Product_Attribute(); $attribute->set_id( wc_attribute_taxonomy_id_by_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) ) ); $attribute->set_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) ); $attribute->set_visible( apply_filters( 'woocommerce_attribute_default_visibility', 1 ) ); $attribute->set_variation( apply_filters( 'woocommerce_attribute_default_is_variation', 0 ) ); if ( $attribute->is_taxonomy() ) { $metabox_class[] = 'taxonomy'; $metabox_class[] = $attribute->get_name(); } include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php'; wp_die(); } /** * Add a new attribute via ajax function. */ public static function add_new_attribute() { check_ajax_referer( 'add-attribute', 'security' ); if ( current_user_can( 'manage_product_terms' ) && isset( $_POST['taxonomy'], $_POST['term'] ) ) { $taxonomy = esc_attr( wp_unslash( $_POST['taxonomy'] ) ); // phpcs:ignore $term = wc_clean( wp_unslash( $_POST['term'] ) ); if ( taxonomy_exists( $taxonomy ) ) { $result = wp_insert_term( $term, $taxonomy ); if ( is_wp_error( $result ) ) { wp_send_json( array( 'error' => $result->get_error_message(), ) ); } else { $term = get_term_by( 'id', $result['term_id'], $taxonomy ); wp_send_json( array( 'term_id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ) ); } } } wp_die( -1 ); } /** * Delete variations via ajax function. */ public static function remove_variations() { check_ajax_referer( 'delete-variations', 'security' ); if ( current_user_can( 'edit_products' ) && isset( $_POST['variation_ids'] ) ) { $variation_ids = array_map( 'absint', (array) wp_unslash( $_POST['variation_ids'] ) ); foreach ( $variation_ids as $variation_id ) { if ( 'product_variation' === get_post_type( $variation_id ) ) { $variation = wc_get_product( $variation_id ); $variation->delete( true ); } } } wp_die( -1 ); } /** * Save attributes via ajax. */ public static function save_attributes() { check_ajax_referer( 'save-attributes', 'security' ); if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['data'], $_POST['post_id'] ) ) { wp_die( -1 ); } $response = array(); try { parse_str( wp_unslash( $_POST['data'] ), $data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $attributes = WC_Meta_Box_Product_Data::prepare_attributes( $data ); $product_id = absint( wp_unslash( $_POST['post_id'] ) ); $product_type = ! empty( $_POST['product_type'] ) ? wc_clean( wp_unslash( $_POST['product_type'] ) ) : 'simple'; $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); $product = new $classname( $product_id ); $product->set_attributes( $attributes ); $product->save(); ob_start(); $attributes = $product->get_attributes( 'edit' ); $i = -1; if ( ! empty( $data['attribute_names'] ) ) { foreach ( $data['attribute_names'] as $attribute_name ) { $attribute = isset( $attributes[ sanitize_title( $attribute_name ) ] ) ? $attributes[ sanitize_title( $attribute_name ) ] : false; if ( ! $attribute ) { continue; } $i++; $metabox_class = array(); if ( $attribute->is_taxonomy() ) { $metabox_class[] = 'taxonomy'; $metabox_class[] = $attribute->get_name(); } include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php'; } } $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Add variation via ajax function. */ public static function add_variation() { check_ajax_referer( 'add-variation', 'security' ); if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['post_id'], $_POST['loop'] ) ) { wp_die( -1 ); } global $post; // Set $post global so its available, like within the admin screens. $product_id = intval( $_POST['post_id'] ); $post = get_post( $product_id ); // phpcs:ignore $loop = intval( $_POST['loop'] ); $product_object = wc_get_product_object( 'variable', $product_id ); // Forces type to variable in case product is unsaved. $variation_object = wc_get_product_object( 'variation' ); $variation_object->set_parent_id( $product_id ); $variation_object->set_attributes( array_fill_keys( array_map( 'sanitize_title', array_keys( $product_object->get_variation_attributes() ) ), '' ) ); $variation_id = $variation_object->save(); $variation = get_post( $variation_id ); $variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility. include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php'; wp_die(); } /** * Link all variations via ajax function. */ public static function link_all_variations() { check_ajax_referer( 'link-variations', 'security' ); if ( ! current_user_can( 'edit_products' ) ) { wp_die( -1 ); } wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 50 ); wc_set_time_limit( 0 ); $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; if ( ! $post_id ) { wp_die(); } $product = wc_get_product( $post_id ); $data_store = $product->get_data_store(); if ( ! is_callable( array( $data_store, 'create_all_product_variations' ) ) ) { wp_die(); } echo esc_html( $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) ) ); $data_store->sort_all_product_variations( $product->get_id() ); wp_die(); } /** * Delete download permissions via ajax function. */ public static function revoke_access_to_download() { check_ajax_referer( 'revoke-access', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['download_id'], $_POST['product_id'], $_POST['order_id'], $_POST['permission_id'] ) ) { wp_die( -1 ); } $download_id = wc_clean( wp_unslash( $_POST['download_id'] ) ); $product_id = intval( $_POST['product_id'] ); $order_id = intval( $_POST['order_id'] ); $permission_id = absint( $_POST['permission_id'] ); $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->delete_by_id( $permission_id ); do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id, $permission_id ); wp_die(); } /** * Grant download permissions via ajax function. */ public static function grant_access_to_download() { check_ajax_referer( 'grant-access', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['loop'], $_POST['order_id'], $_POST['product_ids'] ) ) { wp_die( -1 ); } global $wpdb; $wpdb->hide_errors(); $order_id = intval( $_POST['order_id'] ); $product_ids = array_filter( array_map( 'absint', (array) wp_unslash( $_POST['product_ids'] ) ) ); $loop = intval( $_POST['loop'] ); $file_counter = 0; $order = wc_get_order( $order_id ); if ( ! $order->get_billing_email() ) { wp_die(); } $data = array(); $items = $order->get_items(); // Check against order items first. foreach ( $items as $item ) { $product = $item->get_product(); if ( $product && $product->exists() && in_array( $product->get_id(), $product_ids, true ) && $product->is_downloadable() ) { $data[ $product->get_id() ] = array( 'files' => $product->get_downloads(), 'quantity' => $item->get_quantity(), 'order_item' => $item, ); } } foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( isset( $data[ $product->get_id() ] ) ) { $download_data = $data[ $product->get_id() ]; } else { $download_data = array( 'files' => $product->get_downloads(), 'quantity' => 1, 'order_item' => null, ); } if ( ! empty( $download_data['files'] ) ) { foreach ( $download_data['files'] as $download_id => $file ) { $inserted_id = wc_downloadable_file_permission( $download_id, $product->get_id(), $order, $download_data['quantity'], $download_data['order_item'] ); if ( $inserted_id ) { $download = new WC_Customer_Download( $inserted_id ); $loop ++; $file_counter ++; if ( $file->get_name() ) { $file_count = $file->get_name(); } else { /* translators: %d file count */ $file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter ); } include __DIR__ . '/admin/meta-boxes/views/html-order-download-permission.php'; } } } } wp_die(); } /** * Get customer details via ajax. */ public static function get_customer_details() { check_ajax_referer( 'get-customer-details', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['user_id'] ) ) { wp_die( -1 ); } $user_id = absint( $_POST['user_id'] ); $customer = new WC_Customer( $user_id ); if ( has_filter( 'woocommerce_found_customer_details' ) ) { wc_deprecated_function( 'The woocommerce_found_customer_details filter', '3.0', 'woocommerce_ajax_get_customer_details' ); } $data = $customer->get_data(); $data['date_created'] = $data['date_created'] ? $data['date_created']->getTimestamp() : null; $data['date_modified'] = $data['date_modified'] ? $data['date_modified']->getTimestamp() : null; $customer_data = apply_filters( 'woocommerce_ajax_get_customer_details', $data, $customer, $user_id ); wp_send_json( $customer_data ); } /** * Add order item via ajax. Used on the edit order screen in WP Admin. * * @throws Exception If order is invalid. */ public static function add_order_item() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } if ( ! isset( $_POST['order_id'] ) ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } $order_id = absint( wp_unslash( $_POST['order_id'] ) ); // If we passed through items it means we need to save first before adding a new one. $items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized try { $response = self::maybe_add_order_item( $order_id, $items, $items_to_add ); wp_send_json_success( $response ); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } } /** * Add order item via AJAX. This is refactored for better unit testing. * * @param int $order_id ID of order to add items to. * @param string|array $items Existing items in order. Empty string if no items to add. * @param array $items_to_add Array of items to add. * * @return array Fragments to render and notes HTML. * @throws Exception When unable to add item. */ private static function maybe_add_order_item( $order_id, $items, $items_to_add ) { try { $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } if ( ! empty( $items ) ) { $save_items = array(); parse_str( $items, $save_items ); wc_save_order_items( $order->get_id(), $save_items ); } // Add items to order. $order_notes = array(); $added_items = array(); foreach ( $items_to_add as $item ) { if ( ! isset( $item['id'], $item['qty'] ) || empty( $item['id'] ) ) { continue; } $product_id = absint( $item['id'] ); $qty = wc_stock_amount( $item['qty'] ); $product = wc_get_product( $product_id ); if ( ! $product ) { throw new Exception( __( 'Invalid product ID', 'woocommerce' ) . ' ' . $product_id ); } if ( 'variable' === $product->get_type() ) { /* translators: %s product name */ throw new Exception( sprintf( __( '%s is a variable product parent and cannot be added.', 'woocommerce' ), $product->get_name() ) ); } $validation_error = new WP_Error(); $validation_error = apply_filters( 'woocommerce_ajax_add_order_item_validation', $validation_error, $product, $order, $qty ); if ( $validation_error->get_error_code() ) { /* translators: %s: error message */ throw new Exception( sprintf( __( 'Error: %s', 'woocommerce' ), $validation_error->get_error_message() ) ); } $item_id = $order->add_product( $product, $qty, array( 'order' => $order ) ); $item = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id, $order, $product ); $added_items[ $item_id ] = $item; $order_notes[ $item_id ] = $product->get_formatted_name(); // We do not perform any stock operations here because they will be handled when order is moved to a status where stock operations are applied (like processing, completed etc). do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item, $order ); } /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Added line items: %s', 'woocommerce' ), implode( ', ', $order_notes ) ), false, true ); do_action( 'woocommerce_ajax_order_items_added', $added_items, $order ); $data = get_post_meta( $order_id ); // Get HTML to return. ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $items_html = ob_get_clean(); ob_start(); $notes = wc_get_order_notes( array( 'order_id' => $order_id ) ); include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php'; $notes_html = ob_get_clean(); return array( 'html' => $items_html, 'notes_html' => $notes_html, ); } catch ( Exception $e ) { throw $e; // Forward exception to caller. } } /** * Add order fee via ajax. * * @throws Exception If order is invalid. */ public static function add_order_fee() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $response = array(); try { $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } $amount = isset( $_POST['amount'] ) ? wc_clean( wp_unslash( $_POST['amount'] ) ) : 0; $calculate_tax_args = array( 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '', 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '', 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '', 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '', ); if ( strstr( $amount, '%' ) ) { // We need to calculate totals first, so that $order->get_total() is correct. $order->calculate_totals( false ); $formatted_amount = $amount; $percent = floatval( trim( $amount, '%' ) ); $amount = $order->get_total() * ( $percent / 100 ); } else { $amount = floatval( $amount ); $formatted_amount = wc_price( $amount, array( 'currency' => $order->get_currency() ) ); } $fee = new WC_Order_Item_Fee(); $fee->set_amount( $amount ); $fee->set_total( $amount ); /* translators: %s fee amount */ $fee->set_name( sprintf( __( '%s fee', 'woocommerce' ), wc_clean( $formatted_amount ) ) ); $order->add_item( $fee ); $order->calculate_taxes( $calculate_tax_args ); $order->calculate_totals( false ); $order->save(); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Add order shipping cost via ajax. * * @throws Exception If order is invalid. */ public static function add_order_shipping() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $response = array(); try { $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } $order_taxes = $order->get_taxes(); $shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array(); // Add new shipping. $item = new WC_Order_Item_Shipping(); $item->set_shipping_rate( new WC_Shipping_Rate() ); $item->set_order_id( $order_id ); $item_id = $item->save(); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-shipping.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Add order tax column via ajax. * * @throws Exception If order or tax rate is invalid. */ public static function add_order_tax() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $response = array(); try { $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } $rate_id = isset( $_POST['rate_id'] ) ? absint( $_POST['rate_id'] ) : ''; if ( ! $rate_id ) { throw new Exception( __( 'Invalid rate', 'woocommerce' ) ); } $data = get_post_meta( $order_id ); // Add new tax. $item = new WC_Order_Item_Tax(); $item->set_rate( $rate_id ); $item->set_order_id( $order_id ); $item->save(); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Add order discount via ajax. * * @throws Exception If order or coupon is invalid. */ public static function add_coupon_discount() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $response = array(); try { $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $order = wc_get_order( $order_id ); $calculate_tax_args = array( 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '', 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '', 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '', 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '', ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } if ( empty( $_POST['coupon'] ) ) { throw new Exception( __( 'Invalid coupon', 'woocommerce' ) ); } // Add user ID and/or email so validation for coupon limits works. $user_id_arg = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; $user_email_arg = isset( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : ''; if ( $user_id_arg ) { $order->set_customer_id( $user_id_arg ); } if ( $user_email_arg ) { $order->set_billing_email( $user_email_arg ); } $result = $order->apply_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( is_wp_error( $result ) ) { throw new Exception( html_entity_decode( wp_strip_all_tags( $result->get_error_message() ) ) ); } $order->calculate_taxes( $calculate_tax_args ); $order->calculate_totals( false ); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Remove coupon from an order via ajax. * * @throws Exception If order or coupon is invalid. */ public static function remove_order_coupon() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $response = array(); try { $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $order = wc_get_order( $order_id ); $calculate_tax_args = array( 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '', 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '', 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '', 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '', ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } if ( empty( $_POST['coupon'] ) ) { throw new Exception( __( 'Invalid coupon', 'woocommerce' ) ); } $order->remove_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $order->calculate_taxes( $calculate_tax_args ); $order->calculate_totals( false ); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Remove an order item. * * @throws Exception If order is invalid. */ public static function remove_order_item() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['order_item_ids'] ) ) { wp_die( -1 ); } $response = array(); try { $order_id = absint( $_POST['order_id'] ); $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } if ( ! isset( $_POST['order_item_ids'] ) ) { throw new Exception( __( 'Invalid items', 'woocommerce' ) ); } $order_item_ids = wp_unslash( $_POST['order_item_ids'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $calculate_tax_args = array( 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '', 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '', 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '', 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '', ); if ( ! is_array( $order_item_ids ) && is_numeric( $order_item_ids ) ) { $order_item_ids = array( $order_item_ids ); } // If we passed through items it means we need to save first before deleting. if ( ! empty( $items ) ) { $save_items = array(); parse_str( $items, $save_items ); wc_save_order_items( $order->get_id(), $save_items ); } if ( ! empty( $order_item_ids ) ) { foreach ( $order_item_ids as $item_id ) { $item_id = absint( $item_id ); $item = $order->get_item( $item_id ); // Before deleting the item, adjust any stock values already reduced. if ( $item->is_type( 'line_item' ) ) { $changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 ); if ( $changed_stock && ! is_wp_error( $changed_stock ) ) { /* translators: %1$s: item name %2$s: stock change */ $order->add_order_note( sprintf( __( 'Deleted %1$s and adjusted stock (%2$s)', 'woocommerce' ), $item->get_name(), $changed_stock['from'] . '→' . $changed_stock['to'] ), false, true ); } else { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Deleted %s', 'woocommerce' ), $item->get_name() ), false, true ); } } wc_delete_order_item( $item_id ); } } $order = wc_get_order( $order_id ); $order->calculate_taxes( $calculate_tax_args ); $order->calculate_totals( false ); /** * Fires after order items are removed. * * @since 5.2.0 * * @param int $item_id WC item ID. * @param WC_Order_Item|false $item As returned by $order->get_item( $item_id ). * @param bool|array|WP_Error $changed_store Result of wc_maybe_adjust_line_item_product_stock(). * @param bool|WC_Order|WC_Order_Refund $order As returned by wc_get_order(). */ do_action( 'woocommerce_ajax_order_items_removed', $item_id, $item, $changed_stock, $order ); // Get HTML to return. ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $items_html = ob_get_clean(); ob_start(); $notes = wc_get_order_notes( array( 'order_id' => $order_id ) ); include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php'; $notes_html = ob_get_clean(); wp_send_json_success( array( 'html' => $items_html, 'notes_html' => $notes_html, ) ); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Remove an order tax. * * @throws Exception If there is an error whilst deleting the rate. */ public static function remove_order_tax() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['rate_id'] ) ) { wp_die( -1 ); } $response = array(); try { $order_id = absint( $_POST['order_id'] ); $rate_id = absint( $_POST['rate_id'] ); $order = wc_get_order( $order_id ); if ( ! $order->is_editable() ) { throw new Exception( __( 'Order not editable', 'woocommerce' ) ); } wc_delete_order_item( $rate_id ); // Need to load order again after deleting to have latest items before calculating. $order = wc_get_order( $order_id ); $order->calculate_totals( false ); ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $response['html'] = ob_get_clean(); } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Calc line tax. */ public static function calc_line_taxes() { check_ajax_referer( 'calc-totals', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) { wp_die( -1 ); } $order_id = absint( $_POST['order_id'] ); $calculate_tax_args = array( 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '', 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '', 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '', 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '', ); // Parse the jQuery serialized items. $items = array(); parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Save order items first. wc_save_order_items( $order_id, $items ); // Grab the order and recalculate taxes. $order = wc_get_order( $order_id ); $order->calculate_taxes( $calculate_tax_args ); $order->calculate_totals( false ); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; wp_die(); } /** * Save order items via ajax. */ public static function save_order_items() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) { wp_die( -1 ); } if ( isset( $_POST['order_id'], $_POST['items'] ) ) { $order_id = absint( $_POST['order_id'] ); // Parse the jQuery serialized items. $items = array(); parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Save order items. wc_save_order_items( $order_id, $items ); // Return HTML items. $order = wc_get_order( $order_id ); // Get HTML to return. ob_start(); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; $items_html = ob_get_clean(); ob_start(); $notes = wc_get_order_notes( array( 'order_id' => $order_id ) ); include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php'; $notes_html = ob_get_clean(); wp_send_json_success( array( 'html' => $items_html, 'notes_html' => $notes_html, ) ); } wp_die(); } /** * Load order items via ajax. */ public static function load_order_items() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'] ) ) { wp_die( -1 ); } // Return HTML items. $order_id = absint( $_POST['order_id'] ); $order = wc_get_order( $order_id ); include __DIR__ . '/admin/meta-boxes/views/html-order-items.php'; wp_die(); } /** * Add order note via ajax. */ public static function add_order_note() { check_ajax_referer( 'add-order-note', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['post_id'], $_POST['note'], $_POST['note_type'] ) ) { wp_die( -1 ); } $post_id = absint( $_POST['post_id'] ); $note = wp_kses_post( trim( wp_unslash( $_POST['note'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $note_type = wc_clean( wp_unslash( $_POST['note_type'] ) ); $is_customer_note = ( 'customer' === $note_type ) ? 1 : 0; if ( $post_id > 0 ) { $order = wc_get_order( $post_id ); $comment_id = $order->add_order_note( $note, $is_customer_note, true ); $note = wc_get_order_note( $comment_id ); $note_classes = array( 'note' ); $note_classes[] = $is_customer_note ? 'customer-note' : ''; $note_classes = apply_filters( 'woocommerce_order_note_class', array_filter( $note_classes ), $note ); ?> <li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $note_classes ) ); ?>"> <div class="note_content"> <?php echo wp_kses_post( wpautop( wptexturize( make_clickable( $note->content ) ) ) ); ?> </div> <p class="meta"> <abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'y-m-d h:i:s' ) ); ?>"> <?php /* translators: $1: Date created, $2 Time created */ printf( esc_html__( 'added on %1$s at %2$s', 'woocommerce' ), esc_html( $note->date_created->date_i18n( wc_date_format() ) ), esc_html( $note->date_created->date_i18n( wc_time_format() ) ) ); ?> </abbr> <?php if ( 'system' !== $note->added_by ) : /* translators: %s: note author */ printf( ' ' . esc_html__( 'by %s', 'woocommerce' ), esc_html( $note->added_by ) ); endif; ?> <a href="#" class="delete_note" role="button"><?php esc_html_e( 'Delete note', 'woocommerce' ); ?></a> </p> </li> <?php } wp_die(); } /** * Delete order note via ajax. */ public static function delete_order_note() { check_ajax_referer( 'delete-order-note', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['note_id'] ) ) { wp_die( -1 ); } $note_id = (int) $_POST['note_id']; if ( $note_id > 0 ) { wc_delete_order_note( $note_id ); } wp_die(); } /** * Search for products and echo json. * * @param string $term (default: '') Term to search for. * @param bool $include_variations in search or not. */ public static function json_search_products( $term = '', $include_variations = false ) { check_ajax_referer( 'search-products', 'security' ); if ( empty( $term ) && isset( $_GET['term'] ) ) { $term = (string) wc_clean( wp_unslash( $_GET['term'] ) ); } if ( empty( $term ) ) { wp_die(); } if ( ! empty( $_GET['limit'] ) ) { $limit = absint( $_GET['limit'] ); } else { $limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) ); } $include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array(); $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array(); $exclude_types = array(); if ( ! empty( $_GET['exclude_type'] ) ) { // Support both comma-delimited and array format inputs. $exclude_types = wp_unslash( $_GET['exclude_type'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! is_array( $exclude_types ) ) { $exclude_types = explode( ',', $exclude_types ); } // Sanitize the excluded types against valid product types. foreach ( $exclude_types as &$exclude_type ) { $exclude_type = strtolower( trim( $exclude_type ) ); } $exclude_types = array_intersect( array_merge( array( 'variation' ), array_keys( wc_get_product_types() ) ), $exclude_types ); } $data_store = WC_Data_Store::load( 'product' ); $ids = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids ); $products = array(); foreach ( $ids as $id ) { $product_object = wc_get_product( $id ); if ( ! wc_products_array_filter_readable( $product_object ) ) { continue; } $formatted_name = $product_object->get_formatted_name(); $managing_stock = $product_object->managing_stock(); if ( in_array( $product_object->get_type(), $exclude_types, true ) ) { continue; } if ( $managing_stock && ! empty( $_GET['display_stock'] ) ) { $stock_amount = $product_object->get_stock_quantity(); /* Translators: %d stock amount */ $formatted_name .= ' – ' . sprintf( __( 'Stock: %d', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product_object ) ); } $products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $formatted_name ) ); } wp_send_json( apply_filters( 'woocommerce_json_search_found_products', $products ) ); } /** * Search for product variations and return json. * * @see WC_AJAX::json_search_products() */ public static function json_search_products_and_variations() { self::json_search_products( '', true ); } /** * Search for downloadable product variations and return json. * * @see WC_AJAX::json_search_products() */ public static function json_search_downloadable_products_and_variations() { check_ajax_referer( 'search-products', 'security' ); if ( ! empty( $_GET['limit'] ) ) { $limit = absint( $_GET['limit'] ); } else { $limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) ); } $include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array(); $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array(); $term = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : ''; $data_store = WC_Data_Store::load( 'product' ); $ids = $data_store->search_products( $term, 'downloadable', true, false, $limit ); $product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' ); $products = array(); foreach ( $product_objects as $product_object ) { $products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $product_object->get_formatted_name() ) ); } wp_send_json( $products ); } /** * Search for customers and return json. */ public static function json_search_customers() { ob_start(); check_ajax_referer( 'search-customers', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $term = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : ''; $limit = 0; if ( empty( $term ) ) { wp_die(); } $ids = array(); // Search by ID. if ( is_numeric( $term ) ) { $customer = new WC_Customer( intval( $term ) ); // Customer does not exists. if ( 0 !== $customer->get_id() ) { $ids = array( $customer->get_id() ); } } // Usernames can be numeric so we first check that no users was found by ID before searching for numeric username, this prevents performance issues with ID lookups. if ( empty( $ids ) ) { $data_store = WC_Data_Store::load( 'customer' ); // If search is smaller than 3 characters, limit result set to avoid // too many rows being returned. if ( 3 > strlen( $term ) ) { $limit = 20; } $ids = $data_store->search_customers( $term, $limit ); } $found_customers = array(); if ( ! empty( $_GET['exclude'] ) ) { $ids = array_diff( $ids, array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) ); } foreach ( $ids as $id ) { $customer = new WC_Customer( $id ); /* translators: 1: user display name 2: user ID 3: user email */ $found_customers[ $id ] = sprintf( /* translators: $1: customer name, $2 customer id, $3: customer email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), $customer->get_first_name() . ' ' . $customer->get_last_name(), $customer->get_id(), $customer->get_email() ); } wp_send_json( apply_filters( 'woocommerce_json_search_found_customers', $found_customers ) ); } /** * Search for categories and return json. */ public static function json_search_categories() { ob_start(); check_ajax_referer( 'search-categories', 'security' ); if ( ! current_user_can( 'edit_products' ) ) { wp_die( -1 ); } $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : ''; if ( ! $search_text ) { wp_die(); } $found_categories = array(); $args = array( 'taxonomy' => array( 'product_cat' ), 'orderby' => 'id', 'order' => 'ASC', 'hide_empty' => true, 'fields' => 'all', 'name__like' => $search_text, ); $terms = get_terms( $args ); if ( $terms ) { foreach ( $terms as $term ) { $term->formatted_name = ''; if ( $term->parent ) { $ancestors = array_reverse( get_ancestors( $term->term_id, 'product_cat' ) ); foreach ( $ancestors as $ancestor ) { $ancestor_term = get_term( $ancestor, 'product_cat' ); if ( $ancestor_term ) { $term->formatted_name .= $ancestor_term->name . ' > '; } } } $term->formatted_name .= $term->name . ' (' . $term->count . ')'; $found_categories[ $term->term_id ] = $term; } } wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) ); } /** * Ajax request handling for page searching. */ public static function json_search_pages() { ob_start(); check_ajax_referer( 'search-pages', 'security' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( -1 ); } $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : ''; $limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : -1; $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array(); $args = array( 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'posts_per_page' => $limit, 'post_type' => 'page', 'post_status' => array( 'publish', 'private', 'draft' ), 's' => $search_text, 'post__not_in' => $exclude_ids, ); $search_results_query = new WP_Query( $args ); $pages_results = array(); foreach ( $search_results_query->get_posts() as $post ) { $pages_results[ $post->ID ] = sprintf( /* translators: 1: page name 2: page ID */ __( '%1$s (ID: %2$s)', 'woocommerce' ), get_the_title( $post ), $post->ID ); } wp_send_json( apply_filters( 'woocommerce_json_search_found_pages', $pages_results ) ); } /** * Ajax request handling for categories ordering. */ public static function term_ordering() { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) { wp_die( -1 ); } $id = (int) $_POST['id']; $next_id = isset( $_POST['nextid'] ) && (int) $_POST['nextid'] ? (int) $_POST['nextid'] : null; $taxonomy = isset( $_POST['thetaxonomy'] ) ? esc_attr( wp_unslash( $_POST['thetaxonomy'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $term = get_term_by( 'id', $id, $taxonomy ); if ( ! $id || ! $term || ! $taxonomy ) { wp_die( 0 ); } wc_reorder_terms( $term, $next_id, $taxonomy ); $children = get_terms( $taxonomy, "child_of=$id&menu_order=ASC&hide_empty=0" ); if ( $term && count( $children ) ) { echo 'children'; wp_die(); } // phpcs:enable } /** * Ajax request handling for product ordering. * * Based on Simple Page Ordering by 10up (https://wordpress.org/plugins/simple-page-ordering/). */ public static function product_ordering() { global $wpdb; // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) { wp_die( -1 ); } $sorting_id = absint( $_POST['id'] ); $previd = absint( isset( $_POST['previd'] ) ? $_POST['previd'] : 0 ); $nextid = absint( isset( $_POST['nextid'] ) ? $_POST['nextid'] : 0 ); $menu_orders = wp_list_pluck( $wpdb->get_results( "SELECT ID, menu_order FROM {$wpdb->posts} WHERE post_type = 'product' ORDER BY menu_order ASC, post_title ASC" ), 'menu_order', 'ID' ); $index = 0; foreach ( $menu_orders as $id => $menu_order ) { $id = absint( $id ); if ( $sorting_id === $id ) { continue; } if ( $nextid === $id ) { $index ++; } $index ++; $menu_orders[ $id ] = $index; $wpdb->update( $wpdb->posts, array( 'menu_order' => $index ), array( 'ID' => $id ) ); /** * When a single product has gotten it's ordering updated. * $id The product ID * $index The new menu order */ do_action( 'woocommerce_after_single_product_ordering', $id, $index ); } if ( isset( $menu_orders[ $previd ] ) ) { $menu_orders[ $sorting_id ] = $menu_orders[ $previd ] + 1; } elseif ( isset( $menu_orders[ $nextid ] ) ) { $menu_orders[ $sorting_id ] = $menu_orders[ $nextid ] - 1; } else { $menu_orders[ $sorting_id ] = 0; } $wpdb->update( $wpdb->posts, array( 'menu_order' => $menu_orders[ $sorting_id ] ), array( 'ID' => $sorting_id ) ); WC_Post_Data::delete_product_query_transients(); do_action( 'woocommerce_after_product_ordering', $sorting_id, $menu_orders ); wp_send_json( $menu_orders ); // phpcs:enable } /** * Handle a refund via the edit order screen. * * @throws Exception To return errors. */ public static function refund_line_items() { ob_start(); check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( -1 ); } $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $refund_amount = isset( $_POST['refund_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refund_amount'] ) ), wc_get_price_decimals() ) : 0; $refunded_amount = isset( $_POST['refunded_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refunded_amount'] ) ), wc_get_price_decimals() ) : 0; $refund_reason = isset( $_POST['refund_reason'] ) ? sanitize_text_field( wp_unslash( $_POST['refund_reason'] ) ) : ''; $line_item_qtys = isset( $_POST['line_item_qtys'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_qtys'] ) ), true ) : array(); $line_item_totals = isset( $_POST['line_item_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_totals'] ) ), true ) : array(); $line_item_tax_totals = isset( $_POST['line_item_tax_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_tax_totals'] ) ), true ) : array(); $api_refund = isset( $_POST['api_refund'] ) && 'true' === $_POST['api_refund']; $restock_refunded_items = isset( $_POST['restock_refunded_items'] ) && 'true' === $_POST['restock_refunded_items']; $refund = false; $response = array(); try { $order = wc_get_order( $order_id ); $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded(), wc_get_price_decimals() ); if ( ( ! $refund_amount && ( wc_format_decimal( 0, wc_get_price_decimals() ) !== $refund_amount ) ) || $max_refund < $refund_amount || 0 > $refund_amount ) { throw new Exception( __( 'Invalid refund amount', 'woocommerce' ) ); } if ( wc_format_decimal( $order->get_total_refunded(), wc_get_price_decimals() ) !== $refunded_amount ) { throw new Exception( __( 'Error processing refund. Please try again.', 'woocommerce' ) ); } // Prepare line items which we are refunding. $line_items = array(); $item_ids = array_unique( array_merge( array_keys( $line_item_qtys ), array_keys( $line_item_totals ) ) ); foreach ( $item_ids as $item_id ) { $line_items[ $item_id ] = array( 'qty' => 0, 'refund_total' => 0, 'refund_tax' => array(), ); } foreach ( $line_item_qtys as $item_id => $qty ) { $line_items[ $item_id ]['qty'] = max( $qty, 0 ); } foreach ( $line_item_totals as $item_id => $total ) { $line_items[ $item_id ]['refund_total'] = wc_format_decimal( $total ); } foreach ( $line_item_tax_totals as $item_id => $tax_totals ) { $line_items[ $item_id ]['refund_tax'] = array_filter( array_map( 'wc_format_decimal', $tax_totals ) ); } // Create the refund object. $refund = wc_create_refund( array( 'amount' => $refund_amount, 'reason' => $refund_reason, 'order_id' => $order_id, 'line_items' => $line_items, 'refund_payment' => $api_refund, 'restock_items' => $restock_refunded_items, ) ); if ( is_wp_error( $refund ) ) { throw new Exception( $refund->get_error_message() ); } if ( did_action( 'woocommerce_order_fully_refunded' ) ) { $response['status'] = 'fully_refunded'; } } catch ( Exception $e ) { wp_send_json_error( array( 'error' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Delete a refund. */ public static function delete_refund() { check_ajax_referer( 'order-item', 'security' ); if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['refund_id'] ) ) { wp_die( -1 ); } $refund_ids = array_map( 'absint', is_array( $_POST['refund_id'] ) ? wp_unslash( $_POST['refund_id'] ) : array( wp_unslash( $_POST['refund_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( $refund_ids as $refund_id ) { if ( $refund_id && 'shop_order_refund' === get_post_type( $refund_id ) ) { $refund = wc_get_order( $refund_id ); $order_id = $refund->get_parent_id(); $refund->delete( true ); do_action( 'woocommerce_refund_deleted', $refund_id, $order_id ); } } wp_die(); } /** * Triggered when clicking the rating footer. */ public static function rated() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( -1 ); } update_option( 'woocommerce_admin_footer_text_rated', 1 ); wp_die(); } /** * Create/Update API key. * * @throws Exception On invalid or empty description, user, or permissions. */ public static function update_api_key() { ob_start(); global $wpdb; check_ajax_referer( 'update-api-key', 'security' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( -1 ); } $response = array(); try { if ( empty( $_POST['description'] ) ) { throw new Exception( __( 'Description is missing.', 'woocommerce' ) ); } if ( empty( $_POST['user'] ) ) { throw new Exception( __( 'User is missing.', 'woocommerce' ) ); } if ( empty( $_POST['permissions'] ) ) { throw new Exception( __( 'Permissions is missing.', 'woocommerce' ) ); } $key_id = isset( $_POST['key_id'] ) ? absint( $_POST['key_id'] ) : 0; $description = sanitize_text_field( wp_unslash( $_POST['description'] ) ); $permissions = ( in_array( wp_unslash( $_POST['permissions'] ), array( 'read', 'write', 'read_write' ), true ) ) ? sanitize_text_field( wp_unslash( $_POST['permissions'] ) ) : 'read'; $user_id = absint( $_POST['user'] ); // Check if current user can edit other users. if ( $user_id && ! current_user_can( 'edit_user', $user_id ) ) { if ( get_current_user_id() !== $user_id ) { throw new Exception( __( 'You do not have permission to assign API Keys to the selected user.', 'woocommerce' ) ); } } if ( 0 < $key_id ) { $data = array( 'user_id' => $user_id, 'description' => $description, 'permissions' => $permissions, ); $wpdb->update( $wpdb->prefix . 'woocommerce_api_keys', $data, array( 'key_id' => $key_id ), array( '%d', '%s', '%s', ), array( '%d' ) ); $response = $data; $response['consumer_key'] = ''; $response['consumer_secret'] = ''; $response['message'] = __( 'API Key updated successfully.', 'woocommerce' ); } else { $consumer_key = 'ck_' . wc_rand_hash(); $consumer_secret = 'cs_' . wc_rand_hash(); $data = array( 'user_id' => $user_id, 'description' => $description, 'permissions' => $permissions, 'consumer_key' => wc_api_hash( $consumer_key ), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr( $consumer_key, -7 ), ); $wpdb->insert( $wpdb->prefix . 'woocommerce_api_keys', $data, array( '%d', '%s', '%s', '%s', '%s', '%s', ) ); $key_id = $wpdb->insert_id; $response = $data; $response['consumer_key'] = $consumer_key; $response['consumer_secret'] = $consumer_secret; $response['message'] = __( 'API Key generated successfully. Make sure to copy your new keys now as the secret key will be hidden once you leave this page.', 'woocommerce' ); $response['revoke_url'] = '<a style="color: #a00; text-decoration: none;" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key_id ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ), 'revoke' ) ) . '">' . __( 'Revoke key', 'woocommerce' ) . '</a>'; } } catch ( Exception $e ) { wp_send_json_error( array( 'message' => $e->getMessage() ) ); } // wp_send_json_success must be outside the try block not to break phpunit tests. wp_send_json_success( $response ); } /** * Load variations via AJAX. */ public static function load_variations() { ob_start(); check_ajax_referer( 'load-variations', 'security' ); if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) ) { wp_die( -1 ); } // Set $post global so its available, like within the admin screens. global $post; $loop = 0; $product_id = absint( $_POST['product_id'] ); $post = get_post( $product_id ); // phpcs:ignore $product_object = wc_get_product( $product_id ); $per_page = ! empty( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10; $page = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; $variations = wc_get_products( array( 'status' => array( 'private', 'publish' ), 'type' => 'variation', 'parent' => $product_id, 'limit' => $per_page, 'page' => $page, 'orderby' => array( 'menu_order' => 'ASC', 'ID' => 'DESC', ), 'return' => 'objects', ) ); if ( $variations ) { wc_render_invalid_variation_notice( $product_object ); foreach ( $variations as $variation_object ) { $variation_id = $variation_object->get_id(); $variation = get_post( $variation_id ); $variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility. include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php'; $loop++; } } wp_die(); } /** * Save variations via AJAX. */ public static function save_variations() { ob_start(); check_ajax_referer( 'save-variations', 'security' ); // Check permissions again and make sure we have what we need. if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) { wp_die( -1 ); } $product_id = absint( $_POST['product_id'] ); WC_Admin_Meta_Boxes::$meta_box_errors = array(); WC_Meta_Box_Product_Data::save_variations( $product_id, get_post( $product_id ) ); do_action( 'woocommerce_ajax_save_product_variations', $product_id ); $errors = WC_Admin_Meta_Boxes::$meta_box_errors; if ( $errors ) { echo '<div class="error notice is-dismissible">'; foreach ( $errors as $error ) { echo '<p>' . wp_kses_post( $error ) . '</p>'; } echo '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'woocommerce' ) . '</span></button>'; echo '</div>'; delete_option( 'woocommerce_meta_box_errors' ); } wp_die(); } /** * Bulk action - Toggle Enabled. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_toggle_enabled( $variations, $data ) { foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); $variation->set_status( 'private' === $variation->get_status( 'edit' ) ? 'publish' : 'private' ); $variation->save(); } } /** * Bulk action - Toggle Downloadable Checkbox. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_toggle_downloadable( $variations, $data ) { self::variation_bulk_toggle( $variations, 'downloadable' ); } /** * Bulk action - Toggle Virtual Checkbox. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_toggle_virtual( $variations, $data ) { self::variation_bulk_toggle( $variations, 'virtual' ); } /** * Bulk action - Toggle Manage Stock Checkbox. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_toggle_manage_stock( $variations, $data ) { self::variation_bulk_toggle( $variations, 'manage_stock' ); } /** * Bulk action - Set Regular Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_regular_price( $variations, $data ) { self::variation_bulk_set( $variations, 'regular_price', $data['value'] ); } /** * Bulk action - Set Sale Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_sale_price( $variations, $data ) { self::variation_bulk_set( $variations, 'sale_price', $data['value'] ); } /** * Bulk action - Set Stock Status as In Stock. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_stock_status_instock( $variations, $data ) { self::variation_bulk_set( $variations, 'stock_status', 'instock' ); } /** * Bulk action - Set Stock Status as Out of Stock. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_stock_status_outofstock( $variations, $data ) { self::variation_bulk_set( $variations, 'stock_status', 'outofstock' ); } /** * Bulk action - Set Stock Status as On Backorder. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_stock_status_onbackorder( $variations, $data ) { self::variation_bulk_set( $variations, 'stock_status', 'onbackorder' ); } /** * Bulk action - Set Stock. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_stock( $variations, $data ) { if ( ! isset( $data['value'] ) ) { return; } $quantity = wc_stock_amount( wc_clean( $data['value'] ) ); foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation->managing_stock() ) { $variation->set_stock_quantity( $quantity ); } else { $variation->set_stock_quantity( null ); } $variation->save(); } } /** * Bulk action - Set Low Stock Amount. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_low_stock_amount( $variations, $data ) { if ( ! isset( $data['value'] ) ) { return; } $low_stock_amount = wc_stock_amount( wc_clean( $data['value'] ) ); foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation->managing_stock() ) { $variation->set_low_stock_amount( $low_stock_amount ); } else { $variation->set_low_stock_amount( '' ); } $variation->save(); } } /** * Bulk action - Set Weight. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_weight( $variations, $data ) { self::variation_bulk_set( $variations, 'weight', $data['value'] ); } /** * Bulk action - Set Length. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_length( $variations, $data ) { self::variation_bulk_set( $variations, 'length', $data['value'] ); } /** * Bulk action - Set Width. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_width( $variations, $data ) { self::variation_bulk_set( $variations, 'width', $data['value'] ); } /** * Bulk action - Set Height. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_height( $variations, $data ) { self::variation_bulk_set( $variations, 'height', $data['value'] ); } /** * Bulk action - Set Download Limit. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_download_limit( $variations, $data ) { self::variation_bulk_set( $variations, 'download_limit', $data['value'] ); } /** * Bulk action - Set Download Expiry. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_download_expiry( $variations, $data ) { self::variation_bulk_set( $variations, 'download_expiry', $data['value'] ); } /** * Bulk action - Delete all. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_delete_all( $variations, $data ) { if ( isset( $data['allowed'] ) && 'true' === $data['allowed'] ) { foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); $variation->delete( true ); } } } /** * Bulk action - Sale Schedule. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_sale_schedule( $variations, $data ) { if ( ! isset( $data['date_from'] ) && ! isset( $data['date_to'] ) ) { return; } foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( 'false' !== $data['date_from'] ) { $variation->set_date_on_sale_from( wc_clean( $data['date_from'] ) ); } if ( 'false' !== $data['date_to'] ) { $variation->set_date_on_sale_to( wc_clean( $data['date_to'] ) ); } $variation->save(); } } /** * Bulk action - Increase Regular Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_regular_price_increase( $variations, $data ) { self::variation_bulk_adjust_price( $variations, 'regular_price', '+', wc_clean( $data['value'] ) ); } /** * Bulk action - Decrease Regular Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_regular_price_decrease( $variations, $data ) { self::variation_bulk_adjust_price( $variations, 'regular_price', '-', wc_clean( $data['value'] ) ); } /** * Bulk action - Increase Sale Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_sale_price_increase( $variations, $data ) { self::variation_bulk_adjust_price( $variations, 'sale_price', '+', wc_clean( $data['value'] ) ); } /** * Bulk action - Decrease Sale Prices. * * @param array $variations List of variations. * @param array $data Data to set. * * @used-by bulk_edit_variations */ private static function variation_bulk_action_variable_sale_price_decrease( $variations, $data ) { self::variation_bulk_adjust_price( $variations, 'sale_price', '-', wc_clean( $data['value'] ) ); } /** * Bulk action - Set Price. * * @param array $variations List of variations. * @param string $field price being adjusted _regular_price or _sale_price. * @param string $operator + or -. * @param string $value Price or Percent. * * @used-by bulk_edit_variations */ private static function variation_bulk_adjust_price( $variations, $field, $operator, $value ) { foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); $field_value = $variation->{"get_$field"}( 'edit' ); if ( '%' === substr( $value, -1 ) ) { $percent = wc_format_decimal( substr( $value, 0, -1 ) ); $field_value += NumberUtil::round( ( $field_value / 100 ) * $percent, wc_get_price_decimals() ) * "{$operator}1"; } else { $field_value += $value * "{$operator}1"; } $variation->{"set_$field"}( $field_value ); $variation->save(); } } /** * Bulk set convenience function. * * @param array $variations List of variations. * @param string $field Field to set. * @param string $value to set. */ private static function variation_bulk_set( $variations, $field, $value ) { foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); $variation->{ "set_$field" }( wc_clean( $value ) ); $variation->save(); } } /** * Bulk toggle convenience function. * * @param array $variations List of variations. * @param string $field Field to toggle. */ private static function variation_bulk_toggle( $variations, $field ) { foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); $prev_value = $variation->{ "get_$field" }( 'edit' ); $variation->{ "set_$field" }( ! $prev_value ); $variation->save(); } } /** * Bulk edit variations via AJAX. * * @uses WC_AJAX::variation_bulk_set() * @uses WC_AJAX::variation_bulk_adjust_price() * @uses WC_AJAX::variation_bulk_action_variable_sale_price_decrease() * @uses WC_AJAX::variation_bulk_action_variable_sale_price_increase() * @uses WC_AJAX::variation_bulk_action_variable_regular_price_decrease() * @uses WC_AJAX::variation_bulk_action_variable_regular_price_increase() * @uses WC_AJAX::variation_bulk_action_variable_sale_schedule() * @uses WC_AJAX::variation_bulk_action_delete_all() * @uses WC_AJAX::variation_bulk_action_variable_download_expiry() * @uses WC_AJAX::variation_bulk_action_variable_download_limit() * @uses WC_AJAX::variation_bulk_action_variable_height() * @uses WC_AJAX::variation_bulk_action_variable_width() * @uses WC_AJAX::variation_bulk_action_variable_length() * @uses WC_AJAX::variation_bulk_action_variable_weight() * @uses WC_AJAX::variation_bulk_action_variable_stock() * @uses WC_AJAX::variation_bulk_action_variable_sale_price() * @uses WC_AJAX::variation_bulk_action_variable_regular_price() * @uses WC_AJAX::variation_bulk_action_toggle_manage_stock() * @uses WC_AJAX::variation_bulk_action_toggle_virtual() * @uses WC_AJAX::variation_bulk_action_toggle_downloadable() * @uses WC_AJAX::variation_bulk_action_toggle_enabled * @uses WC_AJAX::variation_bulk_action_variable_low_stock_amount() */ public static function bulk_edit_variations() { ob_start(); check_ajax_referer( 'bulk-edit-variations', 'security' ); // Check permissions again and make sure we have what we need. if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) || empty( $_POST['bulk_action'] ) ) { wp_die( -1 ); } $product_id = absint( $_POST['product_id'] ); $bulk_action = wc_clean( wp_unslash( $_POST['bulk_action'] ) ); $data = ! empty( $_POST['data'] ) ? wc_clean( wp_unslash( $_POST['data'] ) ) : array(); $variations = array(); if ( apply_filters( 'woocommerce_bulk_edit_variations_need_children', true ) ) { $variations = get_posts( array( 'post_parent' => $product_id, 'posts_per_page' => -1, 'post_type' => 'product_variation', 'fields' => 'ids', 'post_status' => array( 'publish', 'private' ), ) ); } if ( method_exists( __CLASS__, "variation_bulk_action_$bulk_action" ) ) { call_user_func( array( __CLASS__, "variation_bulk_action_$bulk_action" ), $variations, $data ); } else { do_action( 'woocommerce_bulk_edit_variations_default', $bulk_action, $data, $product_id, $variations ); } do_action( 'woocommerce_bulk_edit_variations', $bulk_action, $data, $product_id, $variations ); WC_Product_Variable::sync( $product_id ); wc_delete_product_transients( $product_id ); wp_die(); } /** * Handle submissions from assets/js/settings-views-html-settings-tax.js Backbone model. */ public static function tax_rates_save_changes() { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['wc_tax_nonce'], $_POST['changes'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } $current_class = ! empty( $_POST['current_class'] ) ? wp_unslash( $_POST['current_class'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_tax_nonce'] ), 'wc_tax_nonce-class:' . $current_class ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } $current_class = WC_Tax::format_tax_rate_class( $current_class ); // Check User Caps. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( $changes as $tax_rate_id => $data ) { if ( isset( $data['deleted'] ) ) { if ( isset( $data['newRow'] ) ) { // So the user added and deleted a new row. // That's fine, it's not in the database anyways. NEXT! continue; } WC_Tax::_delete_tax_rate( $tax_rate_id ); } $tax_rate = array_intersect_key( $data, array( 'tax_rate_country' => 1, 'tax_rate_state' => 1, 'tax_rate' => 1, 'tax_rate_name' => 1, 'tax_rate_priority' => 1, 'tax_rate_compound' => 1, 'tax_rate_shipping' => 1, 'tax_rate_order' => 1, ) ); if ( isset( $tax_rate['tax_rate'] ) ) { $tax_rate['tax_rate'] = wc_format_decimal( $tax_rate['tax_rate'] ); } if ( isset( $data['newRow'] ) ) { $tax_rate['tax_rate_class'] = $current_class; $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); } elseif ( ! empty( $tax_rate ) ) { WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate ); } if ( isset( $data['postcode'] ) ) { $postcode = array_map( 'wc_clean', $data['postcode'] ); $postcode = array_map( 'wc_normalize_postcode', $postcode ); WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode ); } if ( isset( $data['city'] ) ) { WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', array_map( 'wp_unslash', $data['city'] ) ) ); } } WC_Cache_Helper::invalidate_cache_group( 'taxes' ); WC_Cache_Helper::get_transient_version( 'shipping', true ); wp_send_json_success( array( 'rates' => WC_Tax::get_rates_for_tax_class( $current_class ), ) ); // phpcs:enable } /** * Handle submissions from assets/js/wc-shipping-zones.js Backbone model. */ public static function shipping_zones_save_changes() { if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['changes'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } // Check User Caps. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( $changes as $zone_id => $data ) { if ( isset( $data['deleted'] ) ) { if ( isset( $data['newRow'] ) ) { // So the user added and deleted a new row. // That's fine, it's not in the database anyways. NEXT! continue; } WC_Shipping_Zones::delete_zone( $zone_id ); continue; } $zone_data = array_intersect_key( $data, array( 'zone_id' => 1, 'zone_order' => 1, ) ); if ( isset( $zone_data['zone_id'] ) ) { $zone = new WC_Shipping_Zone( $zone_data['zone_id'] ); if ( isset( $zone_data['zone_order'] ) ) { $zone->set_zone_order( $zone_data['zone_order'] ); } $zone->save(); } } wp_send_json_success( array( 'zones' => WC_Shipping_Zones::get_zones( 'json' ), ) ); } /** * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model. */ public static function shipping_zone_add_method() { if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['method_id'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } // Check User Caps. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) ); $zone = new WC_Shipping_Zone( $zone_id ); $instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) ); wp_send_json_success( array( 'instance_id' => $instance_id, 'zone_id' => $zone->get_id(), 'zone_name' => $zone->get_zone_name(), 'methods' => $zone->get_shipping_methods( false, 'json' ), ) ); } /** * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model. */ public static function shipping_zone_methods_save_changes() { if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['changes'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } global $wpdb; $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) ); $zone = new WC_Shipping_Zone( $zone_id ); $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( isset( $changes['zone_name'] ) ) { $zone->set_zone_name( wc_clean( $changes['zone_name'] ) ); } if ( isset( $changes['zone_locations'] ) ) { $zone->clear_locations( array( 'state', 'country', 'continent' ) ); $locations = array_filter( array_map( 'wc_clean', (array) $changes['zone_locations'] ) ); foreach ( $locations as $location ) { // Each posted location will be in the format type:code. $location_parts = explode( ':', $location ); switch ( $location_parts[0] ) { case 'state': $zone->add_location( $location_parts[1] . ':' . $location_parts[2], 'state' ); break; case 'country': $zone->add_location( $location_parts[1], 'country' ); break; case 'continent': $zone->add_location( $location_parts[1], 'continent' ); break; } } } if ( isset( $changes['zone_postcodes'] ) ) { $zone->clear_locations( 'postcode' ); $postcodes = array_filter( array_map( 'strtoupper', array_map( 'wc_clean', explode( "\n", $changes['zone_postcodes'] ) ) ) ); foreach ( $postcodes as $postcode ) { $zone->add_location( $postcode, 'postcode' ); } } if ( isset( $changes['methods'] ) ) { foreach ( $changes['methods'] as $instance_id => $data ) { $method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) ); if ( isset( $data['deleted'] ) ) { $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); $option_key = $shipping_method->get_instance_option_key(); if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) { delete_option( $option_key ); do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id ); } continue; } $method_data = array_intersect_key( $data, array( 'method_order' => 1, 'enabled' => 1, ) ); if ( isset( $method_data['method_order'] ) ) { $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $method_data['method_order'] ) ), array( 'instance_id' => absint( $instance_id ) ) ); } if ( isset( $method_data['enabled'] ) ) { $is_enabled = absint( 'yes' === $method_data['enabled'] ); if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $is_enabled ), array( 'instance_id' => absint( $instance_id ) ) ) ) { do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method_id, $zone_id, $is_enabled ); } } } } $zone->save(); wp_send_json_success( array( 'zone_id' => $zone->get_id(), 'zone_name' => $zone->get_zone_name(), 'methods' => $zone->get_shipping_methods( false, 'json' ), ) ); } /** * Save method settings */ public static function shipping_zone_methods_save_settings() { if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['data'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $instance_id = absint( $_POST['instance_id'] ); $zone = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id ); $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); $shipping_method->set_post_data( wp_unslash( $_POST['data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $shipping_method->process_admin_options(); WC_Cache_Helper::get_transient_version( 'shipping', true ); wp_send_json_success( array( 'zone_id' => $zone->get_id(), 'zone_name' => $zone->get_zone_name(), 'methods' => $zone->get_shipping_methods( false, 'json' ), 'errors' => $shipping_method->get_errors(), ) ); } /** * Handle submissions from assets/js/wc-shipping-classes.js Backbone model. */ public static function shipping_classes_save_changes() { if ( ! isset( $_POST['wc_shipping_classes_nonce'], $_POST['changes'] ) ) { wp_send_json_error( 'missing_fields' ); wp_die(); } if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_classes_nonce'] ), 'wc_shipping_classes_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_send_json_error( 'bad_nonce' ); wp_die(); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( $changes as $term_id => $data ) { $term_id = absint( $term_id ); if ( isset( $data['deleted'] ) ) { if ( isset( $data['newRow'] ) ) { // So the user added and deleted a new row. // That's fine, it's not in the database anyways. NEXT! continue; } wp_delete_term( $term_id, 'product_shipping_class' ); continue; } $update_args = array(); if ( isset( $data['name'] ) ) { $update_args['name'] = wc_clean( $data['name'] ); } if ( isset( $data['slug'] ) ) { $update_args['slug'] = wc_clean( $data['slug'] ); } if ( isset( $data['description'] ) ) { $update_args['description'] = wc_clean( $data['description'] ); } if ( isset( $data['newRow'] ) ) { $update_args = array_filter( $update_args ); if ( empty( $update_args['name'] ) ) { continue; } $inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args ); $term_id = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id']; } else { wp_update_term( $term_id, 'product_shipping_class', $update_args ); } do_action( 'woocommerce_shipping_classes_save_class', $term_id, $data ); } $wc_shipping = WC_Shipping::instance(); wp_send_json_success( array( 'shipping_classes' => $wc_shipping->get_shipping_classes(), ) ); } /** * Toggle payment gateway on or off via AJAX. * * @since 3.4.0 */ public static function toggle_gateway_enabled() { if ( current_user_can( 'manage_woocommerce' ) && check_ajax_referer( 'woocommerce-toggle-payment-gateway-enabled', 'security' ) && isset( $_POST['gateway_id'] ) ) { // Load gateways. $payment_gateways = WC()->payment_gateways->payment_gateways(); // Get posted gateway. $gateway_id = wc_clean( wp_unslash( $_POST['gateway_id'] ) ); foreach ( $payment_gateways as $gateway ) { if ( ! in_array( $gateway_id, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) { continue; } $enabled = $gateway->get_option( 'enabled', 'no' ); if ( ! wc_string_to_bool( $enabled ) ) { if ( $gateway->needs_setup() ) { wp_send_json_error( 'needs_setup' ); wp_die(); } else { $gateway->update_option( 'enabled', 'yes' ); } } else { // Disable the gateway. $gateway->update_option( 'enabled', 'no' ); } wp_send_json_success( ! wc_string_to_bool( $enabled ) ); wp_die(); } } wp_send_json_error( 'invalid_gateway_id' ); wp_die(); } } WC_AJAX::init(); includes/class-wc-order-item-coupon.php 0000644 00000010001 15132754524 0014145 0 ustar 00 <?php /** * Order Line Item (coupon) * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Order item coupon class. */ class WC_Order_Item_Coupon extends WC_Order_Item { /** * Order Data array. This is the core order data exposed in APIs since 3.0.0. * * @since 3.0.0 * @var array */ protected $extra_data = array( 'code' => '', 'discount' => 0, 'discount_tax' => 0, ); /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set order item name. * * @param string $value Coupon code. */ public function set_name( $value ) { return $this->set_code( $value ); } /** * Set code. * * @param string $value Coupon code. */ public function set_code( $value ) { $this->set_prop( 'code', wc_format_coupon_code( $value ) ); } /** * Set discount amount. * * @param string $value Discount. */ public function set_discount( $value ) { $this->set_prop( 'discount', wc_format_decimal( $value ) ); } /** * Set discounted tax amount. * * @param string $value Discount tax. */ public function set_discount_tax( $value ) { $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order item type. * * @return string */ public function get_type() { return 'coupon'; } /** * Get order item name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { return $this->get_code( $context ); } /** * Get coupon code. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_code( $context = 'view' ) { return $this->get_prop( 'code', $context ); } /** * Get discount amount. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_discount( $context = 'view' ) { return $this->get_prop( 'discount', $context ); } /** * Get discounted tax amount. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * * @return string */ public function get_discount_tax( $context = 'view' ) { return $this->get_prop( 'discount_tax', $context ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * OffsetGet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @return mixed */ public function offsetGet( $offset ) { wc_deprecated_function( 'WC_Order_Item_Coupon::offsetGet', '4.4.0', '' ); if ( 'discount_amount' === $offset ) { $offset = 'discount'; } elseif ( 'discount_amount_tax' === $offset ) { $offset = 'discount_tax'; } return parent::offsetGet( $offset ); } /** * OffsetSet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @param mixed $value Value. */ public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Coupon::offsetSet', '4.4.0', '' ); if ( 'discount_amount' === $offset ) { $offset = 'discount'; } elseif ( 'discount_amount_tax' === $offset ) { $offset = 'discount_tax'; } parent::offsetSet( $offset, $value ); } /** * OffsetExists for ArrayAccess. * * @param string $offset Offset. * @return bool */ public function offsetExists( $offset ) { if ( in_array( $offset, array( 'discount_amount', 'discount_amount_tax' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } includes/class-wc-register-wp-admin-settings.php 0000644 00000012063 15132754524 0016003 0 ustar 00 <?php /** * Take settings registered for WP-Admin and hooks them up to the REST API * * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Register WP admin settings class. */ class WC_Register_WP_Admin_Settings { /** * Contains the current class to pull settings from. * Either a admin page object or WC_Email object * * @var WC_Register_WP_Admin_Settings */ protected $object; /** * Hooks into the settings API and starts registering our settings. * * @since 3.0.0 * @param WC_Email|WC_Settings_Page $object The object that contains the settings to register. * @param string $type Type of settings to register (email or page). */ public function __construct( $object, $type ) { if ( ! is_object( $object ) ) { return; } $this->object = $object; if ( 'page' === $type ) { add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) ); add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) ); } elseif ( 'email' === $type ) { add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) ); add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) ); } } /** * Register's all of our different notification emails as sub groups * of email settings. * * @since 3.0.0 * @param array $groups Existing registered groups. * @return array */ public function register_email_group( $groups ) { $groups[] = array( 'id' => 'email_' . $this->object->id, 'label' => $this->object->title, 'description' => $this->object->description, 'parent_id' => 'email', ); return $groups; } /** * Registers all of the setting form fields for emails to each email type's group. * * @since 3.0.0 * @param array $settings Existing registered settings. * @return array */ public function register_email_settings( $settings ) { foreach ( $this->object->form_fields as $id => $setting ) { $setting['id'] = $id; $setting['option_key'] = array( $this->object->get_option_key(), $id ); $new_setting = $this->register_setting( $setting ); if ( $new_setting ) { $settings[] = $new_setting; } } return $settings; } /** * Registers a setting group, based on admin page ID & label as parent group. * * @since 3.0.0 * @param array $groups Array of previously registered groups. * @return array */ public function register_page_group( $groups ) { $groups[] = array( 'id' => $this->object->get_id(), 'label' => $this->object->get_label(), ); return $groups; } /** * Registers settings to a specific group. * * @since 3.0.0 * @param array $settings Existing registered settings. * @return array */ public function register_page_settings( $settings ) { /** * WP admin settings can be broken down into separate sections from * a UI standpoint. This will grab all the sections associated with * a particular setting group (like 'products') and register them * to the REST API. */ $sections = $this->object->get_sections(); if ( empty( $sections ) ) { // Default section is just an empty string, per admin page classes. $sections = array( '' => '' ); } /** * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. * See the method documentation for an explanation. */ foreach ( $sections as $section => $section_label ) { $settings_from_section = $this->object->get_settings( $section ); foreach ( $settings_from_section as $setting ) { if ( ! isset( $setting['id'] ) ) { continue; } $setting['option_key'] = $setting['id']; $new_setting = $this->register_setting( $setting ); if ( $new_setting ) { $settings[] = $new_setting; } } } return $settings; } /** * Register a setting into the format expected for the Settings REST API. * * @since 3.0.0 * @param array $setting Setting data. * @return array|bool */ public function register_setting( $setting ) { if ( ! isset( $setting['id'] ) ) { return false; } $description = ''; if ( ! empty( $setting['desc'] ) ) { $description = $setting['desc']; } elseif ( ! empty( $setting['description'] ) ) { $description = $setting['description']; } $new_setting = array( 'id' => $setting['id'], 'label' => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ), 'description' => $description, 'type' => $setting['type'], 'option_key' => $setting['option_key'], ); if ( isset( $setting['default'] ) ) { $new_setting['default'] = $setting['default']; } if ( isset( $setting['options'] ) ) { $new_setting['options'] = $setting['options']; } if ( isset( $setting['desc_tip'] ) ) { if ( true === $setting['desc_tip'] ) { $new_setting['tip'] = $description; } elseif ( ! empty( $setting['desc_tip'] ) ) { $new_setting['tip'] = $setting['desc_tip']; } } return $new_setting; } } includes/class-wc-log-levels.php 0000644 00000005050 15132754524 0012656 0 ustar 00 <?php /** * Standard log levels * * @version 3.2.0 * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; /** * Log levels class. */ abstract class WC_Log_Levels { /** * Log Levels * * Description of levels: * 'emergency': System is unusable. * 'alert': Action must be taken immediately. * 'critical': Critical conditions. * 'error': Error conditions. * 'warning': Warning conditions. * 'notice': Normal but significant condition. * 'info': Informational messages. * 'debug': Debug-level messages. * * @see @link {https://tools.ietf.org/html/rfc5424} */ const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; /** * Level strings mapped to integer severity. * * @var array */ protected static $level_to_severity = array( self::EMERGENCY => 800, self::ALERT => 700, self::CRITICAL => 600, self::ERROR => 500, self::WARNING => 400, self::NOTICE => 300, self::INFO => 200, self::DEBUG => 100, ); /** * Severity integers mapped to level strings. * * This is the inverse of $level_severity. * * @var array */ protected static $severity_to_level = array( 800 => self::EMERGENCY, 700 => self::ALERT, 600 => self::CRITICAL, 500 => self::ERROR, 400 => self::WARNING, 300 => self::NOTICE, 200 => self::INFO, 100 => self::DEBUG, ); /** * Validate a level string. * * @param string $level Log level. * @return bool True if $level is a valid level. */ public static function is_valid_level( $level ) { return array_key_exists( strtolower( $level ), self::$level_to_severity ); } /** * Translate level string to integer. * * @param string $level Log level, options: emergency|alert|critical|error|warning|notice|info|debug. * @return int 100 (debug) - 800 (emergency) or 0 if not recognized */ public static function get_level_severity( $level ) { return self::is_valid_level( $level ) ? self::$level_to_severity[ strtolower( $level ) ] : 0; } /** * Translate severity integer to level string. * * @param int $severity Severity level. * @return bool|string False if not recognized. Otherwise string representation of level. */ public static function get_severity_level( $severity ) { if ( ! array_key_exists( $severity, self::$severity_to_level ) ) { return false; } return self::$severity_to_level[ $severity ]; } } includes/class-wc-geolocation.php 0000644 00000024532 15132754524 0013116 0 ustar 00 <?php /** * Geolocation class * * Handles geolocation and updating the geolocation database. * * This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com. * * @package WooCommerce\Classes * @version 3.9.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Geolocation Class. */ class WC_Geolocation { /** * GeoLite IPv4 DB. * * @deprecated 3.4.0 */ const GEOLITE_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz'; /** * GeoLite IPv6 DB. * * @deprecated 3.4.0 */ const GEOLITE_IPV6_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz'; /** * GeoLite2 DB. * * @since 3.4.0 * @deprecated 3.9.0 */ const GEOLITE2_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz'; /** * API endpoints for looking up user IP address. * * @var array */ private static $ip_lookup_apis = array( 'ipify' => 'http://api.ipify.org/', 'ipecho' => 'http://ipecho.net/plain', 'ident' => 'http://ident.me', 'whatismyipaddress' => 'http://bot.whatismyipaddress.com', ); /** * API endpoints for geolocating an IP address * * @var array */ private static $geoip_apis = array( 'ipinfo.io' => 'https://ipinfo.io/%s/json', 'ip-api.com' => 'http://ip-api.com/json/%s', ); /** * Check if geolocation is enabled. * * @since 3.4.0 * @param string $current_settings Current geolocation settings. * @return bool */ private static function is_geolocation_enabled( $current_settings ) { return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true ); } /** * Get current user IP Address. * * @return string */ public static function get_ip_address() { if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 // Make sure we always only send through the first IP in the list which should always be the client IP. return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); } return ''; } /** * Get user IP Address using an external service. * This can be used as a fallback for users on localhost where * get_ip_address() will be a local IP and non-geolocatable. * * @return string */ public static function get_external_ip_address() { $external_ip_address = '0.0.0.0'; if ( '' !== self::get_ip_address() ) { $transient_name = 'external_ip_address_' . self::get_ip_address(); $external_ip_address = get_transient( $transient_name ); } if ( false === $external_ip_address ) { $external_ip_address = '0.0.0.0'; $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); $ip_lookup_services_keys = array_keys( $ip_lookup_services ); shuffle( $ip_lookup_services_keys ); foreach ( $ip_lookup_services_keys as $service_name ) { $service_endpoint = $ip_lookup_services[ $service_name ]; $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) { $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); break; } } set_transient( $transient_name, $external_ip_address, DAY_IN_SECONDS ); } return $external_ip_address; } /** * Geolocate an IP address. * * @param string $ip_address IP Address. * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). * @return array */ public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) { // Filter to allow custom geolocation of the IP address. $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); if ( false !== $country_code ) { return array( 'country' => $country_code, 'state' => '', 'city' => '', 'postcode' => '', ); } if ( empty( $ip_address ) ) { $ip_address = self::get_ip_address(); } $country_code = self::get_country_code_from_headers(); /** * Get geolocation filter. * * @since 3.9.0 * @param array $geolocation Geolocation data, including country, state, city, and postcode. * @param string $ip_address IP Address. */ $geolocation = apply_filters( 'woocommerce_get_geolocation', array( 'country' => $country_code, 'state' => '', 'city' => '', 'postcode' => '', ), $ip_address ); // If we still haven't found a country code, let's consider doing an API lookup. if ( '' === $geolocation['country'] && $api_fallback ) { $geolocation['country'] = self::geolocate_via_api( $ip_address ); } // It's possible that we're in a local environment, in which case the geolocation needs to be done from the // external address. if ( '' === $geolocation['country'] && $fallback ) { $external_ip_address = self::get_external_ip_address(); // Only bother with this if the external IP differs. if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) { return self::geolocate_ip( $external_ip_address, false, $api_fallback ); } } return array( 'country' => $geolocation['country'], 'state' => $geolocation['state'], 'city' => $geolocation['city'], 'postcode' => $geolocation['postcode'], ); } /** * Path to our local db. * * @deprecated 3.9.0 * @param string $deprecated Deprecated since 3.4.0. * @return string */ public static function get_local_database_path( $deprecated = '2' ) { wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' ); $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); return $integration->get_database_service()->get_database_path(); } /** * Update geoip database. * * @deprecated 3.9.0 * Extract files with PharData. Tool built into PHP since 5.3. */ public static function update_database() { wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' ); $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); $integration->update_database(); } /** * Fetches the country code from the request headers, if one is available. * * @since 3.9.0 * @return string The country code pulled from the headers, or empty string if one was not found. */ private static function get_country_code_from_headers() { $country_code = ''; $headers = array( 'MM_COUNTRY_CODE', 'GEOIP_COUNTRY_CODE', 'HTTP_CF_IPCOUNTRY', 'HTTP_X_COUNTRY_CODE', ); foreach ( $headers as $header ) { if ( empty( $_SERVER[ $header ] ) ) { continue; } $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); break; } return $country_code; } /** * Use APIs to Geolocate the user. * * Geolocation APIs can be added through the use of the woocommerce_geolocation_geoip_apis filter. * Provide a name=>value pair for service-slug=>endpoint. * * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result * will be cached in a transient. * * @param string $ip_address IP address. * @return string */ private static function geolocate_via_api( $ip_address ) { $country_code = get_transient( 'geoip_' . $ip_address ); if ( false === $country_code ) { $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); if ( empty( $geoip_services ) ) { return ''; } $geoip_services_keys = array_keys( $geoip_services ); shuffle( $geoip_services_keys ); foreach ( $geoip_services_keys as $service_name ) { $service_endpoint = $geoip_services[ $service_name ]; $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); if ( ! is_wp_error( $response ) && $response['body'] ) { switch ( $service_name ) { case 'ipinfo.io': $data = json_decode( $response['body'] ); $country_code = isset( $data->country ) ? $data->country : ''; break; case 'ip-api.com': $data = json_decode( $response['body'] ); $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine break; default: $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); break; } $country_code = sanitize_text_field( strtoupper( $country_code ) ); if ( $country_code ) { break; } } } set_transient( 'geoip_' . $ip_address, $country_code, DAY_IN_SECONDS ); } return $country_code; } /** * Hook in geolocation functionality. * * @deprecated 3.9.0 * @return null */ public static function init() { wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' ); return null; } /** * Prevent geolocation via MaxMind when using legacy versions of php. * * @deprecated 3.9.0 * @since 3.4.0 * @param string $default_customer_address current value. * @return string */ public static function disable_geolocation_on_legacy_php( $default_customer_address ) { wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' ); if ( self::is_geolocation_enabled( $default_customer_address ) ) { $default_customer_address = 'base'; } return $default_customer_address; } /** * Maybe trigger a DB update for the first time. * * @deprecated 3.9.0 * @param string $new_value New value. * @param string $old_value Old value. * @return string */ public static function maybe_update_database( $new_value, $old_value ) { wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' ); if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { self::update_database(); } return $new_value; } } includes/wc-stock-functions.php 0000644 00000032334 15132754524 0012640 0 ustar 00 <?php /** * WooCommerce Stock Functions * * Functions used to manage product stock levels. * * @package WooCommerce\Functions * @version 3.4.0 */ defined( 'ABSPATH' ) || exit; /** * Update a product's stock amount. * * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). * * @since 3.0.0 this supports set, increase and decrease. * * @param int|WC_Product $product Product ID or product instance. * @param int|null $stock_quantity Stock quantity. * @param string $operation Type of opertion, allows 'set', 'increase' and 'decrease'. * @param bool $updating If true, the product object won't be saved here as it will be updated later. * @return bool|int|null */ function wc_update_product_stock( $product, $stock_quantity = null, $operation = 'set', $updating = false ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = wc_get_product( $product ); } if ( ! $product ) { return false; } if ( ! is_null( $stock_quantity ) && $product->managing_stock() ) { // Some products (variations) can have their stock managed by their parent. Get the correct object to be updated here. $product_id_with_stock = $product->get_stock_managed_by_id(); $product_with_stock = $product_id_with_stock !== $product->get_id() ? wc_get_product( $product_id_with_stock ) : $product; $data_store = WC_Data_Store::load( 'product' ); // Fire actions to let 3rd parties know the stock is about to be changed. if ( $product_with_stock->is_type( 'variation' ) ) { do_action( 'woocommerce_variation_before_set_stock', $product_with_stock ); } else { do_action( 'woocommerce_product_before_set_stock', $product_with_stock ); } // Update the database. $new_stock = $data_store->update_product_stock( $product_id_with_stock, $stock_quantity, $operation ); // Update the product object. $data_store->read_stock_quantity( $product_with_stock, $new_stock ); // If this is not being called during an update routine, save the product so stock status etc is in sync, and caches are cleared. if ( ! $updating ) { $product_with_stock->save(); } // Fire actions to let 3rd parties know the stock changed. if ( $product_with_stock->is_type( 'variation' ) ) { do_action( 'woocommerce_variation_set_stock', $product_with_stock ); } else { do_action( 'woocommerce_product_set_stock', $product_with_stock ); } return $product_with_stock->get_stock_quantity(); } return $product->get_stock_quantity(); } /** * Update a product's stock status. * * @param int $product_id Product ID. * @param string $status Status. */ function wc_update_product_stock_status( $product_id, $status ) { $product = wc_get_product( $product_id ); if ( $product ) { $product->set_stock_status( $status ); $product->save(); } } /** * When a payment is complete, we can reduce stock levels for items within an order. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_maybe_reduce_stock_levels( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); $trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id ); // Only continue if we're reducing stock. if ( ! $trigger_reduce ) { return; } wc_reduce_stock_levels( $order ); // Ensure stock is marked as "reduced" in case payment complete or other stock actions are called. $order->get_data_store()->set_stock_reduced( $order_id, true ); } add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' ); /** * When a payment is cancelled, restore stock. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_maybe_increase_stock_levels( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); $trigger_increase = (bool) $stock_reduced; // Only continue if we're increasing stock. if ( ! $trigger_increase ) { return; } wc_increase_stock_levels( $order ); // Ensure stock is not marked as "reduced" anymore. $order->get_data_store()->set_stock_reduced( $order_id, false ); } add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); /** * Reduce stock levels for items within an order, if stock has not already been reduced for the items. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_reduce_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only reduce stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } /** * Filter order item quantity. * * @param int|float $quantity Quantity. * @param WC_Order $order Order data. * @param WC_Order_Item_Product $item Order item data. */ $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) ); continue; } $item->add_meta_data( '_reduced_stock', $qty, true ); $item->save(); $changes[] = array( 'product' => $product, 'from' => $new_stock + $qty, 'to' => $new_stock, ); } wc_trigger_stock_change_notifications( $order, $changes ); do_action( 'woocommerce_reduce_order_stock', $order ); } /** * After stock change events, triggers emails and adds order notes. * * @since 3.5.0 * @param WC_Order $order order object. * @param array $changes Array of changes. */ function wc_trigger_stock_change_notifications( $order, $changes ) { if ( empty( $changes ) ) { return; } $order_notes = array(); $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); foreach ( $changes as $change ) { $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); if ( $change['to'] <= $no_stock_amount ) { do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); } elseif ( $change['to'] <= $low_stock_amount ) { do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); } if ( $change['to'] < 0 ) { do_action( 'woocommerce_product_on_backorder', array( 'product' => wc_get_product( $change['product']->get_id() ), 'order_id' => $order->get_id(), 'quantity' => abs( $change['from'] - $change['to'] ), ) ); } } $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) ); } /** * Increase stock levels for items within an order. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_increase_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only increase stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) ); continue; } $item->delete_meta_data( '_reduced_stock' ); $item->save(); $changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '→' . $new_stock; } if ( $changes ) { $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) ); } do_action( 'woocommerce_restore_order_stock', $order ); } /** * See how much stock is being held in pending orders. * * @since 3.5.0 * @param WC_Product $product Product to check. * @param integer $exclude_order_id Order ID to exclude. * @return int */ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return 0; } return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); } /** * Hold stock for an order. * * @throws ReserveStockException If reserve stock fails. * * @since 4.1.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_reserve_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since @since 4.1.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); /** * Release held stock for an order. * * @since 4.3.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_release_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); /** * Return low stock amount to determine if notification needs to be sent * * Since 5.2.0, this function no longer redirects from variation to its parent product. * Low stock amount can now be attached to the variation itself and if it isn't, only * then we check the parent product, and if it's not there, then we take the default * from the store-wide setting. * * @param WC_Product $product Product to get data from. * @since 3.5.0 * @return int */ function wc_get_low_stock_amount( WC_Product $product ) { $low_stock_amount = $product->get_low_stock_amount(); if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); $low_stock_amount = $product->get_low_stock_amount(); } if ( '' === $low_stock_amount ) { $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); } return (int) $low_stock_amount; } includes/widgets/class-wc-widget-recent-reviews.php 0000644 00000004361 15132754524 0016502 0 ustar 00 <?php /** * Recent Reviews Widget. * * @package WooCommerce\Widgets * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget recent reviews class. */ class WC_Widget_Recent_Reviews extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_recent_reviews'; $this->widget_description = __( 'Display a list of recent reviews from your store.', 'woocommerce' ); $this->widget_id = 'woocommerce_recent_reviews'; $this->widget_name = __( 'Recent Product Reviews', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Recent reviews', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'number' => array( 'type' => 'number', 'step' => 1, 'min' => 1, 'max' => '', 'std' => 10, 'label' => __( 'Number of reviews to show', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { global $comments, $comment; if ( $this->get_cached_widget( $args ) ) { return; } ob_start(); $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; $comments = get_comments( array( 'number' => $number, 'status' => 'approve', 'post_status' => 'publish', 'post_type' => 'product', 'parent' => 0, ) ); // WPCS: override ok. if ( $comments ) { $this->widget_start( $args, $instance ); echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_review_list', '<ul class="product_list_widget">' ) ); foreach ( (array) $comments as $comment ) { wc_get_template( 'content-widget-reviews.php', array( 'comment' => $comment, 'product' => wc_get_product( $comment->comment_post_ID ), ) ); } echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_review_list', '</ul>' ) ); $this->widget_end( $args ); } $content = ob_get_clean(); echo $content; // WPCS: XSS ok. $this->cache_widget( $args, $content ); } } includes/widgets/class-wc-widget-products.php 0000644 00000014175 15132754524 0015407 0 ustar 00 <?php /** * List products. One widget to rule them all. * * @package WooCommerce\Widgets * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget products. */ class WC_Widget_Products extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_products'; $this->widget_description = __( "A list of your store's products.", 'woocommerce' ); $this->widget_id = 'woocommerce_products'; $this->widget_name = __( 'Products list', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Products', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'number' => array( 'type' => 'number', 'step' => 1, 'min' => 1, 'max' => '', 'std' => 5, 'label' => __( 'Number of products to show', 'woocommerce' ), ), 'show' => array( 'type' => 'select', 'std' => '', 'label' => __( 'Show', 'woocommerce' ), 'options' => array( '' => __( 'All products', 'woocommerce' ), 'featured' => __( 'Featured products', 'woocommerce' ), 'onsale' => __( 'On-sale products', 'woocommerce' ), ), ), 'orderby' => array( 'type' => 'select', 'std' => 'date', 'label' => __( 'Order by', 'woocommerce' ), 'options' => array( 'date' => __( 'Date', 'woocommerce' ), 'price' => __( 'Price', 'woocommerce' ), 'rand' => __( 'Random', 'woocommerce' ), 'sales' => __( 'Sales', 'woocommerce' ), ), ), 'order' => array( 'type' => 'select', 'std' => 'desc', 'label' => _x( 'Order', 'Sorting order', 'woocommerce' ), 'options' => array( 'asc' => __( 'ASC', 'woocommerce' ), 'desc' => __( 'DESC', 'woocommerce' ), ), ), 'hide_free' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Hide free products', 'woocommerce' ), ), 'show_hidden' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Show hidden products', 'woocommerce' ), ), ); parent::__construct(); } /** * Query the products and return them. * * @param array $args Arguments. * @param array $instance Widget instance. * * @return WP_Query */ public function get_products( $args, $instance ) { $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; $show = ! empty( $instance['show'] ) ? sanitize_title( $instance['show'] ) : $this->settings['show']['std']; $orderby = ! empty( $instance['orderby'] ) ? sanitize_title( $instance['orderby'] ) : $this->settings['orderby']['std']; $order = ! empty( $instance['order'] ) ? sanitize_title( $instance['order'] ) : $this->settings['order']['std']; $product_visibility_term_ids = wc_get_product_visibility_term_ids(); $query_args = array( 'posts_per_page' => $number, 'post_status' => 'publish', 'post_type' => 'product', 'no_found_rows' => 1, 'order' => $order, 'meta_query' => array(), 'tax_query' => array( 'relation' => 'AND', ), ); // WPCS: slow query ok. if ( empty( $instance['show_hidden'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => is_search() ? $product_visibility_term_ids['exclude-from-search'] : $product_visibility_term_ids['exclude-from-catalog'], 'operator' => 'NOT IN', ); $query_args['post_parent'] = 0; } if ( ! empty( $instance['hide_free'] ) ) { $query_args['meta_query'][] = array( 'key' => '_price', 'value' => 0, 'compare' => '>', 'type' => 'DECIMAL', ); } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $query_args['tax_query'][] = array( array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_term_ids['outofstock'], 'operator' => 'NOT IN', ), ); // WPCS: slow query ok. } switch ( $show ) { case 'featured': $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_term_ids['featured'], ); break; case 'onsale': $product_ids_on_sale = wc_get_product_ids_on_sale(); $product_ids_on_sale[] = 0; $query_args['post__in'] = $product_ids_on_sale; break; } switch ( $orderby ) { case 'price': $query_args['meta_key'] = '_price'; // WPCS: slow query ok. $query_args['orderby'] = 'meta_value_num'; break; case 'rand': $query_args['orderby'] = 'rand'; break; case 'sales': $query_args['meta_key'] = 'total_sales'; // WPCS: slow query ok. $query_args['orderby'] = 'meta_value_num'; break; default: $query_args['orderby'] = 'date'; } return new WP_Query( apply_filters( 'woocommerce_products_widget_query_args', $query_args ) ); } /** * Output widget. * * @param array $args Arguments. * @param array $instance Widget instance. * * @see WP_Widget */ public function widget( $args, $instance ) { if ( $this->get_cached_widget( $args ) ) { return; } ob_start(); wc_set_loop_prop( 'name', 'widget' ); $products = $this->get_products( $args, $instance ); if ( $products && $products->have_posts() ) { $this->widget_start( $args, $instance ); echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) ); $template_args = array( 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, 'show_rating' => true, ); while ( $products->have_posts() ) { $products->the_post(); wc_get_template( 'content-widget-product.php', $template_args ); } echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) ); $this->widget_end( $args ); } wp_reset_postdata(); echo $this->cache_widget( $args, ob_get_clean() ); // WPCS: XSS ok. } } includes/widgets/class-wc-widget-recently-viewed.php 0000644 00000005656 15132754524 0016656 0 ustar 00 <?php /** * Recent Products Widget. * * @package WooCommerce\Widgets * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget recently viewed. */ class WC_Widget_Recently_Viewed extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_recently_viewed_products'; $this->widget_description = __( "Display a list of a customer's recently viewed products.", 'woocommerce' ); $this->widget_id = 'woocommerce_recently_viewed_products'; $this->widget_name = __( 'Recently Viewed Products list', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Recently Viewed Products', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'number' => array( 'type' => 'number', 'step' => 1, 'min' => 1, 'max' => 15, 'std' => 10, 'label' => __( 'Number of products to show', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { $viewed_products = ! empty( $_COOKIE['woocommerce_recently_viewed'] ) ? (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) : array(); // @codingStandardsIgnoreLine $viewed_products = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) ); if ( empty( $viewed_products ) ) { return; } ob_start(); $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; $query_args = array( 'posts_per_page' => $number, 'no_found_rows' => 1, 'post_status' => 'publish', 'post_type' => 'product', 'post__in' => $viewed_products, 'orderby' => 'post__in', ); if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $query_args['tax_query'] = array( array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'outofstock', 'operator' => 'NOT IN', ), ); // WPCS: slow query ok. } $r = new WP_Query( apply_filters( 'woocommerce_recently_viewed_products_widget_query_args', $query_args ) ); if ( $r->have_posts() ) { $this->widget_start( $args, $instance ); echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) ); $template_args = array( 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, ); while ( $r->have_posts() ) { $r->the_post(); wc_get_template( 'content-widget-product.php', $template_args ); } echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) ); $this->widget_end( $args ); } wp_reset_postdata(); $content = ob_get_clean(); echo $content; // WPCS: XSS ok. } } includes/widgets/class-wc-widget-product-tag-cloud.php 0000644 00000005604 15132754524 0017076 0 ustar 00 <?php /** * Tag Cloud Widget. * * @package WooCommerce\Widgets * @version 3.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Widget product tag cloud */ class WC_Widget_Product_Tag_Cloud extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_product_tag_cloud'; $this->widget_description = __( 'A cloud of your most used product tags.', 'woocommerce' ); $this->widget_id = 'woocommerce_product_tag_cloud'; $this->widget_name = __( 'Product Tag Cloud', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Product tags', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { $current_taxonomy = $this->get_current_taxonomy( $instance ); if ( empty( $instance['title'] ) ) { $taxonomy = get_taxonomy( $current_taxonomy ); $instance['title'] = $taxonomy->labels->name; } $this->widget_start( $args, $instance ); echo '<div class="tagcloud">'; wp_tag_cloud( apply_filters( 'woocommerce_product_tag_cloud_widget_args', array( 'taxonomy' => $current_taxonomy, 'topic_count_text_callback' => array( $this, 'topic_count_text' ), ) ) ); echo '</div>'; $this->widget_end( $args ); } /** * Return the taxonomy being displayed. * * @param object $instance Widget instance. * @return string */ public function get_current_taxonomy( $instance ) { return 'product_tag'; } /** * Returns topic count text. * * @since 3.4.0 * @param int $count Count text. * @return string */ public function topic_count_text( $count ) { /* translators: %s: product count */ return sprintf( _n( '%s product', '%s products', $count, 'woocommerce' ), number_format_i18n( $count ) ); } // Ignore whole block to avoid warnings about PSR2.Methods.MethodDeclaration.Underscore violation. // @codingStandardsIgnoreStart /** * Return the taxonomy being displayed. * * @deprecated 3.4.0 * @param object $instance Widget instance. * @return string */ public function _get_current_taxonomy( $instance ) { wc_deprecated_function( '_get_current_taxonomy', '3.4.0', 'WC_Widget_Product_Tag_Cloud->get_current_taxonomy' ); return $this->get_current_taxonomy( $instance ); } /** * Returns topic count text. * * @deprecated 3.4.0 * @since 2.6.0 * @param int $count Count text. * @return string */ public function _topic_count_text( $count ) { wc_deprecated_function( '_topic_count_text', '3.4.0', 'WC_Widget_Product_Tag_Cloud->topic_count_text' ); return $this->topic_count_text( $count ); } // @codingStandardsIgnoreEnd } includes/widgets/class-wc-widget-price-filter.php 0000644 00000015557 15132754524 0016136 0 ustar 00 <?php /** * Price Filter Widget and related functions. * * Generates a range slider to filter products by price. * * @package WooCommerce\Widgets * @version 2.3.0 */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Widget price filter class. */ class WC_Widget_Price_Filter extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_price_filter'; $this->widget_description = __( 'Display a slider to filter products in your store by price.', 'woocommerce' ); $this->widget_id = 'woocommerce_price_filter'; $this->widget_name = __( 'Filter Products by Price', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Filter by price', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), ); $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2', true ); wp_register_script( 'wc-jquery-ui-touchpunch', WC()->plugin_url() . '/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch' . $suffix . '.js', array( 'jquery-ui-slider' ), $version, true ); wp_register_script( 'wc-price-slider', WC()->plugin_url() . '/assets/js/frontend/price-slider' . $suffix . '.js', array( 'jquery-ui-slider', 'wc-jquery-ui-touchpunch', 'accounting' ), $version, true ); wp_localize_script( 'wc-price-slider', 'woocommerce_price_slider_params', array( 'currency_format_num_decimals' => 0, 'currency_format_symbol' => get_woocommerce_currency_symbol(), 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), ) ); if ( is_customize_preview() ) { wp_enqueue_script( 'wc-price-slider' ); } parent::__construct(); } /** * Output widget. * * @see WP_Widget * * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { global $wp; // Requires lookup table added in 3.6. if ( version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) ) { return; } if ( ! is_shop() && ! is_product_taxonomy() ) { return; } // If there are not posts and we're not filtering, hide the widget. if ( ! WC()->query->get_main_query()->post_count && ! isset( $_GET['min_price'] ) && ! isset( $_GET['max_price'] ) ) { // WPCS: input var ok, CSRF ok. return; } wp_enqueue_script( 'wc-price-slider' ); // Round values to nearest 10 by default. $step = max( apply_filters( 'woocommerce_price_filter_widget_step', 10 ), 1 ); // Find min and max price in current result set. $prices = $this->get_filtered_price(); $min_price = $prices->min_price; $max_price = $prices->max_price; // Check to see if we should add taxes to the prices if store are excl tax but display incl. $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); if ( wc_tax_enabled() && ! wc_prices_include_tax() && 'incl' === $tax_display_mode ) { $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. $tax_rates = WC_Tax::get_rates( $tax_class ); if ( $tax_rates ) { $min_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min_price, $tax_rates ) ); $max_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $max_price, $tax_rates ) ); } } $min_price = apply_filters( 'woocommerce_price_filter_widget_min_amount', floor( $min_price / $step ) * $step ); $max_price = apply_filters( 'woocommerce_price_filter_widget_max_amount', ceil( $max_price / $step ) * $step ); // If both min and max are equal, we don't need a slider. if ( $min_price === $max_price ) { return; } $current_min_price = isset( $_GET['min_price'] ) ? floor( floatval( wp_unslash( $_GET['min_price'] ) ) / $step ) * $step : $min_price; // WPCS: input var ok, CSRF ok. $current_max_price = isset( $_GET['max_price'] ) ? ceil( floatval( wp_unslash( $_GET['max_price'] ) ) / $step ) * $step : $max_price; // WPCS: input var ok, CSRF ok. $this->widget_start( $args, $instance ); if ( '' === get_option( 'permalink_structure' ) ) { $form_action = remove_query_arg( array( 'page', 'paged', 'product-page' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); } else { $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) ); } wc_get_template( 'content-widget-price-filter.php', array( 'form_action' => $form_action, 'step' => $step, 'min_price' => $min_price, 'max_price' => $max_price, 'current_min_price' => $current_min_price, 'current_max_price' => $current_max_price, ) ); $this->widget_end( $args ); } /** * Get filtered min price for current products. * * @return int */ protected function get_filtered_price() { global $wpdb; $args = WC()->query->get_main_query()->query_vars; $tax_query = isset( $args['tax_query'] ) ? $args['tax_query'] : array(); $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); if ( ! is_post_type_archive( 'product' ) && ! empty( $args['taxonomy'] ) && ! empty( $args['term'] ) ) { $tax_query[] = WC()->query->get_main_tax_query(); } foreach ( $meta_query + $tax_query as $key => $query ) { if ( ! empty( $query['price_filter'] ) || ! empty( $query['rating_filter'] ) ) { unset( $meta_query[ $key ] ); } } $meta_query = new WP_Meta_Query( $meta_query ); $tax_query = new WP_Tax_Query( $tax_query ); $search = WC_Query::get_main_search_query_sql(); $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); $search_query_sql = $search ? ' AND ' . $search : ''; $sql = " SELECT min( min_price ) as min_price, MAX( max_price ) as max_price FROM {$wpdb->wc_product_meta_lookup} WHERE product_id IN ( SELECT ID FROM {$wpdb->posts} " . $tax_query_sql['join'] . $meta_query_sql['join'] . " WHERE {$wpdb->posts}.post_type IN ('" . implode( "','", array_map( 'esc_sql', apply_filters( 'woocommerce_price_filter_post_type', array( 'product' ) ) ) ) . "') AND {$wpdb->posts}.post_status = 'publish' " . $tax_query_sql['where'] . $meta_query_sql['where'] . $search_query_sql . ' )'; $sql = apply_filters( 'woocommerce_price_filter_sql', $sql, $meta_query_sql, $tax_query_sql ); return $wpdb->get_row( $sql ); // WPCS: unprepared SQL ok. } } includes/widgets/class-wc-widget-layered-nav.php 0000644 00000035053 15132754524 0015751 0 ustar 00 <?php /** * Layered nav widget * * @package WooCommerce\Widgets * @version 2.6.0 */ use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer; defined( 'ABSPATH' ) || exit; /** * Widget layered nav class. */ class WC_Widget_Layered_Nav extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_layered_nav woocommerce-widget-layered-nav'; $this->widget_description = __( 'Display a list of attributes to filter products in your store.', 'woocommerce' ); $this->widget_id = 'woocommerce_layered_nav'; $this->widget_name = __( 'Filter Products by Attribute', 'woocommerce' ); parent::__construct(); } /** * Updates a particular instance of a widget. * * @see WP_Widget->update * * @param array $new_instance New Instance. * @param array $old_instance Old Instance. * * @return array */ public function update( $new_instance, $old_instance ) { $this->init_settings(); return parent::update( $new_instance, $old_instance ); } /** * Outputs the settings update form. * * @see WP_Widget->form * * @param array $instance Instance. */ public function form( $instance ) { $this->init_settings(); parent::form( $instance ); } /** * Init settings after post types are registered. */ public function init_settings() { $attribute_array = array(); $std_attribute = ''; $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $tax ) { if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { $attribute_array[ $tax->attribute_name ] = $tax->attribute_name; } } $std_attribute = current( $attribute_array ); } $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Filter by', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'attribute' => array( 'type' => 'select', 'std' => $std_attribute, 'label' => __( 'Attribute', 'woocommerce' ), 'options' => $attribute_array, ), 'display_type' => array( 'type' => 'select', 'std' => 'list', 'label' => __( 'Display type', 'woocommerce' ), 'options' => array( 'list' => __( 'List', 'woocommerce' ), 'dropdown' => __( 'Dropdown', 'woocommerce' ), ), ), 'query_type' => array( 'type' => 'select', 'std' => 'and', 'label' => __( 'Query type', 'woocommerce' ), 'options' => array( 'and' => __( 'AND', 'woocommerce' ), 'or' => __( 'OR', 'woocommerce' ), ), ), ); } /** * Get this widgets taxonomy. * * @param array $instance Array of instance options. * @return string */ protected function get_instance_taxonomy( $instance ) { if ( isset( $instance['attribute'] ) ) { return wc_attribute_taxonomy_name( $instance['attribute'] ); } $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $tax ) { if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { return wc_attribute_taxonomy_name( $tax->attribute_name ); } } } return ''; } /** * Get this widgets query type. * * @param array $instance Array of instance options. * @return string */ protected function get_instance_query_type( $instance ) { return isset( $instance['query_type'] ) ? $instance['query_type'] : 'and'; } /** * Get this widgets display type. * * @param array $instance Array of instance options. * @return string */ protected function get_instance_display_type( $instance ) { return isset( $instance['display_type'] ) ? $instance['display_type'] : 'list'; } /** * Output widget. * * @see WP_Widget * * @param array $args Arguments. * @param array $instance Instance. */ public function widget( $args, $instance ) { if ( ! is_shop() && ! is_product_taxonomy() ) { return; } $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); $taxonomy = $this->get_instance_taxonomy( $instance ); $query_type = $this->get_instance_query_type( $instance ); $display_type = $this->get_instance_display_type( $instance ); if ( ! taxonomy_exists( $taxonomy ) ) { return; } $terms = get_terms( $taxonomy, array( 'hide_empty' => '1' ) ); if ( 0 === count( $terms ) ) { return; } ob_start(); $this->widget_start( $args, $instance ); if ( 'dropdown' === $display_type ) { wp_enqueue_script( 'selectWoo' ); wp_enqueue_style( 'select2' ); $found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type ); } else { $found = $this->layered_nav_list( $terms, $taxonomy, $query_type ); } $this->widget_end( $args ); // Force found when option is selected - do not force found on taxonomy attributes. if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { $found = true; } if ( ! $found ) { ob_end_clean(); } else { echo ob_get_clean(); // @codingStandardsIgnoreLine } } /** * Return the currently viewed taxonomy name. * * @return string */ protected function get_current_taxonomy() { return is_tax() ? get_queried_object()->taxonomy : ''; } /** * Return the currently viewed term ID. * * @return int */ protected function get_current_term_id() { return absint( is_tax() ? get_queried_object()->term_id : 0 ); } /** * Return the currently viewed term slug. * * @return int */ protected function get_current_term_slug() { return absint( is_tax() ? get_queried_object()->slug : 0 ); } /** * Show dropdown layered nav. * * @param array $terms Terms. * @param string $taxonomy Taxonomy. * @param string $query_type Query Type. * @return bool Will nav display? */ protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) { global $wp; $found = false; if ( $taxonomy !== $this->get_current_taxonomy() ) { $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); $taxonomy_filter_name = wc_attribute_taxonomy_slug( $taxonomy ); $taxonomy_label = wc_attribute_label( $taxonomy ); /* translators: %s: taxonomy name */ $any_label = apply_filters( 'woocommerce_layered_nav_any_label', sprintf( __( 'Any %s', 'woocommerce' ), $taxonomy_label ), $taxonomy_label, $taxonomy ); $multiple = 'or' === $query_type; $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); if ( '' === get_option( 'permalink_structure' ) ) { $form_action = remove_query_arg( array( 'page', 'paged' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); } else { $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( user_trailingslashit( $wp->request ) ) ); } echo '<form method="get" action="' . esc_url( $form_action ) . '" class="woocommerce-widget-layered-nav-dropdown">'; echo '<select class="woocommerce-widget-layered-nav-dropdown dropdown_layered_nav_' . esc_attr( $taxonomy_filter_name ) . '"' . ( $multiple ? 'multiple="multiple"' : '' ) . '>'; echo '<option value="">' . esc_html( $any_label ) . '</option>'; foreach ( $terms as $term ) { // If on a term page, skip that term in widget list. if ( $term->term_id === $this->get_current_term_id() ) { continue; } // Get count based on current view. $option_is_set = in_array( $term->slug, $current_values, true ); $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; // Only show options with count > 0. if ( 0 < $count ) { $found = true; } elseif ( 0 === $count && ! $option_is_set ) { continue; } echo '<option value="' . esc_attr( urldecode( $term->slug ) ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( $term->name ) . '</option>'; } echo '</select>'; if ( $multiple ) { echo '<button class="woocommerce-widget-layered-nav-dropdown__submit" type="submit" value="' . esc_attr__( 'Apply', 'woocommerce' ) . '">' . esc_html__( 'Apply', 'woocommerce' ) . '</button>'; } if ( 'or' === $query_type ) { echo '<input type="hidden" name="query_type_' . esc_attr( $taxonomy_filter_name ) . '" value="or" />'; } echo '<input type="hidden" name="filter_' . esc_attr( $taxonomy_filter_name ) . '" value="' . esc_attr( implode( ',', $current_values ) ) . '" />'; echo wc_query_string_form_fields( null, array( 'filter_' . $taxonomy_filter_name, 'query_type_' . $taxonomy_filter_name ), '', true ); // @codingStandardsIgnoreLine echo '</form>'; wc_enqueue_js( " // Update value on change. jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).on( 'change', function() { var slug = jQuery( this ).val(); jQuery( ':input[name=\"filter_" . esc_js( $taxonomy_filter_name ) . "\"]' ).val( slug ); // Submit form on change if standard dropdown. if ( ! jQuery( this ).attr( 'multiple' ) ) { jQuery( this ).closest( 'form' ).trigger( 'submit' ); } }); // Use Select2 enhancement if possible if ( jQuery().selectWoo ) { var wc_layered_nav_select = function() { jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).selectWoo( { placeholder: decodeURIComponent('" . rawurlencode( (string) wp_specialchars_decode( $any_label ) ) . "'), minimumResultsForSearch: 5, width: '100%', allowClear: " . ( $multiple ? 'false' : 'true' ) . ", language: { noResults: function() { return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; } } } ); }; wc_layered_nav_select(); } " ); } return $found; } /** * Count products within certain terms, taking the main WP query into consideration. * * This query allows counts to be generated based on the viewed products, not all products. * * @param array $term_ids Term IDs. * @param string $taxonomy Taxonomy. * @param string $query_type Query Type. * @return array */ protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { return wc_get_container()->get( Filterer::class )->get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ); } /** * Wrapper for WC_Query::get_main_tax_query() to ease unit testing. * * @since 4.4.0 * @return array */ protected function get_main_tax_query() { return WC_Query::get_main_tax_query(); } /** * Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing. * * @since 4.4.0 * @return string */ protected function get_main_search_query_sql() { return WC_Query::get_main_search_query_sql(); } /** * Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing. * * @since 4.4.0 * @return array */ protected function get_main_meta_query() { return WC_Query::get_main_meta_query(); } /** * Show list based layered nav. * * @param array $terms Terms. * @param string $taxonomy Taxonomy. * @param string $query_type Query Type. * @return bool Will nav display? */ protected function layered_nav_list( $terms, $taxonomy, $query_type ) { // List display. echo '<ul class="woocommerce-widget-layered-nav-list">'; $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); $found = false; $base_link = $this->get_current_page_url(); foreach ( $terms as $term ) { $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); $option_is_set = in_array( $term->slug, $current_values, true ); $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; // Skip the term for the current archive. if ( $this->get_current_term_id() === $term->term_id ) { continue; } // Only show options with count > 0. if ( 0 < $count ) { $found = true; } elseif ( 0 === $count && ! $option_is_set ) { continue; } $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); $current_filter = array_map( 'sanitize_title', $current_filter ); if ( ! in_array( $term->slug, $current_filter, true ) ) { $current_filter[] = $term->slug; } $link = remove_query_arg( $filter_name, $base_link ); // Add current filters to URL. foreach ( $current_filter as $key => $value ) { // Exclude query arg for current term archive term. if ( $value === $this->get_current_term_slug() ) { unset( $current_filter[ $key ] ); } // Exclude self so filter can be unset on click. if ( $option_is_set && $value === $term->slug ) { unset( $current_filter[ $key ] ); } } if ( ! empty( $current_filter ) ) { asort( $current_filter ); $link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link ); // Add Query type Arg to URL. if ( 'or' === $query_type && ! ( 1 === count( $current_filter ) && $option_is_set ) ) { $link = add_query_arg( 'query_type_' . wc_attribute_taxonomy_slug( $taxonomy ), 'or', $link ); } $link = str_replace( '%2C', ',', $link ); } if ( $count > 0 || $option_is_set ) { $link = apply_filters( 'woocommerce_layered_nav_link', $link, $term, $taxonomy ); $term_html = '<a rel="nofollow" href="' . esc_url( $link ) . '">' . esc_html( $term->name ) . '</a>'; } else { $link = false; $term_html = '<span>' . esc_html( $term->name ) . '</span>'; } $term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '<span class="count">(' . absint( $count ) . ')</span>', $count, $term ); echo '<li class="woocommerce-widget-layered-nav-list__item wc-layered-nav-term ' . ( $option_is_set ? 'woocommerce-widget-layered-nav-list__item--chosen chosen' : '' ) . '">'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); echo '</li>'; } echo '</ul>'; return $found; } } includes/widgets/class-wc-widget-product-categories.php 0000644 00000021072 15132754524 0017341 0 ustar 00 <?php /** * Product Categories Widget * * @package WooCommerce\Widgets * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Product categories widget class. * * @extends WC_Widget */ class WC_Widget_Product_Categories extends WC_Widget { /** * Category ancestors. * * @var array */ public $cat_ancestors; /** * Current Category. * * @var bool */ public $current_cat; /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_product_categories'; $this->widget_description = __( 'A list or dropdown of product categories.', 'woocommerce' ); $this->widget_id = 'woocommerce_product_categories'; $this->widget_name = __( 'Product Categories', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Product categories', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'orderby' => array( 'type' => 'select', 'std' => 'name', 'label' => __( 'Order by', 'woocommerce' ), 'options' => array( 'order' => __( 'Category order', 'woocommerce' ), 'name' => __( 'Name', 'woocommerce' ), ), ), 'dropdown' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Show as dropdown', 'woocommerce' ), ), 'count' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Show product counts', 'woocommerce' ), ), 'hierarchical' => array( 'type' => 'checkbox', 'std' => 1, 'label' => __( 'Show hierarchy', 'woocommerce' ), ), 'show_children_only' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Only show children of the current category', 'woocommerce' ), ), 'hide_empty' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Hide empty categories', 'woocommerce' ), ), 'max_depth' => array( 'type' => 'text', 'std' => '', 'label' => __( 'Maximum depth', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * @param array $args Widget arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { global $wp_query, $post; $count = isset( $instance['count'] ) ? $instance['count'] : $this->settings['count']['std']; $hierarchical = isset( $instance['hierarchical'] ) ? $instance['hierarchical'] : $this->settings['hierarchical']['std']; $show_children_only = isset( $instance['show_children_only'] ) ? $instance['show_children_only'] : $this->settings['show_children_only']['std']; $dropdown = isset( $instance['dropdown'] ) ? $instance['dropdown'] : $this->settings['dropdown']['std']; $orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : $this->settings['orderby']['std']; $hide_empty = isset( $instance['hide_empty'] ) ? $instance['hide_empty'] : $this->settings['hide_empty']['std']; $dropdown_args = array( 'hide_empty' => $hide_empty, ); $list_args = array( 'show_count' => $count, 'hierarchical' => $hierarchical, 'taxonomy' => 'product_cat', 'hide_empty' => $hide_empty, ); $max_depth = absint( isset( $instance['max_depth'] ) ? $instance['max_depth'] : $this->settings['max_depth']['std'] ); $list_args['menu_order'] = false; $dropdown_args['depth'] = $max_depth; $list_args['depth'] = $max_depth; if ( 'order' === $orderby ) { $list_args['orderby'] = 'meta_value_num'; $dropdown_args['orderby'] = 'meta_value_num'; $list_args['meta_key'] = 'order'; $dropdown_args['meta_key'] = 'order'; } $this->current_cat = false; $this->cat_ancestors = array(); if ( is_tax( 'product_cat' ) ) { $this->current_cat = $wp_query->queried_object; $this->cat_ancestors = get_ancestors( $this->current_cat->term_id, 'product_cat' ); } elseif ( is_singular( 'product' ) ) { $terms = wc_get_product_terms( $post->ID, 'product_cat', apply_filters( 'woocommerce_product_categories_widget_product_terms_args', array( 'orderby' => 'parent', 'order' => 'DESC', ) ) ); if ( $terms ) { $main_term = apply_filters( 'woocommerce_product_categories_widget_main_term', $terms[0], $terms ); $this->current_cat = $main_term; $this->cat_ancestors = get_ancestors( $main_term->term_id, 'product_cat' ); } } // Show Siblings and Children Only. if ( $show_children_only && $this->current_cat ) { if ( $hierarchical ) { $include = array_merge( $this->cat_ancestors, array( $this->current_cat->term_id ), get_terms( 'product_cat', array( 'fields' => 'ids', 'parent' => 0, 'hierarchical' => true, 'hide_empty' => false, ) ), get_terms( 'product_cat', array( 'fields' => 'ids', 'parent' => $this->current_cat->term_id, 'hierarchical' => true, 'hide_empty' => false, ) ) ); // Gather siblings of ancestors. if ( $this->cat_ancestors ) { foreach ( $this->cat_ancestors as $ancestor ) { $include = array_merge( $include, get_terms( 'product_cat', array( 'fields' => 'ids', 'parent' => $ancestor, 'hierarchical' => false, 'hide_empty' => false, ) ) ); } } } else { // Direct children. $include = get_terms( 'product_cat', array( 'fields' => 'ids', 'parent' => $this->current_cat->term_id, 'hierarchical' => true, 'hide_empty' => false, ) ); } $list_args['include'] = implode( ',', $include ); $dropdown_args['include'] = $list_args['include']; if ( empty( $include ) ) { return; } } elseif ( $show_children_only ) { $dropdown_args['depth'] = 1; $dropdown_args['child_of'] = 0; $dropdown_args['hierarchical'] = 1; $list_args['depth'] = 1; $list_args['child_of'] = 0; $list_args['hierarchical'] = 1; } $this->widget_start( $args, $instance ); if ( $dropdown ) { wc_product_dropdown_categories( apply_filters( 'woocommerce_product_categories_widget_dropdown_args', wp_parse_args( $dropdown_args, array( 'show_count' => $count, 'hierarchical' => $hierarchical, 'show_uncategorized' => 0, 'selected' => $this->current_cat ? $this->current_cat->slug : '', ) ) ) ); wp_enqueue_script( 'selectWoo' ); wp_enqueue_style( 'select2' ); wc_enqueue_js( " jQuery( '.dropdown_product_cat' ).on( 'change', function() { if ( jQuery(this).val() != '' ) { var this_page = ''; var home_url = '" . esc_js( home_url( '/' ) ) . "'; if ( home_url.indexOf( '?' ) > 0 ) { this_page = home_url + '&product_cat=' + jQuery(this).val(); } else { this_page = home_url + '?product_cat=' + jQuery(this).val(); } location.href = this_page; } else { location.href = '" . esc_js( wc_get_page_permalink( 'shop' ) ) . "'; } }); if ( jQuery().selectWoo ) { var wc_product_cat_select = function() { jQuery( '.dropdown_product_cat' ).selectWoo( { placeholder: '" . esc_js( __( 'Select a category', 'woocommerce' ) ) . "', minimumResultsForSearch: 5, width: '100%', allowClear: true, language: { noResults: function() { return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; } } } ); }; wc_product_cat_select(); } " ); } else { include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-list-walker.php'; $list_args['walker'] = new WC_Product_Cat_List_Walker(); $list_args['title_li'] = ''; $list_args['pad_counts'] = 1; $list_args['show_option_none'] = __( 'No product categories exist.', 'woocommerce' ); $list_args['current_category'] = ( $this->current_cat ) ? $this->current_cat->term_id : ''; $list_args['current_category_ancestors'] = $this->cat_ancestors; $list_args['max_depth'] = $max_depth; echo '<ul class="product-categories">'; wp_list_categories( apply_filters( 'woocommerce_product_categories_widget_args', $list_args ) ); echo '</ul>'; } $this->widget_end( $args ); } } includes/widgets/class-wc-widget-rating-filter.php 0000644 00000010545 15132754524 0016310 0 ustar 00 <?php /** * Rating Filter Widget and related functions. * * @package WooCommerce\Widgets * @version 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * Widget rating filter class. */ class WC_Widget_Rating_Filter extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_rating_filter'; $this->widget_description = __( 'Display a list of star ratings to filter products in your store.', 'woocommerce' ); $this->widget_id = 'woocommerce_rating_filter'; $this->widget_name = __( 'Filter Products by Rating', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Average rating', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), ); parent::__construct(); } /** * Count products after other filters have occurred by adjusting the main query. * * @param int $rating Rating. * @return int */ protected function get_filtered_product_count( $rating ) { global $wpdb; $tax_query = WC_Query::get_main_tax_query(); $meta_query = WC_Query::get_main_meta_query(); // Unset current rating filter. foreach ( $tax_query as $key => $query ) { if ( ! empty( $query['rating_filter'] ) ) { unset( $tax_query[ $key ] ); break; } } // Set new rating filter. $product_visibility_terms = wc_get_product_visibility_term_ids(); $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_terms[ 'rated-' . $rating ], 'operator' => 'IN', 'rating_filter' => true, ); $meta_query = new WP_Meta_Query( $meta_query ); $tax_query = new WP_Tax_Query( $tax_query ); $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); $sql = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) FROM {$wpdb->posts} "; $sql .= $tax_query_sql['join'] . $meta_query_sql['join']; $sql .= " WHERE {$wpdb->posts}.post_type = 'product' AND {$wpdb->posts}.post_status = 'publish' "; $sql .= $tax_query_sql['where'] . $meta_query_sql['where']; $search = WC_Query::get_main_search_query_sql(); if ( $search ) { $sql .= ' AND ' . $search; } return absint( $wpdb->get_var( $sql ) ); // WPCS: unprepared SQL ok. } /** * Widget function. * * @see WP_Widget * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { if ( ! is_shop() && ! is_product_taxonomy() ) { return; } if ( ! WC()->query->get_main_query()->post_count ) { return; } ob_start(); $found = false; $rating_filter = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: input var ok, CSRF ok, sanitization ok. $base_link = remove_query_arg( 'paged', $this->get_current_page_url() ); $this->widget_start( $args, $instance ); echo '<ul>'; for ( $rating = 5; $rating >= 1; $rating-- ) { $count = $this->get_filtered_product_count( $rating ); if ( empty( $count ) ) { continue; } $found = true; $link = $base_link; if ( in_array( $rating, $rating_filter, true ) ) { $link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) ); } else { $link_ratings = implode( ',', array_merge( $rating_filter, array( $rating ) ) ); } $class = in_array( $rating, $rating_filter, true ) ? 'wc-layered-nav-rating chosen' : 'wc-layered-nav-rating'; $link = apply_filters( 'woocommerce_rating_filter_link', $link_ratings ? add_query_arg( 'rating_filter', $link_ratings, $link ) : remove_query_arg( 'rating_filter' ) ); $rating_html = wc_get_star_rating_html( $rating ); $count_html = wp_kses( apply_filters( 'woocommerce_rating_filter_count', "({$count})", $count, $rating ), array( 'em' => array(), 'span' => array(), 'strong' => array(), ) ); printf( '<li class="%s"><a href="%s"><span class="star-rating">%s</span> %s</a></li>', esc_attr( $class ), esc_url( $link ), $rating_html, $count_html ); // WPCS: XSS ok. } echo '</ul>'; $this->widget_end( $args ); if ( ! $found ) { ob_end_clean(); } else { echo ob_get_clean(); // WPCS: XSS ok. } } } includes/widgets/class-wc-widget-layered-nav-filters.php 0000644 00000010765 15132754524 0017422 0 ustar 00 <?php /** * Layered Navigation Filters Widget. * * @package WooCommerce\Widgets * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget layered nav filters. */ class WC_Widget_Layered_Nav_Filters extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_layered_nav_filters'; $this->widget_description = __( 'Display a list of active product filters.', 'woocommerce' ); $this->widget_id = 'woocommerce_layered_nav_filters'; $this->widget_name = __( 'Active Product Filters', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Active filters', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { if ( ! is_shop() && ! is_product_taxonomy() ) { return; } $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); $min_price = isset( $_GET['min_price'] ) ? wc_clean( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok. $max_price = isset( $_GET['max_price'] ) ? wc_clean( wp_unslash( $_GET['max_price'] ) ) : 0; // WPCS: input var ok, CSRF ok. $rating_filter = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: sanitization ok, input var ok, CSRF ok. $base_link = $this->get_current_page_url(); if ( 0 < count( $_chosen_attributes ) || 0 < $min_price || 0 < $max_price || ! empty( $rating_filter ) ) { $this->widget_start( $args, $instance ); echo '<ul>'; // Attributes. if ( ! empty( $_chosen_attributes ) ) { foreach ( $_chosen_attributes as $taxonomy => $data ) { foreach ( $data['terms'] as $term_slug ) { $term = get_term_by( 'slug', $term_slug, $taxonomy ); if ( ! $term ) { continue; } $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // WPCS: input var ok, CSRF ok. $current_filter = array_map( 'sanitize_title', $current_filter ); $new_filter = array_diff( $current_filter, array( $term_slug ) ); $link = remove_query_arg( array( 'add-to-cart', $filter_name ), $base_link ); if ( count( $new_filter ) > 0 ) { $link = add_query_arg( $filter_name, implode( ',', $new_filter ), $link ); } $filter_classes = array( 'chosen', 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) ), 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) . '-' . $term_slug ) ); echo '<li class="' . esc_attr( implode( ' ', $filter_classes ) ) . '"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . esc_html( $term->name ) . '</a></li>'; } } } if ( $min_price ) { $link = remove_query_arg( 'min_price', $base_link ); /* translators: %s: minimum price */ echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( __( 'Min %s', 'woocommerce' ), wc_price( $min_price ) ) . '</a></li>'; // WPCS: XSS ok. } if ( $max_price ) { $link = remove_query_arg( 'max_price', $base_link ); /* translators: %s: maximum price */ echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( __( 'Max %s', 'woocommerce' ), wc_price( $max_price ) ) . '</a></li>'; // WPCS: XSS ok. } if ( ! empty( $rating_filter ) ) { foreach ( $rating_filter as $rating ) { $link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) ); $link = $link_ratings ? add_query_arg( 'rating_filter', $link_ratings ) : remove_query_arg( 'rating_filter', $base_link ); /* translators: %s: rating */ echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '</a></li>'; } } echo '</ul>'; $this->widget_end( $args ); } } } includes/widgets/class-wc-widget-cart.php 0000644 00000003443 15132754524 0014471 0 ustar 00 <?php /** * Shopping Cart Widget. * * Displays shopping cart widget. * * @package WooCommerce\Widgets * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget cart class. */ class WC_Widget_Cart extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_shopping_cart'; $this->widget_description = __( 'Display the customer shopping cart.', 'woocommerce' ); $this->widget_id = 'woocommerce_widget_cart'; $this->widget_name = __( 'Cart', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Cart', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'hide_if_empty' => array( 'type' => 'checkbox', 'std' => 0, 'label' => __( 'Hide if cart is empty', 'woocommerce' ), ), ); if ( is_customize_preview() ) { wp_enqueue_script( 'wc-cart-fragments' ); } parent::__construct(); } /** * Output widget. * * @see WP_Widget * * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { if ( apply_filters( 'woocommerce_widget_cart_is_hidden', is_cart() || is_checkout() ) ) { return; } $hide_if_empty = empty( $instance['hide_if_empty'] ) ? 0 : 1; if ( ! isset( $instance['title'] ) ) { $instance['title'] = __( 'Cart', 'woocommerce' ); } $this->widget_start( $args, $instance ); if ( $hide_if_empty ) { echo '<div class="hide_cart_widget_if_empty">'; } // Insert cart widget placeholder - code in woocommerce.js will update this on page load. echo '<div class="widget_shopping_cart_content"></div>'; if ( $hide_if_empty ) { echo '</div>'; } $this->widget_end( $args ); } } includes/widgets/class-wc-widget-top-rated-products.php 0000644 00000005221 15132754524 0017274 0 ustar 00 <?php /** * Top Rated Products Widget. * Gets and displays top rated products in an unordered list. * * @package WooCommerce\Widgets * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget top rated products class. */ class WC_Widget_Top_Rated_Products extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_top_rated_products'; $this->widget_description = __( "A list of your store's top-rated products.", 'woocommerce' ); $this->widget_id = 'woocommerce_top_rated_products'; $this->widget_name = __( 'Products by Rating list', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => __( 'Top rated products', 'woocommerce' ), 'label' => __( 'Title', 'woocommerce' ), ), 'number' => array( 'type' => 'number', 'step' => 1, 'min' => 1, 'max' => '', 'std' => 5, 'label' => __( 'Number of products to show', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { if ( $this->get_cached_widget( $args ) ) { return; } ob_start(); $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; $query_args = apply_filters( 'woocommerce_top_rated_products_widget_args', array( 'posts_per_page' => $number, 'no_found_rows' => 1, 'post_status' => 'publish', 'post_type' => 'product', 'meta_key' => '_wc_average_rating', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'meta_query' => WC()->query->get_meta_query(), 'tax_query' => WC()->query->get_tax_query(), ) ); // WPCS: slow query ok. $r = new WP_Query( $query_args ); if ( $r->have_posts() ) { $this->widget_start( $args, $instance ); echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) ); $template_args = array( 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, 'show_rating' => true, ); while ( $r->have_posts() ) { $r->the_post(); wc_get_template( 'content-widget-product.php', $template_args ); } echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) ); $this->widget_end( $args ); } wp_reset_postdata(); $content = ob_get_clean(); echo $content; // WPCS: XSS ok. $this->cache_widget( $args, $content ); } } includes/widgets/class-wc-widget-product-search.php 0000644 00000002014 15132754524 0016454 0 ustar 00 <?php /** * Product Search Widget. * * @package WooCommerce\Widgets * @version 2.3.0 */ defined( 'ABSPATH' ) || exit; /** * Widget product search class. */ class WC_Widget_Product_Search extends WC_Widget { /** * Constructor. */ public function __construct() { $this->widget_cssclass = 'woocommerce widget_product_search'; $this->widget_description = __( 'A search form for your store.', 'woocommerce' ); $this->widget_id = 'woocommerce_product_search'; $this->widget_name = __( 'Product Search', 'woocommerce' ); $this->settings = array( 'title' => array( 'type' => 'text', 'std' => '', 'label' => __( 'Title', 'woocommerce' ), ), ); parent::__construct(); } /** * Output widget. * * @see WP_Widget * * @param array $args Arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { $this->widget_start( $args, $instance ); get_product_search_form(); $this->widget_end( $args ); } } includes/class-wc-shipping.php 0000644 00000026525 15132754524 0012440 0 ustar 00 <?php /** * WooCommerce Shipping * * Handles shipping and loads shipping methods via hooks. * * @version 2.6.0 * @package WooCommerce\Classes\Shipping */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Shipping class. */ class WC_Shipping { /** * True if shipping is enabled. * * @var bool */ public $enabled = false; /** * Stores methods loaded into woocommerce. * * @var array|null */ public $shipping_methods = null; /** * Stores the shipping classes. * * @var array */ public $shipping_classes = array(); /** * Stores packages to ship and to get quotes for. * * @var array */ public $packages = array(); /** * The single instance of the class * * @var WC_Shipping * @since 2.1 */ protected static $_instance = null; /** * Main WC_Shipping Instance. * * Ensures only one instance of WC_Shipping is loaded or can be loaded. * * @since 2.1 * @return WC_Shipping Main instance */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Cloning is forbidden. * * @since 2.1 */ public function __clone() { wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. * * @since 2.1 */ public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' ); } /** * Magic getter. * * @param string $name Property name. * @return mixed */ public function __get( $name ) { // Grab from cart for backwards compatibility with versions prior to 3.2. if ( 'shipping_total' === $name ) { return WC()->cart->get_shipping_total(); } if ( 'shipping_taxes' === $name ) { return WC()->cart->get_shipping_taxes(); } } /** * Initialize shipping. */ public function __construct() { $this->enabled = wc_shipping_enabled(); if ( $this->enabled ) { $this->init(); } } /** * Initialize shipping. */ public function init() { do_action( 'woocommerce_shipping_init' ); } /** * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. * * @return array */ public function get_shipping_method_class_names() { // Unique Method ID => Method Class name. $shipping_methods = array( 'flat_rate' => 'WC_Shipping_Flat_Rate', 'free_shipping' => 'WC_Shipping_Free_Shipping', 'local_pickup' => 'WC_Shipping_Local_Pickup', ); // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here. $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); foreach ( $maybe_load_legacy_methods as $method ) { $options = get_option( 'woocommerce_' . $method . '_settings' ); if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; } } return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); } /** * Loads all shipping methods which are hooked in. * If a $package is passed, some methods may add themselves conditionally and zones will be used. * * @param array $package Package information. * @return WC_Shipping_Method[] */ public function load_shipping_methods( $package = array() ) { if ( ! empty( $package ) ) { $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); // translators: %s: shipping zone name. $matched_zone_notice = sprintf( __( 'Customer matched zone "%s"', 'woocommerce' ), $shipping_zone->get_zone_name() ); // Debug output. if ( $debug_mode && ! Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Constants::is_defined( 'WC_DOING_AJAX' ) && ! wc_has_notice( $matched_zone_notice ) ) { wc_add_notice( $matched_zone_notice ); } } else { $this->shipping_methods = array(); } // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { $this->register_shipping_method( $method_class ); } // Methods can register themselves manually through this hook if necessary. do_action( 'woocommerce_load_shipping_methods', $package ); // Return loaded methods. return $this->get_shipping_methods(); } /** * Register a shipping method. * * @param object|string $method Either the name of the method's class, or an instance of the method's class. * * @return bool|void */ public function register_shipping_method( $method ) { if ( ! is_object( $method ) ) { if ( ! class_exists( $method ) ) { return false; } $method = new $method(); } if ( is_null( $this->shipping_methods ) ) { $this->shipping_methods = array(); } $this->shipping_methods[ $method->id ] = $method; } /** * Unregister shipping methods. */ public function unregister_shipping_methods() { $this->shipping_methods = null; } /** * Returns all registered shipping methods for usage. * * @return WC_Shipping_Method[] */ public function get_shipping_methods() { if ( is_null( $this->shipping_methods ) ) { $this->load_shipping_methods(); } return $this->shipping_methods; } /** * Get an array of shipping classes. * * @return array */ public function get_shipping_classes() { if ( empty( $this->shipping_classes ) ) { $classes = get_terms( 'product_shipping_class', array( 'hide_empty' => '0', 'orderby' => 'name', ) ); $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); } return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); } /** * Calculate shipping for (multiple) packages of cart items. * * @param array $packages multi-dimensional array of cart items to calc shipping for. * @return array Array of calculated packages. */ public function calculate_shipping( $packages = array() ) { $this->packages = array(); if ( ! $this->enabled || empty( $packages ) ) { return array(); } // Calculate costs for passed packages. foreach ( $packages as $package_key => $package ) { $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); } /** * Allow packages to be reorganized after calculating the shipping. * * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages * but before WooCommerce does anything with them. A good example of usage is to merge the shipping methods for multiple * packages for marketplaces. * * @since 2.6.0 * * @param array $packages The array of packages after shipping costs are calculated. */ $this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) ); return $this->packages; } /** * See if package is shippable. * * Packages are shippable until proven otherwise e.g. after getting a shipping country. * * @param array $package Package of cart items. * @return bool */ public function is_package_shippable( $package ) { // Packages are shippable until proven otherwise. if ( empty( $package['destination']['country'] ) ) { return true; } $allowed = array_keys( WC()->countries->get_shipping_countries() ); return in_array( $package['destination']['country'], $allowed, true ); } /** * Calculate shipping rates for a package, * * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. * * @param array $package Package of cart items. * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. * * @return array|bool */ public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { // If shipping is disabled or the package is invalid, return false. if ( ! $this->enabled || empty( $package ) ) { return false; } $package['rates'] = array(); // If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates. if ( ! $this->is_package_shippable( $package ) ) { return $package; } // Check if we need to recalculate shipping for this package. $package_to_hash = $package; // Remove data objects so hashes are consistent. foreach ( $package_to_hash['contents'] as $item_id => $item ) { unset( $package_to_hash['contents'][ $item_id ]['data'] ); } // Get rates stored in the WC session data for this package. $wc_session_key = 'shipping_for_package_' . $package_key; $stored_rates = WC()->session->get( $wc_session_key ); // Calculate the hash for this package so we can tell if it's changed since last calculation. $package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) { /** * Fires before getting shipping rates for a package. * * @since 4.3.0 * @param array $package Package of cart items. * @param WC_Shipping_Method $shipping_method Shipping method instance. */ do_action( 'woocommerce_before_get_rates_for_package', $package, $shipping_method ); // Use + instead of array_merge to maintain numeric keys. $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); /** * Fires after getting shipping rates for a package. * * @since 4.3.0 * @param array $package Package of cart items. * @param WC_Shipping_Method $shipping_method Shipping method instance. */ do_action( 'woocommerce_after_get_rates_for_package', $package, $shipping_method ); } } // Filter the calculated rates. $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); // Store in session to avoid recalculation. WC()->session->set( $wc_session_key, array( 'package_hash' => $package_hash, 'rates' => $package['rates'], ) ); } else { $package['rates'] = $stored_rates['rates']; } return $package; } /** * Get packages. * * @return array */ public function get_packages() { return $this->packages; } /** * Reset shipping. * * Reset the totals for shipping as a whole. */ public function reset_shipping() { unset( WC()->session->chosen_shipping_methods ); $this->packages = array(); } /** * Deprecated * * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. */ public function sort_shipping_methods() { wc_deprecated_function( 'sort_shipping_methods', '2.6' ); return $this->shipping_methods; } } includes/class-wc-deprecated-filter-hooks.php 0000644 00000015515 15132754524 0015320 0 ustar 00 <?php /** * Deprecated filter hooks * * @package WooCommerce\Abstracts * @since 3.0.0 * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Handles deprecation notices and triggering of legacy filter hooks */ class WC_Deprecated_Filter_Hooks extends WC_Deprecated_Hooks { /** * Array of deprecated hooks we need to handle. * Format of 'new' => 'old'. * * @var array */ protected $deprecated_hooks = array( 'woocommerce_structured_data_order' => 'woocommerce_email_order_schema_markup', 'woocommerce_add_to_cart_fragments' => 'add_to_cart_fragments', 'woocommerce_add_to_cart_redirect' => 'add_to_cart_redirect', 'woocommerce_product_get_width' => 'woocommerce_product_width', 'woocommerce_product_get_height' => 'woocommerce_product_height', 'woocommerce_product_get_length' => 'woocommerce_product_length', 'woocommerce_product_get_weight' => 'woocommerce_product_weight', 'woocommerce_product_get_sku' => 'woocommerce_get_sku', 'woocommerce_product_get_price' => 'woocommerce_get_price', 'woocommerce_product_get_regular_price' => 'woocommerce_get_regular_price', 'woocommerce_product_get_sale_price' => 'woocommerce_get_sale_price', 'woocommerce_product_get_tax_class' => 'woocommerce_product_tax_class', 'woocommerce_product_get_stock_quantity' => 'woocommerce_get_stock_quantity', 'woocommerce_product_get_attributes' => 'woocommerce_get_product_attributes', 'woocommerce_product_get_gallery_image_ids' => 'woocommerce_product_gallery_attachment_ids', 'woocommerce_product_get_review_count' => 'woocommerce_product_review_count', 'woocommerce_product_get_downloads' => 'woocommerce_product_files', 'woocommerce_order_get_currency' => 'woocommerce_get_currency', 'woocommerce_order_get_discount_total' => 'woocommerce_order_amount_discount_total', 'woocommerce_order_get_discount_tax' => 'woocommerce_order_amount_discount_tax', 'woocommerce_order_get_shipping_total' => 'woocommerce_order_amount_shipping_total', 'woocommerce_order_get_shipping_tax' => 'woocommerce_order_amount_shipping_tax', 'woocommerce_order_get_cart_tax' => 'woocommerce_order_amount_cart_tax', 'woocommerce_order_get_total' => 'woocommerce_order_amount_total', 'woocommerce_order_get_total_tax' => 'woocommerce_order_amount_total_tax', 'woocommerce_order_get_total_discount' => 'woocommerce_order_amount_total_discount', 'woocommerce_order_get_subtotal' => 'woocommerce_order_amount_subtotal', 'woocommerce_order_get_tax_totals' => 'woocommerce_order_tax_totals', 'woocommerce_get_order_refund_get_amount' => 'woocommerce_refund_amount', 'woocommerce_get_order_refund_get_reason' => 'woocommerce_refund_reason', 'default_checkout_billing_country' => 'default_checkout_country', 'default_checkout_billing_state' => 'default_checkout_state', 'default_checkout_billing_postcode' => 'default_checkout_postcode', 'woocommerce_system_status_environment_rows' => 'woocommerce_debug_posting', 'woocommerce_credit_card_type_labels' => 'wocommerce_credit_card_type_labels', 'woocommerce_settings_tabs_advanced' => 'woocommerce_settings_tabs_api', 'woocommerce_settings_advanced' => 'woocommerce_settings_api', ); /** * Array of versions on each hook has been deprecated. * * @var array */ protected $deprecated_version = array( 'woocommerce_email_order_schema_markup' => '3.0.0', 'add_to_cart_fragments' => '3.0.0', 'add_to_cart_redirect' => '3.0.0', 'woocommerce_product_width' => '3.0.0', 'woocommerce_product_height' => '3.0.0', 'woocommerce_product_length' => '3.0.0', 'woocommerce_product_weight' => '3.0.0', 'woocommerce_get_sku' => '3.0.0', 'woocommerce_get_price' => '3.0.0', 'woocommerce_get_regular_price' => '3.0.0', 'woocommerce_get_sale_price' => '3.0.0', 'woocommerce_product_tax_class' => '3.0.0', 'woocommerce_get_stock_quantity' => '3.0.0', 'woocommerce_get_product_attributes' => '3.0.0', 'woocommerce_product_gallery_attachment_ids' => '3.0.0', 'woocommerce_product_review_count' => '3.0.0', 'woocommerce_product_files' => '3.0.0', 'woocommerce_get_currency' => '3.0.0', 'woocommerce_order_amount_discount_total' => '3.0.0', 'woocommerce_order_amount_discount_tax' => '3.0.0', 'woocommerce_order_amount_shipping_total' => '3.0.0', 'woocommerce_order_amount_shipping_tax' => '3.0.0', 'woocommerce_order_amount_cart_tax' => '3.0.0', 'woocommerce_order_amount_total' => '3.0.0', 'woocommerce_order_amount_total_tax' => '3.0.0', 'woocommerce_order_amount_total_discount' => '3.0.0', 'woocommerce_order_amount_subtotal' => '3.0.0', 'woocommerce_order_tax_totals' => '3.0.0', 'woocommerce_refund_amount' => '3.0.0', 'woocommerce_refund_reason' => '3.0.0', 'default_checkout_country' => '3.0.0', 'default_checkout_state' => '3.0.0', 'default_checkout_postcode' => '3.0.0', 'woocommerce_debug_posting' => '3.0.0', 'wocommerce_credit_card_type_labels' => '3.0.0', 'woocommerce_settings_tabs_api' => '3.4.0', 'woocommerce_settings_api' => '3.4.0', ); /** * Hook into the new hook so we can handle deprecated hooks once fired. * * @param string $hook_name Hook name. */ public function hook_in( $hook_name ) { add_filter( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); } /** * If the old hook is in-use, trigger it. * * @param string $new_hook New hook name. * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @param mixed $return_value Returned value. * @return mixed */ public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { if ( has_filter( $old_hook ) ) { $this->display_notice( $old_hook, $new_hook ); $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); } return $return_value; } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed */ protected function trigger_hook( $old_hook, $new_callback_args ) { return apply_filters_ref_array( $old_hook, $new_callback_args ); } } includes/import/class-wc-product-csv-importer.php 0000644 00000075755 15132754524 0016252 0 ustar 00 <?php /** * WooCommerce Product CSV importer * * @package WooCommerce\Import * @version 3.1.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Include dependencies. */ if ( ! class_exists( 'WC_Product_Importer', false ) ) { include_once dirname( __FILE__ ) . '/abstract-wc-product-importer.php'; } if ( ! class_exists( 'WC_Product_CSV_Importer_Controller', false ) ) { include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php'; } /** * WC_Product_CSV_Importer Class. */ class WC_Product_CSV_Importer extends WC_Product_Importer { /** * Tracks current row being parsed. * * @var integer */ protected $parsing_raw_data_index = 0; /** * Initialize importer. * * @param string $file File to read. * @param array $params Arguments for the parser. */ public function __construct( $file, $params = array() ) { $default_args = array( 'start_pos' => 0, // File pointer start. 'end_pos' => -1, // File pointer end. 'lines' => -1, // Max lines to read. 'mapping' => array(), // Column mapping. csv_heading => schema_heading. 'parse' => false, // Whether to sanitize and format data. 'update_existing' => false, // Whether to update existing items. 'delimiter' => ',', // CSV delimiter. 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit. 'enclosure' => '"', // The character used to wrap text in the CSV. 'escape' => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character. ); $this->params = wp_parse_args( $params, $default_args ); $this->file = $file; if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) { $this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] ); } // Import mappings for CSV data. include_once dirname( dirname( __FILE__ ) ) . '/admin/importers/mappings/mappings.php'; $this->read_file(); } /** * Read file. */ protected function read_file() { if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) { wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); } $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine. if ( false !== $handle ) { $this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine // Remove BOM signature from the first item. if ( isset( $this->raw_keys[0] ) ) { $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); } if ( 0 !== $this->params['start_pos'] ) { fseek( $handle, (int) $this->params['start_pos'] ); } while ( 1 ) { $row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine if ( false !== $row ) { $this->raw_data[] = $row; $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { break; } } else { break; } } $this->file_position = ftell( $handle ); } if ( ! empty( $this->params['mapping'] ) ) { $this->set_mapped_keys(); } if ( $this->params['parse'] ) { $this->set_parsed_data(); } } /** * Remove UTF-8 BOM signature. * * @param string $string String to handle. * * @return string */ protected function remove_utf8_bom( $string ) { if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) { $string = substr( $string, 3 ); } return $string; } /** * Set file mapped keys. */ protected function set_mapped_keys() { $mapping = $this->params['mapping']; foreach ( $this->raw_keys as $key ) { $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; } } /** * Parse relative field and return product ID. * * Handles `id:xx` and SKUs. * * If mapping to an id: and the product ID does not exist, this link is not * valid. * * If mapping to a SKU and the product ID does not exist, a temporary object * will be created so it can be updated later. * * @param string $value Field value. * * @return int|string */ public function parse_relative_field( $value ) { global $wpdb; if ( empty( $value ) ) { return ''; } // IDs are prefixed with id:. if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) { $id = intval( $matches[1] ); // If original_id is found, use that instead of the given ID since a new placeholder must have been created already. $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. if ( $original_id ) { return absint( $original_id ); } // See if the given ID maps to a valid product allready. $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok. if ( $existing_id ) { return absint( $existing_id ); } // If we're not updating existing posts, we may need a placeholder product to map to. if ( ! $this->params['update_existing'] ) { $product = wc_get_product_object( 'simple' ); $product->set_name( 'Import placeholder for ' . $id ); $product->set_status( 'importing' ); $product->add_meta_data( '_original_id', $id, true ); $id = $product->save(); } return $id; } $id = wc_get_product_id_by_sku( $value ); if ( $id ) { return $id; } try { $product = wc_get_product_object( 'simple' ); $product->set_name( 'Import placeholder for ' . $value ); $product->set_status( 'importing' ); $product->set_sku( $value ); $id = $product->save(); if ( $id && ! is_wp_error( $id ) ) { return $id; } } catch ( Exception $e ) { return ''; } return ''; } /** * Parse the ID field. * * If we're not doing an update, create a placeholder product so mapping works * for rows following this one. * * @param string $value Field value. * * @return int */ public function parse_id_field( $value ) { global $wpdb; $id = absint( $value ); if ( ! $id ) { return 0; } // See if this maps to an ID placeholder already. $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. if ( $original_id ) { return absint( $original_id ); } // Not updating? Make sure we have a new placeholder for this ID. if ( ! $this->params['update_existing'] ) { $mapped_keys = $this->get_mapped_keys(); $sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) ); $row_sku = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : ''; $id_from_sku = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : ''; // If row has a SKU, make sure placeholder was not made already. if ( $id_from_sku ) { return $id_from_sku; } $product = wc_get_product_object( 'simple' ); $product->set_name( 'Import placeholder for ' . $id ); $product->set_status( 'importing' ); $product->add_meta_data( '_original_id', $id, true ); // If row has a SKU, make sure placeholder has it too. if ( $row_sku ) { $product->set_sku( $row_sku ); } $id = $product->save(); } return $id && ! is_wp_error( $id ) ? $id : 0; } /** * Parse relative comma-delineated field and return product ID. * * @param string $value Field value. * * @return array */ public function parse_relative_comma_field( $value ) { if ( empty( $value ) ) { return array(); } return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) ); } /** * Parse a comma-delineated field from a CSV. * * @param string $value Field value. * * @return array */ public function parse_comma_field( $value ) { if ( empty( $value ) && '0' !== $value ) { return array(); } $value = $this->unescape_data( $value ); return array_map( 'wc_clean', $this->explode_values( $value ) ); } /** * Parse a field that is generally '1' or '0' but can be something else. * * @param string $value Field value. * * @return bool|string */ public function parse_bool_field( $value ) { if ( '0' === $value ) { return false; } if ( '1' === $value ) { return true; } // Don't return explicit true or false for empty fields or values like 'notify'. return wc_clean( $value ); } /** * Parse a float value field. * * @param string $value Field value. * * @return float|string */ public function parse_float_field( $value ) { if ( '' === $value ) { return $value; } // Remove the ' prepended to fields that start with - if needed. $value = $this->unescape_data( $value ); return floatval( $value ); } /** * Parse the stock qty field. * * @param string $value Field value. * * @return float|string */ public function parse_stock_quantity_field( $value ) { if ( '' === $value ) { return $value; } // Remove the ' prepended to fields that start with - if needed. $value = $this->unescape_data( $value ); return wc_stock_amount( $value ); } /** * Parse the tax status field. * * @param string $value Field value. * * @return string */ public function parse_tax_status_field( $value ) { if ( '' === $value ) { return $value; } // Remove the ' prepended to fields that start with - if needed. $value = $this->unescape_data( $value ); if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { $value = wc_string_to_bool( $value ) ? 'taxable' : 'none'; } return wc_clean( $value ); } /** * Parse a category field from a CSV. * Categories are separated by commas and subcategories are "parent > subcategory". * * @param string $value Field value. * * @return array of arrays with "parent" and "name" keys. */ public function parse_categories_field( $value ) { if ( empty( $value ) ) { return array(); } $row_terms = $this->explode_values( $value ); $categories = array(); foreach ( $row_terms as $row_term ) { $parent = null; $_terms = array_map( 'trim', explode( '>', $row_term ) ); $total = count( $_terms ); foreach ( $_terms as $index => $_term ) { // Don't allow users without capabilities to create new categories. if ( ! current_user_can( 'manage_product_terms' ) ) { break; } $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) ); if ( is_wp_error( $term ) ) { if ( $term->get_error_code() === 'term_exists' ) { // When term exists, error data should contain existing term id. $term_id = $term->get_error_data(); } else { break; // We cannot continue on any other error. } } else { // New term. $term_id = $term['term_id']; } // Only requires assign the last category. if ( ( 1 + $index ) === $total ) { $categories[] = $term_id; } else { // Store parent to be able to insert or query categories based in parent ID. $parent = $term_id; } } } return $categories; } /** * Parse a tag field from a CSV. * * @param string $value Field value. * * @return array */ public function parse_tags_field( $value ) { if ( empty( $value ) ) { return array(); } $value = $this->unescape_data( $value ); $names = $this->explode_values( $value ); $tags = array(); foreach ( $names as $name ) { $term = get_term_by( 'name', $name, 'product_tag' ); if ( ! $term || is_wp_error( $term ) ) { $term = (object) wp_insert_term( $name, 'product_tag' ); } if ( ! is_wp_error( $term ) ) { $tags[] = $term->term_id; } } return $tags; } /** * Parse a tag field from a CSV with space separators. * * @param string $value Field value. * * @return array */ public function parse_tags_spaces_field( $value ) { if ( empty( $value ) ) { return array(); } $value = $this->unescape_data( $value ); $names = $this->explode_values( $value, ' ' ); $tags = array(); foreach ( $names as $name ) { $term = get_term_by( 'name', $name, 'product_tag' ); if ( ! $term || is_wp_error( $term ) ) { $term = (object) wp_insert_term( $name, 'product_tag' ); } if ( ! is_wp_error( $term ) ) { $tags[] = $term->term_id; } } return $tags; } /** * Parse a shipping class field from a CSV. * * @param string $value Field value. * * @return int */ public function parse_shipping_class_field( $value ) { if ( empty( $value ) ) { return 0; } $term = get_term_by( 'name', $value, 'product_shipping_class' ); if ( ! $term || is_wp_error( $term ) ) { $term = (object) wp_insert_term( $value, 'product_shipping_class' ); } if ( is_wp_error( $term ) ) { return 0; } return $term->term_id; } /** * Parse images list from a CSV. Images can be filenames or URLs. * * @param string $value Field value. * * @return array */ public function parse_images_field( $value ) { if ( empty( $value ) ) { return array(); } $images = array(); $separator = apply_filters( 'woocommerce_product_import_image_separator', ',' ); foreach ( $this->explode_values( $value, $separator ) as $image ) { if ( stristr( $image, '://' ) ) { $images[] = esc_url_raw( $image ); } else { $images[] = sanitize_file_name( $image ); } } return $images; } /** * Parse dates from a CSV. * Dates requires the format YYYY-MM-DD and time is optional. * * @param string $value Field value. * * @return string|null */ public function parse_date_field( $value ) { if ( empty( $value ) ) { return null; } if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) { // Don't include the time if the field had time in it. return current( explode( ' ', $value ) ); } return null; } /** * Parse backorders from a CSV. * * @param string $value Field value. * * @return string */ public function parse_backorders_field( $value ) { if ( empty( $value ) ) { return 'no'; } $value = $this->parse_bool_field( $value ); if ( 'notify' === $value ) { return 'notify'; } elseif ( is_bool( $value ) ) { return $value ? 'yes' : 'no'; } return 'no'; } /** * Just skip current field. * * By default is applied wc_clean() to all not listed fields * in self::get_formatting_callback(), use this method to skip any formatting. * * @param string $value Field value. * * @return string */ public function parse_skip_field( $value ) { return $value; } /** * Parse download file urls, we should allow shortcodes here. * * Allow shortcodes if present, othersiwe esc_url the value. * * @param string $value Field value. * * @return string */ public function parse_download_file_field( $value ) { // Absolute file paths. if ( 0 === strpos( $value, 'http' ) ) { return esc_url_raw( $value ); } // Relative and shortcode paths. return wc_clean( $value ); } /** * Parse an int value field * * @param int $value field value. * * @return int */ public function parse_int_field( $value ) { // Remove the ' prepended to fields that start with - if needed. $value = $this->unescape_data( $value ); return intval( $value ); } /** * Parse a description value field * * @param string $description field value. * * @return string */ public function parse_description_field( $description ) { $parts = explode( "\\\\n", $description ); foreach ( $parts as $key => $part ) { $parts[ $key ] = str_replace( '\n', "\n", $part ); } return implode( '\\\n', $parts ); } /** * Parse the published field. 1 is published, 0 is private, -1 is draft. * Alternatively, 'true' can be used for published and 'false' for draft. * * @param string $value Field value. * * @return float|string */ public function parse_published_field( $value ) { if ( '' === $value ) { return $value; } // Remove the ' prepended to fields that start with - if needed. $value = $this->unescape_data( $value ); if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { return wc_string_to_bool( $value ) ? 1 : -1; } return floatval( $value ); } /** * Deprecated get formatting callback method. * * @deprecated 4.3.0 * @return array */ protected function get_formating_callback() { return $this->get_formatting_callback(); } /** * Get formatting callback. * * @since 4.3.0 * @return array */ protected function get_formatting_callback() { /** * Columns not mentioned here will get parsed with 'wc_clean'. * column_name => callback. */ $data_formatting = array( 'id' => array( $this, 'parse_id_field' ), 'type' => array( $this, 'parse_comma_field' ), 'published' => array( $this, 'parse_published_field' ), 'featured' => array( $this, 'parse_bool_field' ), 'date_on_sale_from' => array( $this, 'parse_date_field' ), 'date_on_sale_to' => array( $this, 'parse_date_field' ), 'name' => array( $this, 'parse_skip_field' ), 'short_description' => array( $this, 'parse_description_field' ), 'description' => array( $this, 'parse_description_field' ), 'manage_stock' => array( $this, 'parse_bool_field' ), 'low_stock_amount' => array( $this, 'parse_stock_quantity_field' ), 'backorders' => array( $this, 'parse_backorders_field' ), 'stock_status' => array( $this, 'parse_bool_field' ), 'sold_individually' => array( $this, 'parse_bool_field' ), 'width' => array( $this, 'parse_float_field' ), 'length' => array( $this, 'parse_float_field' ), 'height' => array( $this, 'parse_float_field' ), 'weight' => array( $this, 'parse_float_field' ), 'reviews_allowed' => array( $this, 'parse_bool_field' ), 'purchase_note' => 'wp_filter_post_kses', 'price' => 'wc_format_decimal', 'regular_price' => 'wc_format_decimal', 'stock_quantity' => array( $this, 'parse_stock_quantity_field' ), 'category_ids' => array( $this, 'parse_categories_field' ), 'tag_ids' => array( $this, 'parse_tags_field' ), 'tag_ids_spaces' => array( $this, 'parse_tags_spaces_field' ), 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ), 'images' => array( $this, 'parse_images_field' ), 'parent_id' => array( $this, 'parse_relative_field' ), 'grouped_products' => array( $this, 'parse_relative_comma_field' ), 'upsell_ids' => array( $this, 'parse_relative_comma_field' ), 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ), 'download_limit' => array( $this, 'parse_int_field' ), 'download_expiry' => array( $this, 'parse_int_field' ), 'product_url' => 'esc_url_raw', 'menu_order' => 'intval', 'tax_status' => array( $this, 'parse_tax_status_field' ), ); /** * Match special column names. */ $regex_match_data_formatting = array( '/attributes:value*/' => array( $this, 'parse_comma_field' ), '/attributes:visible*/' => array( $this, 'parse_bool_field' ), '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ), '/downloads:url*/' => array( $this, 'parse_download_file_field' ), '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields. ); $callbacks = array(); // Figure out the parse function for each column. foreach ( $this->get_mapped_keys() as $index => $heading ) { $callback = 'wc_clean'; if ( isset( $data_formatting[ $heading ] ) ) { $callback = $data_formatting[ $heading ]; } else { foreach ( $regex_match_data_formatting as $regex => $callback ) { if ( preg_match( $regex, $heading ) ) { $callback = $callback; break; } } } $callbacks[] = $callback; } return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this ); } /** * Check if strings starts with determined word. * * @param string $haystack Complete sentence. * @param string $needle Excerpt. * * @return bool */ protected function starts_with( $haystack, $needle ) { return substr( $haystack, 0, strlen( $needle ) ) === $needle; } /** * Expand special and internal data into the correct formats for the product CRUD. * * @param array $data Data to import. * * @return array */ protected function expand_data( $data ) { $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data ); // Images field maps to image and gallery id fields. if ( isset( $data['images'] ) ) { $images = $data['images']; $data['raw_image_id'] = array_shift( $images ); if ( ! empty( $images ) ) { $data['raw_gallery_image_ids'] = $images; } unset( $data['images'] ); } // Type, virtual and downloadable are all stored in the same column. if ( isset( $data['type'] ) ) { $data['type'] = array_map( 'strtolower', $data['type'] ); $data['virtual'] = in_array( 'virtual', $data['type'], true ); $data['downloadable'] = in_array( 'downloadable', $data['type'], true ); // Convert type to string. $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) ); if ( ! $data['type'] ) { $data['type'] = 'simple'; } } // Status is mapped from a special published field. if ( isset( $data['published'] ) ) { $statuses = array( -1 => 'draft', 0 => 'private', 1 => 'publish', ); $data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : 'draft'; // Fix draft status of variations. if ( isset( $data['type'] ) && 'variation' === $data['type'] && -1 === $data['published'] ) { $data['status'] = 'publish'; } unset( $data['published'] ); } if ( isset( $data['stock_quantity'] ) ) { if ( '' === $data['stock_quantity'] ) { $data['manage_stock'] = false; $data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true; } else { $data['manage_stock'] = true; } } // Stock is bool or 'backorder'. if ( isset( $data['stock_status'] ) ) { if ( 'backorder' === $data['stock_status'] ) { $data['stock_status'] = 'onbackorder'; } else { $data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock'; } } // Prepare grouped products. if ( isset( $data['grouped_products'] ) ) { $data['children'] = $data['grouped_products']; unset( $data['grouped_products'] ); } // Tag ids. if ( isset( $data['tag_ids_spaces'] ) ) { $data['tag_ids'] = $data['tag_ids_spaces']; unset( $data['tag_ids_spaces'] ); } // Handle special column names which span multiple columns. $attributes = array(); $downloads = array(); $meta_data = array(); foreach ( $data as $key => $value ) { if ( $this->starts_with( $key, 'attributes:name' ) ) { if ( ! empty( $value ) ) { $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value; } unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'attributes:value' ) ) { $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value; unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) { $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value ); unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) { $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value ); unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'attributes:default' ) ) { if ( ! empty( $value ) ) { $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value; } unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'downloads:id' ) ) { if ( ! empty( $value ) ) { $downloads[ str_replace( 'downloads:id', '', $key ) ]['id'] = $value; } unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'downloads:name' ) ) { if ( ! empty( $value ) ) { $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value; } unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'downloads:url' ) ) { if ( ! empty( $value ) ) { $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value; } unset( $data[ $key ] ); } elseif ( $this->starts_with( $key, 'meta:' ) ) { $meta_data[] = array( 'key' => str_replace( 'meta:', '', $key ), 'value' => $value, ); unset( $data[ $key ] ); } } if ( ! empty( $attributes ) ) { // Remove empty attributes and clear indexes. foreach ( $attributes as $attribute ) { if ( empty( $attribute['name'] ) ) { continue; } $data['raw_attributes'][] = $attribute; } } if ( ! empty( $downloads ) ) { $data['downloads'] = array(); foreach ( $downloads as $key => $file ) { if ( empty( $file['url'] ) ) { continue; } $data['downloads'][] = array( 'download_id' => isset( $file['id'] ) ? $file['id'] : null, 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ), 'file' => $file['url'], ); } } if ( ! empty( $meta_data ) ) { $data['meta_data'] = $meta_data; } return $data; } /** * Map and format raw data to known fields. */ protected function set_parsed_data() { $parse_functions = $this->get_formatting_callback(); $mapped_keys = $this->get_mapped_keys(); $use_mb = function_exists( 'mb_convert_encoding' ); // Parse the data. foreach ( $this->raw_data as $row_index => $row ) { // Skip empty rows. if ( ! count( array_filter( $row ) ) ) { continue; } $this->parsing_raw_data_index = $row_index; $data = array(); do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys ); foreach ( $row as $id => $value ) { // Skip ignored columns. if ( empty( $mapped_keys[ $id ] ) ) { continue; } // Convert UTF8. if ( $use_mb ) { $encoding = mb_detect_encoding( $value, mb_detect_order(), true ); if ( $encoding ) { $value = mb_convert_encoding( $value, 'UTF-8', $encoding ); } else { $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); } } else { $value = wp_check_invalid_utf8( $value, true ); } $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value ); } /** * Filter product importer parsed data. * * @param array $parsed_data Parsed data. * @param WC_Product_Importer $importer Importer instance. */ $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); } } /** * Get a string to identify the row from parsed data. * * @param array $parsed_data Parsed data. * * @return string */ protected function get_row_id( $parsed_data ) { $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : ''; $row_data = array(); if ( $name ) { $row_data[] = $name; } if ( $id ) { /* translators: %d: product ID */ $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id ); } if ( $sku ) { /* translators: %s: product SKU */ $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku ); } return implode( ', ', $row_data ); } /** * Process importer. * * Do not import products with IDs or SKUs that already exist if option * update existing is false, and likewise, if updating products, do not * process rows which do not exist if an ID/SKU is provided. * * @return array */ public function import() { $this->start_time = time(); $index = 0; $update_existing = $this->params['update_existing']; $data = array( 'imported' => array(), 'failed' => array(), 'updated' => array(), 'skipped' => array(), ); foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) { do_action( 'woocommerce_product_import_before_import', $parsed_data ); $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; $sku = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : ''; $id_exists = false; $sku_exists = false; if ( $id ) { $product = wc_get_product( $id ); $id_exists = $product && 'importing' !== $product->get_status(); } if ( $sku ) { $id_from_sku = wc_get_product_id_by_sku( $sku ); $product = $id_from_sku ? wc_get_product( $id_from_sku ) : false; $sku_exists = $product && 'importing' !== $product->get_status(); } if ( $id_exists && ! $update_existing ) { $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', esc_html__( 'A product with this ID already exists.', 'woocommerce' ), array( 'id' => $id, 'row' => $this->get_row_id( $parsed_data ), ) ); continue; } if ( $sku_exists && ! $update_existing ) { $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', esc_html__( 'A product with this SKU already exists.', 'woocommerce' ), array( 'sku' => esc_attr( $sku ), 'row' => $this->get_row_id( $parsed_data ), ) ); continue; } if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) { $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', esc_html__( 'No matching product exists to update.', 'woocommerce' ), array( 'id' => $id, 'sku' => esc_attr( $sku ), 'row' => $this->get_row_id( $parsed_data ), ) ); continue; } $result = $this->process_item( $parsed_data ); if ( is_wp_error( $result ) ) { $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) ); $data['failed'][] = $result; } elseif ( $result['updated'] ) { $data['updated'][] = $result['id']; } else { $data['imported'][] = $result['id']; } $index ++; if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) { $this->file_position = $this->file_positions[ $index ]; break; } } return $data; } } includes/import/abstract-wc-product-importer.php 0000644 00000053610 15132754524 0016141 0 ustar 00 <?php /** * Abstract Product importer * * @package WooCommerce\Import * @version 3.1.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Include dependencies. */ if ( ! class_exists( 'WC_Importer_Interface', false ) ) { include_once WC_ABSPATH . 'includes/interfaces/class-wc-importer-interface.php'; } /** * WC_Product_Importer Class. */ abstract class WC_Product_Importer implements WC_Importer_Interface { /** * CSV file. * * @var string */ protected $file = ''; /** * The file position after the last read. * * @var int */ protected $file_position = 0; /** * Importer parameters. * * @var array */ protected $params = array(); /** * Raw keys - CSV raw headers. * * @var array */ protected $raw_keys = array(); /** * Mapped keys - CSV headers. * * @var array */ protected $mapped_keys = array(); /** * Raw data. * * @var array */ protected $raw_data = array(); /** * Raw data. * * @var array */ protected $file_positions = array(); /** * Parsed data. * * @var array */ protected $parsed_data = array(); /** * Start time of current import. * * (default value: 0) * * @var int */ protected $start_time = 0; /** * Get file raw headers. * * @return array */ public function get_raw_keys() { return $this->raw_keys; } /** * Get file mapped headers. * * @return array */ public function get_mapped_keys() { return ! empty( $this->mapped_keys ) ? $this->mapped_keys : $this->raw_keys; } /** * Get raw data. * * @return array */ public function get_raw_data() { return $this->raw_data; } /** * Get parsed data. * * @return array */ public function get_parsed_data() { /** * Filter product importer parsed data. * * @param array $parsed_data Parsed data. * @param WC_Product_Importer $importer Importer instance. */ return apply_filters( 'woocommerce_product_importer_parsed_data', $this->parsed_data, $this ); } /** * Get importer parameters. * * @return array */ public function get_params() { return $this->params; } /** * Get file pointer position from the last read. * * @return int */ public function get_file_position() { return $this->file_position; } /** * Get file pointer position as a percentage of file size. * * @return int */ public function get_percent_complete() { $size = filesize( $this->file ); if ( ! $size ) { return 0; } return absint( min( NumberUtil::round( ( $this->file_position / $size ) * 100 ), 100 ) ); } /** * Prepare a single product for create or update. * * @param array $data Item data. * @return WC_Product|WP_Error */ protected function get_product_object( $data ) { $id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; // Type is the most important part here because we need to be using the correct class and methods. if ( isset( $data['type'] ) ) { if ( ! array_key_exists( $data['type'], WC_Admin_Exporters::get_product_types() ) ) { return new WP_Error( 'woocommerce_product_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) ); } try { // Prevent getting "variation_invalid_id" error message from Variation Data Store. if ( 'variation' === $data['type'] ) { $id = wp_update_post( array( 'ID' => $id, 'post_type' => 'product_variation', ) ); } $product = wc_get_product_object( $data['type'], $id ); } catch ( WC_Data_Exception $e ) { return new WP_Error( 'woocommerce_product_csv_importer_' . $e->getErrorCode(), $e->getMessage(), array( 'status' => 401 ) ); } } elseif ( ! empty( $data['id'] ) ) { $product = wc_get_product( $id ); if ( ! $product ) { return new WP_Error( 'woocommerce_product_csv_importer_invalid_id', /* translators: %d: product ID */ sprintf( __( 'Invalid product ID %d.', 'woocommerce' ), $id ), array( 'id' => $id, 'status' => 401, ) ); } } else { $product = wc_get_product_object( 'simple', $id ); } return apply_filters( 'woocommerce_product_import_get_product_object', $product, $data ); } /** * Process a single item and save. * * @throws Exception If item cannot be processed. * @param array $data Raw CSV data. * @return array|WP_Error */ protected function process_item( $data ) { try { do_action( 'woocommerce_product_import_before_process_item', $data ); $data = apply_filters( 'woocommerce_product_import_process_item_data', $data ); // Get product ID from SKU if created during the importation. if ( empty( $data['id'] ) && ! empty( $data['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $data['sku'] ); if ( $product_id ) { $data['id'] = $product_id; } } $object = $this->get_product_object( $data ); $updating = false; if ( is_wp_error( $object ) ) { return $object; } if ( $object->get_id() && 'importing' !== $object->get_status() ) { $updating = true; } if ( 'external' === $object->get_type() ) { unset( $data['manage_stock'], $data['stock_status'], $data['backorders'], $data['low_stock_amount'] ); } if ( 'variation' === $object->get_type() ) { if ( isset( $data['status'] ) && -1 === $data['status'] ) { $data['status'] = 0; // Variations cannot be drafts - set to private. } } if ( 'importing' === $object->get_status() ) { $object->set_status( 'publish' ); $object->set_slug( '' ); } $result = $object->set_props( array_diff_key( $data, array_flip( array( 'meta_data', 'raw_image_id', 'raw_gallery_image_ids', 'raw_attributes' ) ) ) ); if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } if ( 'variation' === $object->get_type() ) { $this->set_variation_data( $object, $data ); } else { $this->set_product_data( $object, $data ); } $this->set_image_data( $object, $data ); $this->set_meta_data( $object, $data ); $object = apply_filters( 'woocommerce_product_import_pre_insert_product_object', $object, $data ); $object->save(); do_action( 'woocommerce_product_import_inserted_product_object', $object, $data ); return array( 'id' => $object->get_id(), 'updated' => $updating, ); } catch ( Exception $e ) { return new WP_Error( 'woocommerce_product_importer_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Convert raw image URLs to IDs and set. * * @param WC_Product $product Product instance. * @param array $data Item data. */ protected function set_image_data( &$product, $data ) { // Image URLs need converting to IDs before inserting. if ( isset( $data['raw_image_id'] ) ) { $product->set_image_id( $this->get_attachment_id_from_url( $data['raw_image_id'], $product->get_id() ) ); } // Gallery image URLs need converting to IDs before inserting. if ( isset( $data['raw_gallery_image_ids'] ) ) { $gallery_image_ids = array(); foreach ( $data['raw_gallery_image_ids'] as $image_id ) { $gallery_image_ids[] = $this->get_attachment_id_from_url( $image_id, $product->get_id() ); } $product->set_gallery_image_ids( $gallery_image_ids ); } } /** * Append meta data. * * @param WC_Product $product Product instance. * @param array $data Item data. */ protected function set_meta_data( &$product, $data ) { if ( isset( $data['meta_data'] ) ) { foreach ( $data['meta_data'] as $meta ) { $product->update_meta_data( $meta['key'], $meta['value'] ); } } } /** * Set product data. * * @param WC_Product $product Product instance. * @param array $data Item data. * @throws Exception If data cannot be set. */ protected function set_product_data( &$product, $data ) { if ( isset( $data['raw_attributes'] ) ) { $attributes = array(); $default_attributes = array(); $existing_attributes = $product->get_attributes(); foreach ( $data['raw_attributes'] as $position => $attribute ) { $attribute_id = 0; // Get ID if is a global attribute. if ( ! empty( $attribute['taxonomy'] ) ) { $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] ); } // Set attribute visibility. if ( isset( $attribute['visible'] ) ) { $is_visible = $attribute['visible']; } else { $is_visible = 1; } // Get name. $attribute_name = $attribute_id ? wc_attribute_taxonomy_name_by_id( $attribute_id ) : $attribute['name']; // Set if is a variation attribute based on existing attributes if possible so updates via CSV do not change this. $is_variation = 0; if ( $existing_attributes ) { foreach ( $existing_attributes as $existing_attribute ) { if ( $existing_attribute->get_name() === $attribute_name ) { $is_variation = $existing_attribute->get_variation(); break; } } } if ( $attribute_id ) { if ( isset( $attribute['value'] ) ) { $options = array_map( 'wc_sanitize_term_text_based', $attribute['value'] ); $options = array_filter( $options, 'strlen' ); } else { $options = array(); } // Check for default attributes and set "is_variation". if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $options, true ) ) { $default_term = get_term_by( 'name', $attribute['default'], $attribute_name ); if ( $default_term && ! is_wp_error( $default_term ) ) { $default = $default_term->slug; } else { $default = sanitize_title( $attribute['default'] ); } $default_attributes[ $attribute_name ] = $default; $is_variation = 1; } if ( ! empty( $options ) ) { $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( $attribute_id ); $attribute_object->set_name( $attribute_name ); $attribute_object->set_options( $options ); $attribute_object->set_position( $position ); $attribute_object->set_visible( $is_visible ); $attribute_object->set_variation( $is_variation ); $attributes[] = $attribute_object; } } elseif ( isset( $attribute['value'] ) ) { // Check for default attributes and set "is_variation". if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $attribute['value'], true ) ) { $default_attributes[ sanitize_title( $attribute['name'] ) ] = $attribute['default']; $is_variation = 1; } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_name( $attribute['name'] ); $attribute_object->set_options( $attribute['value'] ); $attribute_object->set_position( $position ); $attribute_object->set_visible( $is_visible ); $attribute_object->set_variation( $is_variation ); $attributes[] = $attribute_object; } } $product->set_attributes( $attributes ); // Set variable default attributes. if ( $product->is_type( 'variable' ) ) { $product->set_default_attributes( $default_attributes ); } } } /** * Set variation data. * * @param WC_Product $variation Product instance. * @param array $data Item data. * @return WC_Product|WP_Error * @throws Exception If data cannot be set. */ protected function set_variation_data( &$variation, $data ) { $parent = false; // Check if parent exist. if ( isset( $data['parent_id'] ) ) { $parent = wc_get_product( $data['parent_id'] ); if ( $parent ) { $variation->set_parent_id( $parent->get_id() ); } } // Stop if parent does not exists. if ( ! $parent ) { return new WP_Error( 'woocommerce_product_importer_missing_variation_parent_id', __( 'Variation cannot be imported: Missing parent ID or parent does not exist yet.', 'woocommerce' ), array( 'status' => 401 ) ); } // Stop if parent is a product variation. if ( $parent->is_type( 'variation' ) ) { return new WP_Error( 'woocommerce_product_importer_parent_set_as_variation', __( 'Variation cannot be imported: Parent product cannot be a product variation', 'woocommerce' ), array( 'status' => 401 ) ); } if ( isset( $data['raw_attributes'] ) ) { $attributes = array(); $parent_attributes = $this->get_variation_parent_attributes( $data['raw_attributes'], $parent ); foreach ( $data['raw_attributes'] as $attribute ) { $attribute_id = 0; // Get ID if is a global attribute. if ( ! empty( $attribute['taxonomy'] ) ) { $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] ); } if ( $attribute_id ) { $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } else { $attribute_name = sanitize_title( $attribute['name'] ); } if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { continue; } $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); $attribute_value = isset( $attribute['value'] ) ? current( $attribute['value'] ) : ''; if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $attribute_value = $term->slug; } else { $attribute_value = sanitize_title( $attribute_value ); } } $attributes[ $attribute_key ] = $attribute_value; } $variation->set_attributes( $attributes ); } } /** * Get variation parent attributes and set "is_variation". * * @param array $attributes Attributes list. * @param WC_Product $parent Parent product data. * @return array */ protected function get_variation_parent_attributes( $attributes, $parent ) { $parent_attributes = $parent->get_attributes(); $require_save = false; foreach ( $attributes as $attribute ) { $attribute_id = 0; // Get ID if is a global attribute. if ( ! empty( $attribute['taxonomy'] ) ) { $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] ); } if ( $attribute_id ) { $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); } else { $attribute_name = sanitize_title( $attribute['name'] ); } // Check if attribute handle variations. if ( isset( $parent_attributes[ $attribute_name ] ) && ! $parent_attributes[ $attribute_name ]->get_variation() ) { // Re-create the attribute to CRUD save and generate again. $parent_attributes[ $attribute_name ] = clone $parent_attributes[ $attribute_name ]; $parent_attributes[ $attribute_name ]->set_variation( 1 ); $require_save = true; } } // Save variation attributes. if ( $require_save ) { $parent->set_attributes( array_values( $parent_attributes ) ); $parent->save(); } return $parent_attributes; } /** * Get attachment ID. * * @param string $url Attachment URL. * @param int $product_id Product ID. * @return int * @throws Exception If attachment cannot be loaded. */ public function get_attachment_id_from_url( $url, $product_id ) { if ( empty( $url ) ) { return 0; } $id = 0; $upload_dir = wp_upload_dir( null, false ); $base_url = $upload_dir['baseurl'] . '/'; // Check first if attachment is inside the WordPress uploads directory, or we're given a filename only. if ( false !== strpos( $url, $base_url ) || false === strpos( $url, '://' ) ) { // Search for yyyy/mm/slug.extension or slug.extension - remove the base URL. $file = str_replace( $base_url, '', $url ); $args = array( 'post_type' => 'attachment', 'post_status' => 'any', 'fields' => 'ids', 'meta_query' => array( // @codingStandardsIgnoreLine. 'relation' => 'OR', array( 'key' => '_wp_attached_file', 'value' => '^' . $file, 'compare' => 'REGEXP', ), array( 'key' => '_wp_attached_file', 'value' => '/' . $file, 'compare' => 'LIKE', ), array( 'key' => '_wc_attachment_source', 'value' => '/' . $file, 'compare' => 'LIKE', ), ), ); } else { // This is an external URL, so compare to source. $args = array( 'post_type' => 'attachment', 'post_status' => 'any', 'fields' => 'ids', 'meta_query' => array( // @codingStandardsIgnoreLine. array( 'value' => $url, 'key' => '_wc_attachment_source', ), ), ); } $ids = get_posts( $args ); // @codingStandardsIgnoreLine. if ( $ids ) { $id = current( $ids ); } // Upload if attachment does not exists. if ( ! $id && stristr( $url, '://' ) ) { $upload = wc_rest_upload_image_from_url( $url ); if ( is_wp_error( $upload ) ) { throw new Exception( $upload->get_error_message(), 400 ); } $id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_id ); if ( ! wp_attachment_is_image( $id ) ) { /* translators: %s: image URL */ throw new Exception( sprintf( __( 'Not able to attach "%s".', 'woocommerce' ), $url ), 400 ); } // Save attachment source for future reference. update_post_meta( $id, '_wc_attachment_source', $url ); } if ( ! $id ) { /* translators: %s: image URL */ throw new Exception( sprintf( __( 'Unable to use image "%s".', 'woocommerce' ), $url ), 400 ); } return $id; } /** * Get attribute taxonomy ID from the imported data. * If does not exists register a new attribute. * * @param string $raw_name Attribute name. * @return int * @throws Exception If taxonomy cannot be loaded. */ public function get_attribute_taxonomy_id( $raw_name ) { global $wpdb, $wc_product_attributes; // These are exported as labels, so convert the label to a name if possible first. $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); $attribute_name = array_search( $raw_name, $attribute_labels, true ); if ( ! $attribute_name ) { $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); } $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); // Get the ID from the name. if ( $attribute_id ) { return $attribute_id; } // If the attribute does not exist, create it. $attribute_id = wc_create_attribute( array( 'name' => $raw_name, 'slug' => $attribute_name, 'type' => 'select', 'order_by' => 'menu_order', 'has_archives' => false, ) ); if ( is_wp_error( $attribute_id ) ) { throw new Exception( $attribute_id->get_error_message(), 400 ); } // Register as taxonomy while importing. $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name ); register_taxonomy( $taxonomy_name, apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ), apply_filters( 'woocommerce_taxonomy_args_' . $taxonomy_name, array( 'labels' => array( 'name' => $raw_name, ), 'hierarchical' => true, 'show_ui' => false, 'query_var' => true, 'rewrite' => false, ) ) ); // Set product attributes global. $wc_product_attributes = array(); foreach ( wc_get_attribute_taxonomies() as $taxonomy ) { $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy; } return $attribute_id; } /** * Memory exceeded * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( 'woocommerce_product_importer_memory_exceeded', $return ); } /** * Get memory limit * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return intval( $memory_limit ) * 1024 * 1024; } /** * Time exceeded. * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( 'woocommerce_product_importer_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( 'woocommerce_product_importer_time_exceeded', $return ); } /** * Explode CSV cell values using commas by default, and handling escaped * separators. * * @since 3.2.0 * @param string $value Value to explode. * @param string $separator Separator separating each value. Defaults to comma. * @return array */ protected function explode_values( $value, $separator = ',' ) { $value = str_replace( '\\,', '::separator::', $value ); $values = explode( $separator, $value ); $values = array_map( array( $this, 'explode_values_formatter' ), $values ); return $values; } /** * Remove formatting and trim each value. * * @since 3.2.0 * @param string $value Value to format. * @return string */ protected function explode_values_formatter( $value ) { return trim( str_replace( '::separator::', ',', $value ) ); } /** * The exporter prepends a ' to escape fields that start with =, +, - or @. * Remove the prepended ' character preceding those characters. * * @since 3.5.2 * @param string $value A string that may or may not have been escaped with '. * @return string */ protected function unescape_data( $value ) { $active_content_triggers = array( "'=", "'+", "'-", "'@" ); if ( in_array( mb_substr( $value, 0, 2 ), $active_content_triggers, true ) ) { $value = mb_substr( $value, 1 ); } return $value; } } includes/wc-formatting-functions.php 0000644 00000125721 15132754524 0013672 0 ustar 00 <?php /** * WooCommerce Formatting * * Functions for formatting data. * * @package WooCommerce\Functions * @version 2.1.0 */ use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; /** * Converts a string (e.g. 'yes' or 'no') to a bool. * * @since 3.0.0 * @param string|bool $string String to convert. If a bool is passed it will be returned as-is. * @return bool */ function wc_string_to_bool( $string ) { return is_bool( $string ) ? $string : ( 'yes' === strtolower( $string ) || 1 === $string || 'true' === strtolower( $string ) || '1' === $string ); } /** * Converts a bool to a 'yes' or 'no'. * * @since 3.0.0 * @param bool|string $bool Bool to convert. If a string is passed it will first be converted to a bool. * @return string */ function wc_bool_to_string( $bool ) { if ( ! is_bool( $bool ) ) { $bool = wc_string_to_bool( $bool ); } return true === $bool ? 'yes' : 'no'; } /** * Explode a string into an array by $delimiter and remove empty values. * * @since 3.0.0 * @param string $string String to convert. * @param string $delimiter Delimiter, defaults to ','. * @return array */ function wc_string_to_array( $string, $delimiter = ',' ) { return is_array( $string ) ? $string : array_filter( explode( $delimiter, $string ) ); } /** * Sanitize taxonomy names. Slug format (no spaces, lowercase). * Urldecode is used to reverse munging of UTF8 characters. * * @param string $taxonomy Taxonomy name. * @return string */ function wc_sanitize_taxonomy_name( $taxonomy ) { return apply_filters( 'sanitize_taxonomy_name', urldecode( sanitize_title( urldecode( $taxonomy ) ) ), $taxonomy ); } /** * Sanitize permalink values before insertion into DB. * * Cannot use wc_clean because it sometimes strips % chars and breaks the user's setting. * * @since 2.6.0 * @param string $value Permalink. * @return string */ function wc_sanitize_permalink( $value ) { global $wpdb; $value = $wpdb->strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ); if ( is_wp_error( $value ) ) { $value = ''; } $value = esc_url_raw( trim( $value ) ); $value = str_replace( 'http://', '', $value ); return untrailingslashit( $value ); } /** * Gets the filename part of a download URL. * * @param string $file_url File URL. * @return string */ function wc_get_filename_from_url( $file_url ) { $parts = wp_parse_url( $file_url ); if ( isset( $parts['path'] ) ) { return basename( $parts['path'] ); } } /** * Normalise dimensions, unify to cm then convert to wanted unit value. * * Usage: * wc_get_dimension( 55, 'in' ); * wc_get_dimension( 55, 'in', 'm' ); * * @param int|float $dimension Dimension. * @param string $to_unit Unit to convert to. * Options: 'in', 'm', 'cm', 'm'. * @param string $from_unit Unit to convert from. * Defaults to ''. * Options: 'in', 'm', 'cm', 'm'. * @return float */ function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) { $to_unit = strtolower( $to_unit ); if ( empty( $from_unit ) ) { $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) ); } // Unify all units to cm first. if ( $from_unit !== $to_unit ) { switch ( $from_unit ) { case 'in': $dimension *= 2.54; break; case 'm': $dimension *= 100; break; case 'mm': $dimension *= 0.1; break; case 'yd': $dimension *= 91.44; break; } // Output desired unit. switch ( $to_unit ) { case 'in': $dimension *= 0.3937; break; case 'm': $dimension *= 0.01; break; case 'mm': $dimension *= 10; break; case 'yd': $dimension *= 0.010936133; break; } } return ( $dimension < 0 ) ? 0 : $dimension; } /** * Normalise weights, unify to kg then convert to wanted unit value. * * Usage: * wc_get_weight(55, 'kg'); * wc_get_weight(55, 'kg', 'lbs'); * * @param int|float $weight Weight. * @param string $to_unit Unit to convert to. * Options: 'g', 'kg', 'lbs', 'oz'. * @param string $from_unit Unit to convert from. * Defaults to ''. * Options: 'g', 'kg', 'lbs', 'oz'. * @return float */ function wc_get_weight( $weight, $to_unit, $from_unit = '' ) { $weight = (float) $weight; $to_unit = strtolower( $to_unit ); if ( empty( $from_unit ) ) { $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) ); } // Unify all units to kg first. if ( $from_unit !== $to_unit ) { switch ( $from_unit ) { case 'g': $weight *= 0.001; break; case 'lbs': $weight *= 0.453592; break; case 'oz': $weight *= 0.0283495; break; } // Output desired unit. switch ( $to_unit ) { case 'g': $weight *= 1000; break; case 'lbs': $weight *= 2.20462; break; case 'oz': $weight *= 35.274; break; } } return ( $weight < 0 ) ? 0 : $weight; } /** * Trim trailing zeros off prices. * * @param string|float|int $price Price. * @return string */ function wc_trim_zeros( $price ) { return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ); } /** * Round a tax amount. * * @param double $value Amount to round. * @param int $precision DP to round. Defaults to wc_get_price_decimals. * @return float */ function wc_round_tax_total( $value, $precision = null ) { $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision ); if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound } elseif ( 2 === wc_get_tax_rounding_mode() ) { $rounded_tax = wc_legacy_round_half_down( $value, $precision ); } else { $rounded_tax = NumberUtil::round( $value, $precision ); } return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE ); } /** * Round half down in PHP 5.2. * * @since 3.2.6 * @param float $value Value to round. * @param int $precision Precision to round down to. * @return float */ function wc_legacy_round_half_down( $value, $precision ) { $value = wc_float_to_string( $value ); if ( false !== strstr( $value, '.' ) ) { $value = explode( '.', $value ); if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) { $value[1] = substr( $value[1], 0, -1 ) . '4'; } $value = implode( '.', $value ); } return NumberUtil::round( floatval( $value ), $precision ); } /** * Make a refund total negative. * * @param float $amount Refunded amount. * * @return float */ function wc_format_refund_total( $amount ) { return $amount * -1; } /** * Format decimal numbers ready for DB storage. * * Sanitize, optionally remove decimals, and optionally round + trim off zeros. * * This function does not remove thousands - this should be done before passing a value to the function. * * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands). * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding. * @param bool $trim_zeros From end of string. * @return string */ function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) { $locale = localeconv(); $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); // Remove locale from string. if ( ! is_float( $number ) ) { $number = str_replace( $decimals, '.', $number ); // Convert multiple dots to just one. $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) ); } if ( false !== $dp ) { $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp ); $number = number_format( floatval( $number ), $dp, '.', '' ); } elseif ( is_float( $number ) ) { // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf. $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) ); // We already had a float, so trailing zeros are not needed. $trim_zeros = true; } if ( $trim_zeros && strstr( $number, '.' ) ) { $number = rtrim( rtrim( $number, '0' ), '.' ); } return $number; } /** * Convert a float to a string without locale formatting which PHP adds when changing floats to strings. * * @param float $float Float value to format. * @return string */ function wc_float_to_string( $float ) { if ( ! is_float( $float ) ) { return $float; } $locale = localeconv(); $string = strval( $float ); $string = str_replace( $locale['decimal_point'], '.', $string ); return $string; } /** * Format a price with WC Currency Locale settings. * * @param string $value Price to localize. * @return string */ function wc_format_localized_price( $value ) { return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value ); } /** * Format a decimal with PHP Locale settings. * * @param string $value Decimal to localize. * @return string */ function wc_format_localized_decimal( $value ) { $locale = localeconv(); return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $locale['decimal_point'], strval( $value ) ), $value ); } /** * Format a coupon code. * * @since 3.0.0 * @param string $value Coupon code to format. * @return string */ function wc_format_coupon_code( $value ) { return apply_filters( 'woocommerce_coupon_code', $value ); } /** * Sanitize a coupon code. * * Uses sanitize_post_field since coupon codes are stored as * post_titles - the sanitization and escaping must match. * * @since 3.6.0 * @param string $value Coupon code to format. * @return string */ function wc_sanitize_coupon_code( $value ) { return wp_filter_kses( sanitize_post_field( 'post_title', $value, 0, 'db' ) ); } /** * Clean variables using sanitize_text_field. Arrays are cleaned recursively. * Non-scalar values are ignored. * * @param string|array $var Data to sanitize. * @return string|array */ function wc_clean( $var ) { if ( is_array( $var ) ) { return array_map( 'wc_clean', $var ); } else { return is_scalar( $var ) ? sanitize_text_field( $var ) : $var; } } /** * Function wp_check_invalid_utf8 with recursive array support. * * @param string|array $var Data to sanitize. * @return string|array */ function wc_check_invalid_utf8( $var ) { if ( is_array( $var ) ) { return array_map( 'wc_check_invalid_utf8', $var ); } else { return wp_check_invalid_utf8( $var ); } } /** * Run wc_clean over posted textarea but maintain line breaks. * * @since 3.0.0 * @param string $var Data to sanitize. * @return string */ function wc_sanitize_textarea( $var ) { return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ) ) ); } /** * Sanitize a string destined to be a tooltip. * * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr() * @param string $var Data to sanitize. * @return string */ function wc_sanitize_tooltip( $var ) { return htmlspecialchars( wp_kses( html_entity_decode( $var ), array( 'br' => array(), 'em' => array(), 'strong' => array(), 'small' => array(), 'span' => array(), 'ul' => array(), 'li' => array(), 'ol' => array(), 'p' => array(), ) ) ); } /** * Merge two arrays. * * @param array $a1 First array to merge. * @param array $a2 Second array to merge. * @return array */ function wc_array_overlay( $a1, $a2 ) { foreach ( $a1 as $k => $v ) { if ( ! array_key_exists( $k, $a2 ) ) { continue; } if ( is_array( $v ) && is_array( $a2[ $k ] ) ) { $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] ); } else { $a1[ $k ] = $a2[ $k ]; } } return $a1; } /** * Formats a stock amount by running it through a filter. * * @param int|float $amount Stock amount. * @return int|float */ function wc_stock_amount( $amount ) { return apply_filters( 'woocommerce_stock_amount', $amount ); } /** * Get the price format depending on the currency position. * * @return string */ function get_woocommerce_price_format() { $currency_pos = get_option( 'woocommerce_currency_pos' ); $format = '%1$s%2$s'; switch ( $currency_pos ) { case 'left': $format = '%1$s%2$s'; break; case 'right': $format = '%2$s%1$s'; break; case 'left_space': $format = '%1$s %2$s'; break; case 'right_space': $format = '%2$s %1$s'; break; } return apply_filters( 'woocommerce_price_format', $format, $currency_pos ); } /** * Return the thousand separator for prices. * * @since 2.3 * @return string */ function wc_get_price_thousand_separator() { return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) ); } /** * Return the decimal separator for prices. * * @since 2.3 * @return string */ function wc_get_price_decimal_separator() { $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) ); return $separator ? stripslashes( $separator ) : '.'; } /** * Return the number of decimals after the decimal point. * * @since 2.3 * @return int */ function wc_get_price_decimals() { return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) ); } /** * Format the price with a currency symbol. * * @param float $price Raw price. * @param array $args Arguments to format a price { * Array of arguments. * Defaults to empty array. * * @type bool $ex_tax_label Adds exclude tax label. * Defaults to false. * @type string $currency Currency code. * Defaults to empty string (Use the result from get_woocommerce_currency()). * @type string $decimal_separator Decimal separator. * Defaults the result of wc_get_price_decimal_separator(). * @type string $thousand_separator Thousand separator. * Defaults the result of wc_get_price_thousand_separator(). * @type string $decimals Number of decimals. * Defaults the result of wc_get_price_decimals(). * @type string $price_format Price format depending on the currency position. * Defaults the result of get_woocommerce_price_format(). * } * @return string */ function wc_price( $price, $args = array() ) { $args = apply_filters( 'wc_price_args', wp_parse_args( $args, array( 'ex_tax_label' => false, 'currency' => '', 'decimal_separator' => wc_get_price_decimal_separator(), 'thousand_separator' => wc_get_price_thousand_separator(), 'decimals' => wc_get_price_decimals(), 'price_format' => get_woocommerce_price_format(), ) ) ); $original_price = $price; // Convert to float to avoid issues on PHP 8. $price = (float) $price; $unformatted_price = $price; $negative = $price < 0; /** * Filter raw price. * * @param float $raw_price Raw price. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price ); /** * Filter formatted price. * * @param float $formatted_price Formatted price. * @param float $price Unformatted price. * @param int $decimals Number of decimals. * @param string $decimal_separator Decimal separator. * @param string $thousand_separator Thousand separator. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ $price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price ); if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) { $price = wc_trim_zeros( $price ); } $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '<span class="woocommerce-Price-currencySymbol">' . get_woocommerce_currency_symbol( $args['currency'] ) . '</span>', $price ); $return = '<span class="woocommerce-Price-amount amount"><bdi>' . $formatted_price . '</bdi></span>'; if ( $args['ex_tax_label'] && wc_tax_enabled() ) { $return .= ' <small class="woocommerce-Price-taxLabel tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>'; } /** * Filters the string of price markup. * * @param string $return Price HTML markup. * @param string $price Formatted price. * @param array $args Pass on the args. * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price ); } /** * Notation to numbers. * * This function transforms the php.ini notation for numbers (like '2M') to an integer. * * @param string $size Size value. * @return int */ function wc_let_to_num( $size ) { $l = substr( $size, -1 ); $ret = (int) substr( $size, 0, -1 ); switch ( strtoupper( $l ) ) { case 'P': $ret *= 1024; // No break. case 'T': $ret *= 1024; // No break. case 'G': $ret *= 1024; // No break. case 'M': $ret *= 1024; // No break. case 'K': $ret *= 1024; // No break. } return $ret; } /** * WooCommerce Date Format - Allows to change date format for everything WooCommerce. * * @return string */ function wc_date_format() { $date_format = get_option( 'date_format' ); if ( empty( $date_format ) ) { // Return default date format if the option is empty. $date_format = 'F j, Y'; } return apply_filters( 'woocommerce_date_format', $date_format ); } /** * WooCommerce Time Format - Allows to change time format for everything WooCommerce. * * @return string */ function wc_time_format() { $time_format = get_option( 'time_format' ); if ( empty( $time_format ) ) { // Return default time format if the option is empty. $time_format = 'g:i a'; } return apply_filters( 'woocommerce_time_format', $time_format ); } /** * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime. * * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress. * * @since 3.0.0 * @param string $time_string Time string. * @param int|null $from_timestamp Timestamp to convert from. * @return int */ function wc_string_to_timestamp( $time_string, $from_timestamp = null ) { $original_timezone = date_default_timezone_get(); // @codingStandardsIgnoreStart date_default_timezone_set( 'UTC' ); if ( null === $from_timestamp ) { $next_timestamp = strtotime( $time_string ); } else { $next_timestamp = strtotime( $time_string, $from_timestamp ); } date_default_timezone_set( $original_timezone ); // @codingStandardsIgnoreEnd return $next_timestamp; } /** * Convert a date string to a WC_DateTime. * * @since 3.1.0 * @param string $time_string Time string. * @return WC_DateTime */ function wc_string_to_datetime( $time_string ) { // Strings are defined in local WP timezone. Convert to UTC. if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) { $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; } else { $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) ); } $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); // Set local timezone or offset. if ( get_option( 'timezone_string' ) ) { $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { $datetime->set_utc_offset( wc_timezone_offset() ); } return $datetime; } /** * WooCommerce Timezone - helper to retrieve the timezone string for a site until. * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730). * * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. * * @since 2.1 * @return string PHP timezone string for the site */ function wc_timezone_string() { // Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/. if ( function_exists( 'wp_timezone_string' ) ) { return wp_timezone_string(); } // If site timezone string exists, return it. $timezone = get_option( 'timezone_string' ); if ( $timezone ) { return $timezone; } // Get UTC offset, if it isn't set then return UTC. $utc_offset = floatval( get_option( 'gmt_offset', 0 ) ); if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) { return 'UTC'; } // Adjust UTC offset from hours to seconds. $utc_offset = (int) ( $utc_offset * 3600 ); // Attempt to guess the timezone string from the UTC offset. $timezone = timezone_name_from_abbr( '', $utc_offset ); if ( $timezone ) { return $timezone; } // Last try, guess timezone string manually. foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { // WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone. if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return $city['timezone_id']; } } } // Fallback to UTC. return 'UTC'; } /** * Get timezone offset in seconds. * * @since 3.0.0 * @return float */ function wc_timezone_offset() { $timezone = get_option( 'timezone_string' ); if ( $timezone ) { $timezone_object = new DateTimeZone( $timezone ); return $timezone_object->getOffset( new DateTime( 'now' ) ); } else { return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; } } /** * Callback which can flatten post meta (gets the first value if it's an array). * * @since 3.0.0 * @param array $value Value to flatten. * @return mixed */ function wc_flatten_meta_callback( $value ) { return is_array( $value ) ? current( $value ) : $value; } if ( ! function_exists( 'wc_rgb_from_hex' ) ) { /** * Convert RGB to HEX. * * @param mixed $color Color. * * @return array */ function wc_rgb_from_hex( $color ) { $color = str_replace( '#', '', $color ); // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF". $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color ); $rgb = array(); $rgb['R'] = hexdec( $color[0] . $color[1] ); $rgb['G'] = hexdec( $color[2] . $color[3] ); $rgb['B'] = hexdec( $color[4] . $color[5] ); return $rgb; } } if ( ! function_exists( 'wc_hex_darker' ) ) { /** * Make HEX color darker. * * @param mixed $color Color. * @param int $factor Darker factor. * Defaults to 30. * @return string */ function wc_hex_darker( $color, $factor = 30 ) { $base = wc_rgb_from_hex( $color ); $color = '#'; foreach ( $base as $k => $v ) { $amount = $v / 100; $amount = NumberUtil::round( $amount * $factor ); $new_decimal = $v - $amount; $new_hex_component = dechex( $new_decimal ); if ( strlen( $new_hex_component ) < 2 ) { $new_hex_component = '0' . $new_hex_component; } $color .= $new_hex_component; } return $color; } } if ( ! function_exists( 'wc_hex_lighter' ) ) { /** * Make HEX color lighter. * * @param mixed $color Color. * @param int $factor Lighter factor. * Defaults to 30. * @return string */ function wc_hex_lighter( $color, $factor = 30 ) { $base = wc_rgb_from_hex( $color ); $color = '#'; foreach ( $base as $k => $v ) { $amount = 255 - $v; $amount = $amount / 100; $amount = NumberUtil::round( $amount * $factor ); $new_decimal = $v + $amount; $new_hex_component = dechex( $new_decimal ); if ( strlen( $new_hex_component ) < 2 ) { $new_hex_component = '0' . $new_hex_component; } $color .= $new_hex_component; } return $color; } } if ( ! function_exists( 'wc_hex_is_light' ) ) { /** * Determine whether a hex color is light. * * @param mixed $color Color. * @return bool True if a light color. */ function wc_hex_is_light( $color ) { $hex = str_replace( '#', '', $color ); $c_r = hexdec( substr( $hex, 0, 2 ) ); $c_g = hexdec( substr( $hex, 2, 2 ) ); $c_b = hexdec( substr( $hex, 4, 2 ) ); $brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000; return $brightness > 155; } } if ( ! function_exists( 'wc_light_or_dark' ) ) { /** * Detect if we should use a light or dark color on a background color. * * @param mixed $color Color. * @param string $dark Darkest reference. * Defaults to '#000000'. * @param string $light Lightest reference. * Defaults to '#FFFFFF'. * @return string */ function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { return wc_hex_is_light( $color ) ? $dark : $light; } } if ( ! function_exists( 'wc_format_hex' ) ) { /** * Format string as hex. * * @param string $hex HEX color. * @return string|null */ function wc_format_hex( $hex ) { $hex = trim( str_replace( '#', '', $hex ) ); if ( strlen( $hex ) === 3 ) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } return $hex ? '#' . $hex : null; } } /** * Format the postcode according to the country and length of the postcode. * * @param string $postcode Unformatted postcode. * @param string $country Base country. * @return string */ function wc_format_postcode( $postcode, $country ) { $postcode = wc_normalize_postcode( $postcode ); switch ( $country ) { case 'CA': case 'GB': $postcode = substr_replace( $postcode, ' ', -3, 0 ); break; case 'IE': $postcode = substr_replace( $postcode, ' ', 3, 0 ); break; case 'BR': case 'PL': $postcode = substr_replace( $postcode, '-', -3, 0 ); break; case 'JP': $postcode = substr_replace( $postcode, '-', 3, 0 ); break; case 'PT': $postcode = substr_replace( $postcode, '-', 4, 0 ); break; case 'PR': case 'US': $postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' ); break; case 'NL': $postcode = substr_replace( $postcode, ' ', 4, 0 ); break; } return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country ); } /** * Normalize postcodes. * * Remove spaces and convert characters to uppercase. * * @since 2.6.0 * @param string $postcode Postcode. * @return string */ function wc_normalize_postcode( $postcode ) { return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) ); } /** * Format phone numbers. * * @param string $phone Phone number. * @return string */ function wc_format_phone_number( $phone ) { if ( ! WC_Validation::is_phone( $phone ) ) { return ''; } return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) ); } /** * Sanitize phone number. * Allows only numbers and "+" (plus sign). * * @since 3.6.0 * @param string $phone Phone number. * @return string */ function wc_sanitize_phone_number( $phone ) { return preg_replace( '/[^\d+]/', '', $phone ); } /** * Wrapper for mb_strtoupper which see's if supported first. * * @since 3.1.0 * @param string $string String to format. * @return string */ function wc_strtoupper( $string ) { return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string ); } /** * Make a string lowercase. * Try to use mb_strtolower() when available. * * @since 2.3 * @param string $string String to format. * @return string */ function wc_strtolower( $string ) { return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string ); } /** * Trim a string and append a suffix. * * @param string $string String to trim. * @param integer $chars Amount of characters. * Defaults to 200. * @param string $suffix Suffix. * Defaults to '...'. * @return string */ function wc_trim_string( $string, $chars = 200, $suffix = '...' ) { if ( strlen( $string ) > $chars ) { if ( function_exists( 'mb_substr' ) ) { $string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix; } else { $string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix; } } return $string; } /** * Format content to display shortcodes. * * @since 2.3.0 * @param string $raw_string Raw string. * @return string */ function wc_format_content( $raw_string ) { return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string ); } /** * Format product short description. * Adds support for Jetpack Markdown. * * @codeCoverageIgnore * @since 2.4.0 * @param string $content Product short description. * @return string */ function wc_format_product_short_description( $content ) { // Add support for Jetpack Markdown. if ( class_exists( 'WPCom_Markdown' ) ) { $markdown = WPCom_Markdown::get_instance(); return wpautop( $markdown->transform( $content, array( 'unslash' => false, ) ) ); } return $content; } /** * Formats curency symbols when saved in settings. * * @codeCoverageIgnore * @param string $value Option value. * @param array $option Option name. * @param string $raw_value Raw value. * @return string */ function wc_format_option_price_separators( $value, $option, $raw_value ) { return wp_kses_post( $raw_value ); } add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 ); /** * Formats decimals when saved in settings. * * @codeCoverageIgnore * @param string $value Option value. * @param array $option Option name. * @param string $raw_value Raw value. * @return string */ function wc_format_option_price_num_decimals( $value, $option, $raw_value ) { return is_null( $raw_value ) ? 2 : absint( $raw_value ); } add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 ); /** * Formats hold stock option and sets cron event up. * * @codeCoverageIgnore * @param string $value Option value. * @param array $option Option name. * @param string $raw_value Raw value. * @return string */ function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) { $value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''. wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); if ( '' !== $value ) { $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) ); wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); } return $value; } add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 ); /** * Sanitize terms from an attribute text based. * * @since 2.4.5 * @param string $term Term value. * @return string */ function wc_sanitize_term_text_based( $term ) { return trim( wp_strip_all_tags( wp_unslash( $term ) ) ); } if ( ! function_exists( 'wc_make_numeric_postcode' ) ) { /** * Make numeric postcode. * * Converts letters to numbers so we can do a simple range check on postcodes. * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00) * * @since 2.6.0 * @param string $postcode Regular postcode. * @return string */ function wc_make_numeric_postcode( $postcode ) { $postcode = str_replace( array( ' ', '-' ), '', $postcode ); $postcode_length = strlen( $postcode ); $letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) ); $letters_to_numbers = array_flip( $letters_to_numbers ); $numeric_postcode = ''; for ( $i = 0; $i < $postcode_length; $i ++ ) { if ( is_numeric( $postcode[ $i ] ) ) { $numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT ); } elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) { $numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT ); } else { $numeric_postcode .= '00'; } } return $numeric_postcode; } } /** * Format the stock amount ready for display based on settings. * * @since 3.0.0 * @param WC_Product $product Product object for which the stock you need to format. * @return string */ function wc_format_stock_for_display( $product ) { $display = __( 'In stock', 'woocommerce' ); $stock_amount = $product->get_stock_quantity(); switch ( get_option( 'woocommerce_stock_format' ) ) { case 'low_amount': if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) { /* translators: %s: stock amount */ $display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); } break; case '': /* translators: %s: stock amount */ $display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); break; } if ( $product->backorders_allowed() && $product->backorders_require_notification() ) { $display .= ' ' . __( '(can be backordered)', 'woocommerce' ); } return $display; } /** * Format the stock quantity ready for display. * * @since 3.0.0 * @param int $stock_quantity Stock quantity. * @param WC_Product $product Product instance so that we can pass through the filters. * @return string */ function wc_format_stock_quantity_for_display( $stock_quantity, $product ) { return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product ); } /** * Format a sale price for display. * * @since 3.0.0 * @param string $regular_price Regular price. * @param string $sale_price Sale price. * @return string */ function wc_format_sale_price( $regular_price, $sale_price ) { $price = '<del aria-hidden="true">' . ( is_numeric( $regular_price ) ? wc_price( $regular_price ) : $regular_price ) . '</del> <ins>' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . '</ins>'; return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price ); } /** * Format a price range for display. * * @param string $from Price from. * @param string $to Price to. * @return string */ function wc_format_price_range( $from, $to ) { /* translators: 1: price from 2: price to */ $price = sprintf( _x( '%1$s – %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to ); return apply_filters( 'woocommerce_format_price_range', $price, $from, $to ); } /** * Format a weight for display. * * @since 3.0.0 * @param float $weight Weight. * @return string */ function wc_format_weight( $weight ) { $weight_string = wc_format_localized_decimal( $weight ); if ( ! empty( $weight_string ) ) { $weight_string .= ' ' . get_option( 'woocommerce_weight_unit' ); } else { $weight_string = __( 'N/A', 'woocommerce' ); } return apply_filters( 'woocommerce_format_weight', $weight_string, $weight ); } /** * Format dimensions for display. * * @since 3.0.0 * @param array $dimensions Array of dimensions. * @return string */ function wc_format_dimensions( $dimensions ) { $dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) ); if ( ! empty( $dimension_string ) ) { $dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' ); } else { $dimension_string = __( 'N/A', 'woocommerce' ); } return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions ); } /** * Format a date for output. * * @since 3.0.0 * @param WC_DateTime $date Instance of WC_DateTime. * @param string $format Data format. * Defaults to the wc_date_format function if not set. * @return string */ function wc_format_datetime( $date, $format = '' ) { if ( ! $format ) { $format = wc_date_format(); } if ( ! is_a( $date, 'WC_DateTime' ) ) { return ''; } return $date->date_i18n( $format ); } /** * Process oEmbeds. * * @since 3.1.0 * @param string $content Content. * @return string */ function wc_do_oembeds( $content ) { global $wp_embed; $content = $wp_embed->autoembed( $content ); return $content; } /** * Get part of a string before :. * * Used for example in shipping methods ids where they take the format * method_id:instance_id * * @since 3.2.0 * @param string $string String to extract. * @return string */ function wc_get_string_before_colon( $string ) { return trim( current( explode( ':', (string) $string ) ) ); } /** * Array merge and sum function. * * Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc * * @since 3.2.0 * @return array */ function wc_array_merge_recursive_numeric() { $arrays = func_get_args(); // If there's only one array, it's already merged. if ( 1 === count( $arrays ) ) { return $arrays[0]; } // Remove any items in $arrays that are NOT arrays. foreach ( $arrays as $key => $array ) { if ( ! is_array( $array ) ) { unset( $arrays[ $key ] ); } } // We start by setting the first array as our final array. // We will merge all other arrays with this one. $final = array_shift( $arrays ); foreach ( $arrays as $b ) { foreach ( $final as $key => $value ) { // If $key does not exist in $b, then it is unique and can be safely merged. if ( ! isset( $b[ $key ] ) ) { $final[ $key ] = $value; } else { // If $key is present in $b, then we need to merge and sum numeric values in both. if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) { // If both values for these keys are numeric, we sum them. $final[ $key ] = $value + $b[ $key ]; } elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) { // If both values are arrays, we recursively call ourself. $final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] ); } else { // If both keys exist but differ in type, then we cannot merge them. // In this scenario, we will $b's value for $key is used. $final[ $key ] = $b[ $key ]; } } } // Finally, we need to merge any keys that exist only in $b. foreach ( $b as $key => $value ) { if ( ! isset( $final[ $key ] ) ) { $final[ $key ] = $value; } } } return $final; } /** * Implode and escape HTML attributes for output. * * @since 3.3.0 * @param array $raw_attributes Attribute name value pairs. * @return string */ function wc_implode_html_attributes( $raw_attributes ) { $attributes = array(); foreach ( $raw_attributes as $name => $value ) { $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; } return implode( ' ', $attributes ); } /** * Escape JSON for use on HTML or attribute text nodes. * * @since 3.5.5 * @param string $json JSON to escape. * @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled. * @return string Escaped JSON. */ function wc_esc_json( $json, $html = false ) { return _wp_specialchars( $json, $html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only. 'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset. true // Double escape entities: `&` -> `&amp;`. ); } /** * Parse a relative date option from the settings API into a standard format. * * @since 3.4.0 * @param mixed $raw_value Value stored in DB. * @return array Nicely formatted array with number and unit values. */ function wc_parse_relative_date_option( $raw_value ) { $periods = array( 'days' => __( 'Day(s)', 'woocommerce' ), 'weeks' => __( 'Week(s)', 'woocommerce' ), 'months' => __( 'Month(s)', 'woocommerce' ), 'years' => __( 'Year(s)', 'woocommerce' ), ); $value = wp_parse_args( (array) $raw_value, array( 'number' => '', 'unit' => 'days', ) ); $value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : ''; if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) { $value['unit'] = 'days'; } return $value; } /** * Format the endpoint slug, strip out anything not allowed in a url. * * @since 3.5.0 * @param string $raw_value The raw value. * @return string */ function wc_sanitize_endpoint_slug( $raw_value ) { return sanitize_title( $raw_value ); } add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); includes/class-wc-product-external.php 0000644 00000011533 15132754524 0014110 0 ustar 00 <?php /** * External Product * * External products cannot be bought; they link offsite. Extends simple products. * * @package WooCommerce\Classes\Products * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; /** * Product external class. */ class WC_Product_External extends WC_Product { /** * Stores product data. * * @var array */ protected $extra_data = array( 'product_url' => '', 'button_text' => '', ); /** * Get internal type. * * @return string */ public function get_type() { return 'external'; } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- | | Methods for getting data from the product object. */ /** * Get product url. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_product_url( $context = 'view' ) { return esc_url_raw( $this->get_prop( 'product_url', $context ) ); } /** * Get button text. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_button_text( $context = 'view' ) { return $this->get_prop( 'button_text', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting product data. These should not update anything in the | database itself and should only change what is stored in the class | object. */ /** * Set product URL. * * @since 3.0.0 * @param string $product_url Product URL. */ public function set_product_url( $product_url ) { $this->set_prop( 'product_url', htmlspecialchars_decode( $product_url ) ); } /** * Set button text. * * @since 3.0.0 * @param string $button_text Button text. */ public function set_button_text( $button_text ) { $this->set_prop( 'button_text', $button_text ); } /** * External products cannot be stock managed. * * @since 3.0.0 * @param bool $manage_stock If manage stock. */ public function set_manage_stock( $manage_stock ) { $this->set_prop( 'manage_stock', false ); if ( true === $manage_stock ) { $this->error( 'product_external_invalid_manage_stock', __( 'External products cannot be stock managed.', 'woocommerce' ) ); } } /** * External products cannot be stock managed. * * @since 3.0.0 * * @param string $stock_status Stock status. */ public function set_stock_status( $stock_status = '' ) { $this->set_prop( 'stock_status', 'instock' ); if ( 'instock' !== $stock_status ) { $this->error( 'product_external_invalid_stock_status', __( 'External products cannot be stock managed.', 'woocommerce' ) ); } } /** * External products cannot be backordered. * * @since 3.0.0 * @param string $backorders Options: 'yes', 'no' or 'notify'. */ public function set_backorders( $backorders ) { $this->set_prop( 'backorders', 'no' ); if ( 'no' !== $backorders ) { $this->error( 'product_external_invalid_backorders', __( 'External products cannot be backordered.', 'woocommerce' ) ); } } /* |-------------------------------------------------------------------------- | Other Actions |-------------------------------------------------------------------------- */ /** * Returns false if the product cannot be bought. * * @access public * @return bool */ public function is_purchasable() { return apply_filters( 'woocommerce_is_purchasable', false, $this ); } /** * Get the add to url used mainly in loops. * * @access public * @return string */ public function add_to_cart_url() { return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_product_url(), $this ); } /** * Get the add to cart button text for the single page. * * @access public * @return string */ public function single_add_to_cart_text() { return apply_filters( 'woocommerce_product_single_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); } /** * Get the add to cart button text. * * @access public * @return string */ public function add_to_cart_text() { return apply_filters( 'woocommerce_product_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ return apply_filters( 'woocommerce_product_add_to_cart_description', $this->get_button_text() ? $this->get_button_text() : sprintf( __( 'Buy “%s”', 'woocommerce' ), $this->get_name() ), $this ); } } includes/class-wc-payment-gateways.php 0000644 00000012651 15132754524 0014111 0 ustar 00 <?php /** * WooCommerce Payment Gateways * * Loads payment gateways via hooks for use in the store. * * @version 2.2.0 * @package WooCommerce\Classes\Payment */ defined( 'ABSPATH' ) || exit; /** * Payment gateways class. */ class WC_Payment_Gateways { /** * Payment gateway classes. * * @var array */ public $payment_gateways = array(); /** * The single instance of the class. * * @var WC_Payment_Gateways * @since 2.1.0 */ protected static $_instance = null; /** * Main WC_Payment_Gateways Instance. * * Ensures only one instance of WC_Payment_Gateways is loaded or can be loaded. * * @since 2.1 * @return WC_Payment_Gateways Main instance */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Cloning is forbidden. * * @since 2.1 */ public function __clone() { wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. * * @since 2.1 */ public function __wakeup() { wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' ); } /** * Initialize payment gateways. */ public function __construct() { $this->init(); } /** * Load gateways and hook in functions. */ public function init() { $load_gateways = array( 'WC_Gateway_BACS', 'WC_Gateway_Cheque', 'WC_Gateway_COD', ); if ( $this->should_load_paypal_standard() ) { $load_gateways[] = 'WC_Gateway_Paypal'; } // Filter. $load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways ); // Get sort order option. $ordering = (array) get_option( 'woocommerce_gateway_order' ); $order_end = 999; // Load gateways in order. foreach ( $load_gateways as $gateway ) { if ( is_string( $gateway ) && class_exists( $gateway ) ) { $gateway = new $gateway(); } // Gateways need to be valid and extend WC_Payment_Gateway. if ( ! is_a( $gateway, 'WC_Payment_Gateway' ) ) { continue; } if ( isset( $ordering[ $gateway->id ] ) && is_numeric( $ordering[ $gateway->id ] ) ) { // Add in position. $this->payment_gateways[ $ordering[ $gateway->id ] ] = $gateway; } else { // Add to end of the array. $this->payment_gateways[ $order_end ] = $gateway; $order_end++; } } ksort( $this->payment_gateways ); } /** * Get gateways. * * @return array */ public function payment_gateways() { $_available_gateways = array(); if ( count( $this->payment_gateways ) > 0 ) { foreach ( $this->payment_gateways as $gateway ) { $_available_gateways[ $gateway->id ] = $gateway; } } return $_available_gateways; } /** * Get array of registered gateway ids * * @since 2.6.0 * @return array of strings */ public function get_payment_gateway_ids() { return wp_list_pluck( $this->payment_gateways, 'id' ); } /** * Get available gateways. * * @return array */ public function get_available_payment_gateways() { $_available_gateways = array(); foreach ( $this->payment_gateways as $gateway ) { if ( $gateway->is_available() ) { if ( ! is_add_payment_method_page() ) { $_available_gateways[ $gateway->id ] = $gateway; } elseif ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) { $_available_gateways[ $gateway->id ] = $gateway; } } } return array_filter( (array) apply_filters( 'woocommerce_available_payment_gateways', $_available_gateways ), array( $this, 'filter_valid_gateway_class' ) ); } /** * Callback for array filter. Returns true if gateway is of correct type. * * @since 3.6.0 * @param object $gateway Gateway to check. * @return bool */ protected function filter_valid_gateway_class( $gateway ) { return $gateway && is_a( $gateway, 'WC_Payment_Gateway' ); } /** * Set the current, active gateway. * * @param array $gateways Available payment gateways. */ public function set_current_gateway( $gateways ) { // Be on the defensive. if ( ! is_array( $gateways ) || empty( $gateways ) ) { return; } $current_gateway = false; if ( WC()->session ) { $current = WC()->session->get( 'chosen_payment_method' ); if ( $current && isset( $gateways[ $current ] ) ) { $current_gateway = $gateways[ $current ]; } } if ( ! $current_gateway ) { $current_gateway = current( $gateways ); } // Ensure we can make a call to set_current() without triggering an error. if ( $current_gateway && is_callable( array( $current_gateway, 'set_current' ) ) ) { $current_gateway->set_current(); } } /** * Save options in admin. */ public function process_admin_options() { $gateway_order = isset( $_POST['gateway_order'] ) ? wc_clean( wp_unslash( $_POST['gateway_order'] ) ) : ''; // WPCS: input var ok, CSRF ok. $order = array(); if ( is_array( $gateway_order ) && count( $gateway_order ) > 0 ) { $loop = 0; foreach ( $gateway_order as $gateway_id ) { $order[ esc_attr( $gateway_id ) ] = $loop; $loop++; } } update_option( 'woocommerce_gateway_order', $order ); } /** * Determines if PayPal Standard should be loaded. * * @since 5.5.0 * @return bool Whether PayPal Standard should be loaded or not. */ protected function should_load_paypal_standard() { $paypal = new WC_Gateway_Paypal(); return $paypal->should_load(); } } includes/class-wc-integrations.php 0000644 00000002434 15132754524 0013316 0 ustar 00 <?php /** * WooCommerce Integrations class * * Loads Integrations into WooCommerce. * * @version 3.9.0 * @package WooCommerce\Classes\Integrations */ defined( 'ABSPATH' ) || exit; /** * Integrations class. */ class WC_Integrations { /** * Array of integrations. * * @var array */ public $integrations = array(); /** * Initialize integrations. */ public function __construct() { do_action( 'woocommerce_integrations_init' ); $load_integrations = array( 'WC_Integration_MaxMind_Geolocation', ); $load_integrations = apply_filters( 'woocommerce_integrations', $load_integrations ); // Load integration classes. foreach ( $load_integrations as $integration ) { $load_integration = new $integration(); $this->integrations[ $load_integration->id ] = $load_integration; } } /** * Return loaded integrations. * * @return array */ public function get_integrations() { return $this->integrations; } /** * Return a desired integration. * * @since 3.9.0 * @param string $id The id of the integration to get. * @return mixed|null The integration if one is found, otherwise null. */ public function get_integration( $id ) { if ( isset( $this->integrations[ $id ] ) ) { return $this->integrations[ $id ]; } return null; } } includes/class-wc-background-updater.php 0000644 00000006717 15132754524 0014401 0 ustar 00 <?php /** * Background Updater * * @version 2.6.0 * @deprecated 3.6.0 Replaced with queue. * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'WC_Background_Process', false ) ) { include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php'; } /** * WC_Background_Updater Class. */ class WC_Background_Updater extends WC_Background_Process { /** * Initiate new background process. */ public function __construct() { // Uses unique prefix per blog so each blog has separate queue. $this->prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_updater'; parent::__construct(); } /** * Dispatch updater. * * Updater will still run via cron job if this fails for any reason. */ public function dispatch() { $dispatched = parent::dispatch(); $logger = wc_get_logger(); if ( is_wp_error( $dispatched ) ) { $logger->error( sprintf( 'Unable to dispatch WooCommerce updater: %s', $dispatched->get_error_message() ), array( 'source' => 'wc_db_updates' ) ); } } /** * Handle cron healthcheck * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_process_running() ) { // Background process already running. return; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); return; } $this->handle(); } /** * Schedule fallback event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Is the updater running? * * @return boolean */ public function is_updating() { return false === $this->is_queue_empty(); } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param string $callback Update callback function. * @return string|bool */ protected function task( $callback ) { wc_maybe_define_constant( 'WC_UPDATING', true ); $logger = wc_get_logger(); include_once dirname( __FILE__ ) . '/wc-update-functions.php'; $result = false; if ( is_callable( $callback ) ) { $logger->info( sprintf( 'Running %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); $result = (bool) call_user_func( $callback, $this ); if ( $result ) { $logger->info( sprintf( '%s callback needs to run again', $callback ), array( 'source' => 'wc_db_updates' ) ); } else { $logger->info( sprintf( 'Finished running %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); } } else { $logger->notice( sprintf( 'Could not find %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); } return $result ? $callback : false; } /** * Complete * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { $logger = wc_get_logger(); $logger->info( 'Data update complete', array( 'source' => 'wc_db_updates' ) ); WC_Install::update_db_version(); parent::complete(); } /** * See if the batch limit has been exceeded. * * @return bool */ public function is_memory_exceeded() { return $this->memory_exceeded(); } } includes/class-wc-rest-exception.php 0000644 00000000424 15132754524 0013556 0 ustar 00 <?php /** * WooCommerce REST Exception Class * * Extends Exception to provide additional data. * * @package WooCommerce\RestApi * @since 2.6.0 */ defined( 'ABSPATH' ) || exit; /** * WC_REST_Exception class. */ class WC_REST_Exception extends WC_Data_Exception {} includes/gateways/cod/class-wc-gateway-cod.php 0000644 00000031205 15132754524 0015403 0 ustar 00 <?php /** * Class WC_Gateway_COD file. * * @package WooCommerce\Gateways */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Cash on Delivery Gateway. * * Provides a Cash on Delivery Payment Gateway. * * @class WC_Gateway_COD * @extends WC_Payment_Gateway * @version 2.1.0 * @package WooCommerce\Classes\Payment */ class WC_Gateway_COD extends WC_Payment_Gateway { /** * Constructor for the gateway. */ public function __construct() { // Setup general properties. $this->setup_properties(); // Load the settings. $this->init_form_fields(); $this->init_settings(); // Get settings. $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); $this->instructions = $this->get_option( 'instructions' ); $this->enable_for_methods = $this->get_option( 'enable_for_methods', array() ); $this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes'; add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) ); add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 ); // Customer Emails. add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); } /** * Setup general properties for the gateway. */ protected function setup_properties() { $this->id = 'cod'; $this->icon = apply_filters( 'woocommerce_cod_icon', '' ); $this->method_title = __( 'Cash on delivery', 'woocommerce' ); $this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' ); $this->has_fields = false; } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'label' => __( 'Enable cash on delivery', 'woocommerce' ), 'type' => 'checkbox', 'description' => '', 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), 'default' => __( 'Cash on delivery', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Payment method description that the customer will see on your website.', 'woocommerce' ), 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), 'desc_tip' => true, ), 'instructions' => array( 'title' => __( 'Instructions', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Instructions that will be added to the thank you page.', 'woocommerce' ), 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), 'desc_tip' => true, ), 'enable_for_methods' => array( 'title' => __( 'Enable for shipping methods', 'woocommerce' ), 'type' => 'multiselect', 'class' => 'wc-enhanced-select', 'css' => 'width: 400px;', 'default' => '', 'description' => __( 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', 'woocommerce' ), 'options' => $this->load_shipping_method_options(), 'desc_tip' => true, 'custom_attributes' => array( 'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ), ), ), 'enable_for_virtual' => array( 'title' => __( 'Accept for virtual orders', 'woocommerce' ), 'label' => __( 'Accept COD if the order is virtual', 'woocommerce' ), 'type' => 'checkbox', 'default' => 'yes', ), ); } /** * Check If The Gateway Is Available For Use. * * @return bool */ public function is_available() { $order = null; $needs_shipping = false; // Test if shipping is needed first. if ( WC()->cart && WC()->cart->needs_shipping() ) { $needs_shipping = true; } elseif ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) { $order_id = absint( get_query_var( 'order-pay' ) ); $order = wc_get_order( $order_id ); // Test if order needs shipping. if ( $order && 0 < count( $order->get_items() ) ) { foreach ( $order->get_items() as $item ) { $_product = $item->get_product(); if ( $_product && $_product->needs_shipping() ) { $needs_shipping = true; break; } } } } $needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); // Virtual order, with virtual disabled. if ( ! $this->enable_for_virtual && ! $needs_shipping ) { return false; } // Only apply if all packages are being shipped via chosen method, or order is virtual. if ( ! empty( $this->enable_for_methods ) && $needs_shipping ) { $order_shipping_items = is_object( $order ) ? $order->get_shipping_methods() : false; $chosen_shipping_methods_session = WC()->session->get( 'chosen_shipping_methods' ); if ( $order_shipping_items ) { $canonical_rate_ids = $this->get_canonical_order_shipping_item_rate_ids( $order_shipping_items ); } else { $canonical_rate_ids = $this->get_canonical_package_rate_ids( $chosen_shipping_methods_session ); } if ( ! count( $this->get_matching_rates( $canonical_rate_ids ) ) ) { return false; } } return parent::is_available(); } /** * Checks to see whether or not the admin settings are being accessed by the current request. * * @return bool */ private function is_accessing_settings() { if ( is_admin() ) { // phpcs:disable WordPress.Security.NonceVerification if ( ! isset( $_REQUEST['page'] ) || 'wc-settings' !== $_REQUEST['page'] ) { return false; } if ( ! isset( $_REQUEST['tab'] ) || 'checkout' !== $_REQUEST['tab'] ) { return false; } if ( ! isset( $_REQUEST['section'] ) || 'cod' !== $_REQUEST['section'] ) { return false; } // phpcs:enable WordPress.Security.NonceVerification return true; } if ( Constants::is_true( 'REST_REQUEST' ) ) { global $wp; if ( isset( $wp->query_vars['rest_route'] ) && false !== strpos( $wp->query_vars['rest_route'], '/payment_gateways' ) ) { return true; } } return false; } /** * Loads all of the shipping method options for the enable_for_methods field. * * @return array */ private function load_shipping_method_options() { // Since this is expensive, we only want to do it if we're actually on the settings page. if ( ! $this->is_accessing_settings() ) { return array(); } $data_store = WC_Data_Store::load( 'shipping-zone' ); $raw_zones = $data_store->get_zones(); foreach ( $raw_zones as $raw_zone ) { $zones[] = new WC_Shipping_Zone( $raw_zone ); } $zones[] = new WC_Shipping_Zone( 0 ); $options = array(); foreach ( WC()->shipping()->load_shipping_methods() as $method ) { $options[ $method->get_method_title() ] = array(); // Translators: %1$s shipping method name. $options[ $method->get_method_title() ][ $method->id ] = sprintf( __( 'Any "%1$s" method', 'woocommerce' ), $method->get_method_title() ); foreach ( $zones as $zone ) { $shipping_method_instances = $zone->get_shipping_methods(); foreach ( $shipping_method_instances as $shipping_method_instance_id => $shipping_method_instance ) { if ( $shipping_method_instance->id !== $method->id ) { continue; } $option_id = $shipping_method_instance->get_rate_id(); // Translators: %1$s shipping method title, %2$s shipping method id. $option_instance_title = sprintf( __( '%1$s (#%2$s)', 'woocommerce' ), $shipping_method_instance->get_title(), $shipping_method_instance_id ); // Translators: %1$s zone name, %2$s shipping method instance name. $option_title = sprintf( __( '%1$s – %2$s', 'woocommerce' ), $zone->get_id() ? $zone->get_zone_name() : __( 'Other locations', 'woocommerce' ), $option_instance_title ); $options[ $method->get_method_title() ][ $option_id ] = $option_title; } } } return $options; } /** * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. * * @since 3.4.0 * * @param array $order_shipping_items Array of WC_Order_Item_Shipping objects. * @return array $canonical_rate_ids Rate IDs in a canonical format. */ private function get_canonical_order_shipping_item_rate_ids( $order_shipping_items ) { $canonical_rate_ids = array(); foreach ( $order_shipping_items as $order_shipping_item ) { $canonical_rate_ids[] = $order_shipping_item->get_method_id() . ':' . $order_shipping_item->get_instance_id(); } return $canonical_rate_ids; } /** * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. * * @since 3.4.0 * * @param array $chosen_package_rate_ids Rate IDs as generated by shipping methods. Can be anything if a shipping method doesn't honor WC conventions. * @return array $canonical_rate_ids Rate IDs in a canonical format. */ private function get_canonical_package_rate_ids( $chosen_package_rate_ids ) { $shipping_packages = WC()->shipping()->get_packages(); $canonical_rate_ids = array(); if ( ! empty( $chosen_package_rate_ids ) && is_array( $chosen_package_rate_ids ) ) { foreach ( $chosen_package_rate_ids as $package_key => $chosen_package_rate_id ) { if ( ! empty( $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ] ) ) { $chosen_rate = $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ]; $canonical_rate_ids[] = $chosen_rate->get_method_id() . ':' . $chosen_rate->get_instance_id(); } } } return $canonical_rate_ids; } /** * Indicates whether a rate exists in an array of canonically-formatted rate IDs that activates this gateway. * * @since 3.4.0 * * @param array $rate_ids Rate ids to check. * @return boolean */ private function get_matching_rates( $rate_ids ) { // First, match entries in 'method_id:instance_id' format. Then, match entries in 'method_id' format by stripping off the instance ID from the candidates. return array_unique( array_merge( array_intersect( $this->enable_for_methods, $rate_ids ), array_intersect( $this->enable_for_methods, array_unique( array_map( 'wc_get_string_before_colon', $rate_ids ) ) ) ) ); } /** * Process the payment and return the result. * * @param int $order_id Order ID. * @return array */ public function process_payment( $order_id ) { $order = wc_get_order( $order_id ); if ( $order->get_total() > 0 ) { // Mark as processing or on-hold (payment won't be taken until delivery). $order->update_status( apply_filters( 'woocommerce_cod_process_payment_order_status', $order->has_downloadable_item() ? 'on-hold' : 'processing', $order ), __( 'Payment to be made upon delivery.', 'woocommerce' ) ); } else { $order->payment_complete(); } // Remove cart. WC()->cart->empty_cart(); // Return thankyou redirect. return array( 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ); } /** * Output for the order received page. */ public function thankyou_page() { if ( $this->instructions ) { echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) ); } } /** * Change payment complete order status to completed for COD orders. * * @since 3.1.0 * @param string $status Current order status. * @param int $order_id Order ID. * @param WC_Order|false $order Order object. * @return string */ public function change_payment_complete_order_status( $status, $order_id = 0, $order = false ) { if ( $order && 'cod' === $order->get_payment_method() ) { $status = 'completed'; } return $status; } /** * Add content to the WC emails. * * @param WC_Order $order Order object. * @param bool $sent_to_admin Sent to admin. * @param bool $plain_text Email format: plain text or HTML. */ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { if ( $this->instructions && ! $sent_to_admin && $this->id === $order->get_payment_method() ) { echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); } } } includes/gateways/bacs/class-wc-gateway-bacs.php 0000644 00000033634 15132754524 0015721 0 ustar 00 <?php /** * Class WC_Gateway_BACS file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Bank Transfer Payment Gateway. * * Provides a Bank Transfer Payment Gateway. Based on code by Mike Pepper. * * @class WC_Gateway_BACS * @extends WC_Payment_Gateway * @version 2.1.0 * @package WooCommerce\Classes\Payment */ class WC_Gateway_BACS extends WC_Payment_Gateway { /** * Array of locales * * @var array */ public $locale; /** * Constructor for the gateway. */ public function __construct() { $this->id = 'bacs'; $this->icon = apply_filters( 'woocommerce_bacs_icon', '' ); $this->has_fields = false; $this->method_title = __( 'Direct bank transfer', 'woocommerce' ); $this->method_description = __( 'Take payments in person via BACS. More commonly known as direct bank/wire transfer.', 'woocommerce' ); // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); $this->instructions = $this->get_option( 'instructions' ); // BACS account fields shown on the thanks page and in emails. $this->account_details = get_option( 'woocommerce_bacs_accounts', array( array( 'account_name' => $this->get_option( 'account_name' ), 'account_number' => $this->get_option( 'account_number' ), 'sort_code' => $this->get_option( 'sort_code' ), 'bank_name' => $this->get_option( 'bank_name' ), 'iban' => $this->get_option( 'iban' ), 'bic' => $this->get_option( 'bic' ), ), ) ); // Actions. add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) ); add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) ); // Customer Emails. add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable bank transfer', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'Direct bank transfer', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), 'default' => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ), 'desc_tip' => true, ), 'instructions' => array( 'title' => __( 'Instructions', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, ), 'account_details' => array( 'type' => 'account_details', ), ); } /** * Generate account details html. * * @return string */ public function generate_account_details_html() { ob_start(); $country = WC()->countries->get_base_country(); $locale = $this->get_country_locale(); // Get sortcode label in the $locale array and use appropriate one. $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); ?> <tr valign="top"> <th scope="row" class="titledesc"><?php esc_html_e( 'Account details:', 'woocommerce' ); ?></th> <td class="forminp" id="bacs_accounts"> <div class="wc_input_table_wrapper"> <table class="widefat wc_input_table sortable" cellspacing="0"> <thead> <tr> <th class="sort"> </th> <th><?php esc_html_e( 'Account name', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Account number', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'Bank name', 'woocommerce' ); ?></th> <th><?php echo esc_html( $sortcode ); ?></th> <th><?php esc_html_e( 'IBAN', 'woocommerce' ); ?></th> <th><?php esc_html_e( 'BIC / Swift', 'woocommerce' ); ?></th> </tr> </thead> <tbody class="accounts"> <?php $i = -1; if ( $this->account_details ) { foreach ( $this->account_details as $account ) { $i++; echo '<tr class="account"> <td class="sort"></td> <td><input type="text" value="' . esc_attr( wp_unslash( $account['account_name'] ) ) . '" name="bacs_account_name[' . esc_attr( $i ) . ']" /></td> <td><input type="text" value="' . esc_attr( $account['account_number'] ) . '" name="bacs_account_number[' . esc_attr( $i ) . ']" /></td> <td><input type="text" value="' . esc_attr( wp_unslash( $account['bank_name'] ) ) . '" name="bacs_bank_name[' . esc_attr( $i ) . ']" /></td> <td><input type="text" value="' . esc_attr( $account['sort_code'] ) . '" name="bacs_sort_code[' . esc_attr( $i ) . ']" /></td> <td><input type="text" value="' . esc_attr( $account['iban'] ) . '" name="bacs_iban[' . esc_attr( $i ) . ']" /></td> <td><input type="text" value="' . esc_attr( $account['bic'] ) . '" name="bacs_bic[' . esc_attr( $i ) . ']" /></td> </tr>'; } } ?> </tbody> <tfoot> <tr> <th colspan="7"><a href="#" class="add button"><?php esc_html_e( '+ Add account', 'woocommerce' ); ?></a> <a href="#" class="remove_rows button"><?php esc_html_e( 'Remove selected account(s)', 'woocommerce' ); ?></a></th> </tr> </tfoot> </table> </div> <script type="text/javascript"> jQuery(function() { jQuery('#bacs_accounts').on( 'click', 'a.add', function(){ var size = jQuery('#bacs_accounts').find('tbody .account').length; jQuery('<tr class="account">\ <td class="sort"></td>\ <td><input type="text" name="bacs_account_name[' + size + ']" /></td>\ <td><input type="text" name="bacs_account_number[' + size + ']" /></td>\ <td><input type="text" name="bacs_bank_name[' + size + ']" /></td>\ <td><input type="text" name="bacs_sort_code[' + size + ']" /></td>\ <td><input type="text" name="bacs_iban[' + size + ']" /></td>\ <td><input type="text" name="bacs_bic[' + size + ']" /></td>\ </tr>').appendTo('#bacs_accounts table tbody'); return false; }); }); </script> </td> </tr> <?php return ob_get_clean(); } /** * Save account details table. */ public function save_account_details() { $accounts = array(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification already handled in WC_Admin_Settings::save() if ( isset( $_POST['bacs_account_name'] ) && isset( $_POST['bacs_account_number'] ) && isset( $_POST['bacs_bank_name'] ) && isset( $_POST['bacs_sort_code'] ) && isset( $_POST['bacs_iban'] ) && isset( $_POST['bacs_bic'] ) ) { $account_names = wc_clean( wp_unslash( $_POST['bacs_account_name'] ) ); $account_numbers = wc_clean( wp_unslash( $_POST['bacs_account_number'] ) ); $bank_names = wc_clean( wp_unslash( $_POST['bacs_bank_name'] ) ); $sort_codes = wc_clean( wp_unslash( $_POST['bacs_sort_code'] ) ); $ibans = wc_clean( wp_unslash( $_POST['bacs_iban'] ) ); $bics = wc_clean( wp_unslash( $_POST['bacs_bic'] ) ); foreach ( $account_names as $i => $name ) { if ( ! isset( $account_names[ $i ] ) ) { continue; } $accounts[] = array( 'account_name' => $account_names[ $i ], 'account_number' => $account_numbers[ $i ], 'bank_name' => $bank_names[ $i ], 'sort_code' => $sort_codes[ $i ], 'iban' => $ibans[ $i ], 'bic' => $bics[ $i ], ); } } // phpcs:enable update_option( 'woocommerce_bacs_accounts', $accounts ); } /** * Output for the order received page. * * @param int $order_id Order ID. */ public function thankyou_page( $order_id ) { if ( $this->instructions ) { echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ) ); } $this->bank_details( $order_id ); } /** * Add content to the WC emails. * * @param WC_Order $order Order object. * @param bool $sent_to_admin Sent to admin. * @param bool $plain_text Email format: plain text or HTML. */ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { if ( ! $sent_to_admin && 'bacs' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { if ( $this->instructions ) { echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); } $this->bank_details( $order->get_id() ); } } /** * Get bank details and place into a list format. * * @param int $order_id Order ID. */ private function bank_details( $order_id = '' ) { if ( empty( $this->account_details ) ) { return; } // Get order and store in $order. $order = wc_get_order( $order_id ); // Get the order country and country $locale. $country = $order->get_billing_country(); $locale = $this->get_country_locale(); // Get sortcode label in the $locale array and use appropriate one. $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); $bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details, $order_id ); if ( ! empty( $bacs_accounts ) ) { $account_html = ''; $has_details = false; foreach ( $bacs_accounts as $bacs_account ) { $bacs_account = (object) $bacs_account; if ( $bacs_account->account_name ) { $account_html .= '<h3 class="wc-bacs-bank-details-account-name">' . wp_kses_post( wp_unslash( $bacs_account->account_name ) ) . ':</h3>' . PHP_EOL; } $account_html .= '<ul class="wc-bacs-bank-details order_details bacs_details">' . PHP_EOL; // BACS account fields shown on the thanks page and in emails. $account_fields = apply_filters( 'woocommerce_bacs_account_fields', array( 'bank_name' => array( 'label' => __( 'Bank', 'woocommerce' ), 'value' => $bacs_account->bank_name, ), 'account_number' => array( 'label' => __( 'Account number', 'woocommerce' ), 'value' => $bacs_account->account_number, ), 'sort_code' => array( 'label' => $sortcode, 'value' => $bacs_account->sort_code, ), 'iban' => array( 'label' => __( 'IBAN', 'woocommerce' ), 'value' => $bacs_account->iban, ), 'bic' => array( 'label' => __( 'BIC', 'woocommerce' ), 'value' => $bacs_account->bic, ), ), $order_id ); foreach ( $account_fields as $field_key => $field ) { if ( ! empty( $field['value'] ) ) { $account_html .= '<li class="' . esc_attr( $field_key ) . '">' . wp_kses_post( $field['label'] ) . ': <strong>' . wp_kses_post( wptexturize( $field['value'] ) ) . '</strong></li>' . PHP_EOL; $has_details = true; } } $account_html .= '</ul>'; } if ( $has_details ) { echo '<section class="woocommerce-bacs-bank-details"><h2 class="wc-bacs-bank-details-heading">' . esc_html__( 'Our bank details', 'woocommerce' ) . '</h2>' . wp_kses_post( PHP_EOL . $account_html ) . '</section>'; } } } /** * Process the payment and return the result. * * @param int $order_id Order ID. * @return array */ public function process_payment( $order_id ) { $order = wc_get_order( $order_id ); if ( $order->get_total() > 0 ) { // Mark as on-hold (we're awaiting the payment). $order->update_status( apply_filters( 'woocommerce_bacs_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) ); } else { $order->payment_complete(); } // Remove cart. WC()->cart->empty_cart(); // Return thankyou redirect. return array( 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ); } /** * Get country locale if localized. * * @return array */ public function get_country_locale() { if ( empty( $this->locale ) ) { // Locale information to be used - only those that are not 'Sort Code'. $this->locale = apply_filters( 'woocommerce_get_bacs_locale', array( 'AU' => array( 'sortcode' => array( 'label' => __( 'BSB', 'woocommerce' ), ), ), 'CA' => array( 'sortcode' => array( 'label' => __( 'Bank transit number', 'woocommerce' ), ), ), 'IN' => array( 'sortcode' => array( 'label' => __( 'IFSC', 'woocommerce' ), ), ), 'IT' => array( 'sortcode' => array( 'label' => __( 'Branch sort', 'woocommerce' ), ), ), 'NZ' => array( 'sortcode' => array( 'label' => __( 'Bank code', 'woocommerce' ), ), ), 'SE' => array( 'sortcode' => array( 'label' => __( 'Bank code', 'woocommerce' ), ), ), 'US' => array( 'sortcode' => array( 'label' => __( 'Routing number', 'woocommerce' ), ), ), 'ZA' => array( 'sortcode' => array( 'label' => __( 'Branch code', 'woocommerce' ), ), ), ) ); } return $this->locale; } } includes/gateways/cheque/class-wc-gateway-cheque.php 0000644 00000010376 15132754524 0016623 0 ustar 00 <?php /** * Class WC_Gateway_Cheque file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Cheque Payment Gateway. * * Provides a Cheque Payment Gateway, mainly for testing purposes. * * @class WC_Gateway_Cheque * @extends WC_Payment_Gateway * @version 2.1.0 * @package WooCommerce\Classes\Payment */ class WC_Gateway_Cheque extends WC_Payment_Gateway { /** * Constructor for the gateway. */ public function __construct() { $this->id = 'cheque'; $this->icon = apply_filters( 'woocommerce_cheque_icon', '' ); $this->has_fields = false; $this->method_title = _x( 'Check payments', 'Check payment method', 'woocommerce' ); $this->method_description = __( 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', 'woocommerce' ); // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); $this->instructions = $this->get_option( 'instructions' ); // Actions. add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_thankyou_cheque', array( $this, 'thankyou_page' ) ); // Customer Emails. add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable check payments', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), 'default' => __( 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', 'woocommerce' ), 'desc_tip' => true, ), 'instructions' => array( 'title' => __( 'Instructions', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, ), ); } /** * Output for the order received page. */ public function thankyou_page() { if ( $this->instructions ) { echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) ); } } /** * Add content to the WC emails. * * @access public * @param WC_Order $order Order object. * @param bool $sent_to_admin Sent to admin. * @param bool $plain_text Email format: plain text or HTML. */ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { if ( $this->instructions && ! $sent_to_admin && 'cheque' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); } } /** * Process the payment and return the result. * * @param int $order_id Order ID. * @return array */ public function process_payment( $order_id ) { $order = wc_get_order( $order_id ); if ( $order->get_total() > 0 ) { // Mark as on-hold (we're awaiting the cheque). $order->update_status( apply_filters( 'woocommerce_cheque_process_payment_order_status', 'on-hold', $order ), _x( 'Awaiting check payment', 'Check payment method', 'woocommerce' ) ); } else { $order->payment_complete(); } // Remove cart. WC()->cart->empty_cart(); // Return thankyou redirect. return array( 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ); } } includes/gateways/class-wc-payment-gateway-cc.php 0000644 00000007461 15132754524 0016140 0 ustar 00 <?php /** * Class WC_Payment_Gateway_CC file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Credit Card Payment Gateway * * @since 2.6.0 * @package WooCommerce\Classes */ class WC_Payment_Gateway_CC extends WC_Payment_Gateway { /** * Builds our payment fields area - including tokenization fields for logged * in users, and the actual payment fields. * * @since 2.6.0 */ public function payment_fields() { if ( $this->supports( 'tokenization' ) && is_checkout() ) { $this->tokenization_script(); $this->saved_payment_methods(); $this->form(); $this->save_payment_method_checkbox(); } else { $this->form(); } } /** * Output field name HTML * * Gateways which support tokenization do not require names - we don't want the data to post to the server. * * @since 2.6.0 * @param string $name Field name. * @return string */ public function field_name( $name ) { return $this->supports( 'tokenization' ) ? '' : ' name="' . esc_attr( $this->id . '-' . $name ) . '" '; } /** * Outputs fields for entering credit card information. * * @since 2.6.0 */ public function form() { wp_enqueue_script( 'wc-credit-card-form' ); $fields = array(); $cvc_field = '<p class="form-row form-row-last"> <label for="' . esc_attr( $this->id ) . '-card-cvc">' . esc_html__( 'Card code', 'woocommerce' ) . ' <span class="required">*</span></label> <input id="' . esc_attr( $this->id ) . '-card-cvc" class="input-text wc-credit-card-form-card-cvc" inputmode="numeric" autocomplete="off" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" maxlength="4" placeholder="' . esc_attr__( 'CVC', 'woocommerce' ) . '" ' . $this->field_name( 'card-cvc' ) . ' style="width:100px" /> </p>'; $default_fields = array( 'card-number-field' => '<p class="form-row form-row-wide"> <label for="' . esc_attr( $this->id ) . '-card-number">' . esc_html__( 'Card number', 'woocommerce' ) . ' <span class="required">*</span></label> <input id="' . esc_attr( $this->id ) . '-card-number" class="input-text wc-credit-card-form-card-number" inputmode="numeric" autocomplete="cc-number" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" placeholder="•••• •••• •••• ••••" ' . $this->field_name( 'card-number' ) . ' /> </p>', 'card-expiry-field' => '<p class="form-row form-row-first"> <label for="' . esc_attr( $this->id ) . '-card-expiry">' . esc_html__( 'Expiry (MM/YY)', 'woocommerce' ) . ' <span class="required">*</span></label> <input id="' . esc_attr( $this->id ) . '-card-expiry" class="input-text wc-credit-card-form-card-expiry" inputmode="numeric" autocomplete="cc-exp" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" placeholder="' . esc_attr__( 'MM / YY', 'woocommerce' ) . '" ' . $this->field_name( 'card-expiry' ) . ' /> </p>', ); if ( ! $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) { $default_fields['card-cvc-field'] = $cvc_field; } $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_credit_card_form_fields', $default_fields, $this->id ) ); ?> <fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-cc-form" class='wc-credit-card-form wc-payment-form'> <?php do_action( 'woocommerce_credit_card_form_start', $this->id ); ?> <?php foreach ( $fields as $field ) { echo $field; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } ?> <?php do_action( 'woocommerce_credit_card_form_end', $this->id ); ?> <div class="clear"></div> </fieldset> <?php if ( $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) { echo '<fieldset>' . $cvc_field . '</fieldset>'; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } } } includes/gateways/class-wc-payment-gateway-echeck.php 0000644 00000004516 15132754524 0016773 0 ustar 00 <?php /** * Class WC_Payment_Gateway_eCheck file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class for eCheck Payment Gateway * * @since 2.6.0 * @package WooCommerce\Classes */ class WC_Payment_Gateway_ECheck extends WC_Payment_Gateway { /** * Builds our payment fields area - including tokenization fields for logged * in users, and the actual payment fields. * * @since 2.6.0 */ public function payment_fields() { if ( $this->supports( 'tokenization' ) && is_checkout() ) { $this->tokenization_script(); $this->saved_payment_methods(); $this->form(); $this->save_payment_method_checkbox(); } else { $this->form(); } } /** * Outputs fields for entering eCheck information. * * @since 2.6.0 */ public function form() { $fields = array(); $default_fields = array( 'routing-number' => '<p class="form-row form-row-first"> <label for="' . esc_attr( $this->id ) . '-routing-number">' . esc_html__( 'Routing number', 'woocommerce' ) . ' <span class="required">*</span></label> <input id="' . esc_attr( $this->id ) . '-routing-number" class="input-text wc-echeck-form-routing-number" type="text" maxlength="9" autocomplete="off" placeholder="•••••••••" name="' . esc_attr( $this->id ) . '-routing-number" /> </p>', 'account-number' => '<p class="form-row form-row-wide"> <label for="' . esc_attr( $this->id ) . '-account-number">' . esc_html__( 'Account number', 'woocommerce' ) . ' <span class="required">*</span></label> <input id="' . esc_attr( $this->id ) . '-account-number" class="input-text wc-echeck-form-account-number" type="text" autocomplete="off" name="' . esc_attr( $this->id ) . '-account-number" maxlength="17" /> </p>', ); $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_echeck_form_fields', $default_fields, $this->id ) ); ?> <fieldset id="<?php echo esc_attr( $this->id ); ?>-cc-form" class='wc-echeck-form wc-payment-form'> <?php do_action( 'woocommerce_echeck_form_start', $this->id ); ?> <?php foreach ( $fields as $field ) { echo $field; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } ?> <?php do_action( 'woocommerce_echeck_form_end', $this->id ); ?> <div class="clear"></div> </fieldset> <?php } } includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php 0000644 00000044174 15132754524 0022152 0 ustar 00 <?php /** * Class WC_Gateway_Paypal_Request file. * * @package WooCommerce\Gateways */ use Automattic\WooCommerce\Utilities\NumberUtil; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Generates requests to send to PayPal. */ class WC_Gateway_Paypal_Request { /** * Stores line items to send to PayPal. * * @var array */ protected $line_items = array(); /** * Pointer to gateway making the request. * * @var WC_Gateway_Paypal */ protected $gateway; /** * Endpoint for requests from PayPal. * * @var string */ protected $notify_url; /** * Endpoint for requests to PayPal. * * @var string */ protected $endpoint; /** * Constructor. * * @param WC_Gateway_Paypal $gateway Paypal gateway object. */ public function __construct( $gateway ) { $this->gateway = $gateway; $this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' ); } /** * Get the PayPal request URL for an order. * * @param WC_Order $order Order object. * @param bool $sandbox Whether to use sandbox mode or not. * @return string */ public function get_request_url( $order, $sandbox = false ) { $this->endpoint = $sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' : 'https://www.paypal.com/cgi-bin/webscr?'; $paypal_args = $this->get_paypal_args( $order ); $paypal_args['bn'] = 'WooThemes_Cart'; // Append WooCommerce PayPal Partner Attribution ID. This should not be overridden for this gateway. // Mask (remove) PII from the logs. $mask = array( 'first_name' => '***', 'last_name' => '***', 'address1' => '***', 'address2' => '***', 'city' => '***', 'state' => '***', 'zip' => '***', 'country' => '***', 'email' => '***@***', 'night_phone_a' => '***', 'night_phone_b' => '***', 'night_phone_c' => '***', ); WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( array_merge( $paypal_args, array_intersect_key( $mask, $paypal_args ) ), true ) ); return $this->endpoint . http_build_query( $paypal_args, '', '&' ); } /** * Limit length of an arg. * * @param string $string Argument to limit. * @param integer $limit Limit size in characters. * @return string */ protected function limit_length( $string, $limit = 127 ) { $str_limit = $limit - 3; if ( function_exists( 'mb_strimwidth' ) ) { if ( mb_strlen( $string ) > $limit ) { $string = mb_strimwidth( $string, 0, $str_limit ) . '...'; } } else { if ( strlen( $string ) > $limit ) { $string = substr( $string, 0, $str_limit ) . '...'; } } return $string; } /** * Get transaction args for paypal request, except for line item args. * * @param WC_Order $order Order object. * @return array */ protected function get_transaction_args( $order ) { return array_merge( array( 'cmd' => '_cart', 'business' => $this->gateway->get_option( 'email' ), 'no_note' => 1, 'currency_code' => get_woocommerce_currency(), 'charset' => 'utf-8', 'rm' => is_ssl() ? 2 : 1, 'upload' => 1, 'return' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ), 'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ), 'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ), 'paymentaction' => $this->gateway->get_option( 'paymentaction' ), 'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ), 'custom' => wp_json_encode( array( 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key(), ) ), 'notify_url' => $this->limit_length( $this->notify_url, 255 ), 'first_name' => $this->limit_length( $order->get_billing_first_name(), 32 ), 'last_name' => $this->limit_length( $order->get_billing_last_name(), 64 ), 'address1' => $this->limit_length( $order->get_billing_address_1(), 100 ), 'address2' => $this->limit_length( $order->get_billing_address_2(), 100 ), 'city' => $this->limit_length( $order->get_billing_city(), 40 ), 'state' => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ), 'zip' => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ), 'country' => $this->limit_length( $order->get_billing_country(), 2 ), 'email' => $this->limit_length( $order->get_billing_email() ), ), $this->get_phone_number_args( $order ), $this->get_shipping_args( $order ) ); } /** * If the default request with line items is too long, generate a new one with only one line item. * * If URL is longer than 2,083 chars, ignore line items and send cart to Paypal as a single item. * One item's name can only be 127 characters long, so the URL should not be longer than limit. * URL character limit via: * https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer. * * @param WC_Order $order Order to be sent to Paypal. * @param array $paypal_args Arguments sent to Paypal in the request. * @return array */ protected function fix_request_length( $order, $paypal_args ) { $max_paypal_length = 2083; $query_candidate = http_build_query( $paypal_args, '', '&' ); if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) { return $paypal_args; } return apply_filters( 'woocommerce_paypal_args', array_merge( $this->get_transaction_args( $order ), $this->get_line_item_args( $order, true ) ), $order ); } /** * Get PayPal Args for passing to PP. * * @param WC_Order $order Order object. * @return array */ protected function get_paypal_args( $order ) { WC_Gateway_Paypal::log( 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url ); $force_one_line_item = apply_filters( 'woocommerce_paypal_force_one_line_item', false, $order ); if ( ( wc_tax_enabled() && wc_prices_include_tax() ) || ! $this->line_items_valid( $order ) ) { $force_one_line_item = true; } $paypal_args = apply_filters( 'woocommerce_paypal_args', array_merge( $this->get_transaction_args( $order ), $this->get_line_item_args( $order, $force_one_line_item ) ), $order ); return $this->fix_request_length( $order, $paypal_args ); } /** * Get phone number args for paypal request. * * @param WC_Order $order Order object. * @return array */ protected function get_phone_number_args( $order ) { $phone_number = wc_sanitize_phone_number( $order->get_billing_phone() ); if ( in_array( $order->get_billing_country(), array( 'US', 'CA' ), true ) ) { $phone_number = ltrim( $phone_number, '+1' ); $phone_args = array( 'night_phone_a' => substr( $phone_number, 0, 3 ), 'night_phone_b' => substr( $phone_number, 3, 3 ), 'night_phone_c' => substr( $phone_number, 6, 4 ), ); } else { $calling_code = WC()->countries->get_country_calling_code( $order->get_billing_country() ); $calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code; if ( $calling_code ) { $phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $order->get_billing_phone() ) ); } $phone_args = array( 'night_phone_a' => $calling_code, 'night_phone_b' => $phone_number, ); } return $phone_args; } /** * Get shipping args for paypal request. * * @param WC_Order $order Order object. * @return array */ protected function get_shipping_args( $order ) { $shipping_args = array(); if ( $order->needs_shipping_address() ) { $shipping_args['address_override'] = $this->gateway->get_option( 'address_override' ) === 'yes' ? 1 : 0; $shipping_args['no_shipping'] = 0; if ( 'yes' === $this->gateway->get_option( 'send_shipping' ) ) { // If we are sending shipping, send shipping address instead of billing. $shipping_args['first_name'] = $this->limit_length( $order->get_shipping_first_name(), 32 ); $shipping_args['last_name'] = $this->limit_length( $order->get_shipping_last_name(), 64 ); $shipping_args['address1'] = $this->limit_length( $order->get_shipping_address_1(), 100 ); $shipping_args['address2'] = $this->limit_length( $order->get_shipping_address_2(), 100 ); $shipping_args['city'] = $this->limit_length( $order->get_shipping_city(), 40 ); $shipping_args['state'] = $this->get_paypal_state( $order->get_shipping_country(), $order->get_shipping_state() ); $shipping_args['country'] = $this->limit_length( $order->get_shipping_country(), 2 ); $shipping_args['zip'] = $this->limit_length( wc_format_postcode( $order->get_shipping_postcode(), $order->get_shipping_country() ), 32 ); } } else { $shipping_args['no_shipping'] = 1; } return $shipping_args; } /** * Get shipping cost line item args for paypal request. * * @param WC_Order $order Order object. * @param bool $force_one_line_item Whether one line item was forced by validation or URL length. * @return array */ protected function get_shipping_cost_line_item( $order, $force_one_line_item ) { $line_item_args = array(); $shipping_total = $order->get_shipping_total(); if ( $force_one_line_item ) { $shipping_total += $order->get_shipping_tax(); } // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). // We also check that shipping is not the **only** cost as PayPal won't allow payment // if the items have no cost. if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { $line_item_args['shipping_1'] = $this->number_format( $shipping_total, $order ); } elseif ( $order->get_shipping_total() > 0 ) { /* translators: %s: Order shipping method */ $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $shipping_total, $order ) ); } return $line_item_args; } /** * Get line item args for paypal request as a single line item. * * @param WC_Order $order Order object. * @return array */ protected function get_line_item_args_single_item( $order ) { $this->delete_line_items(); $all_items_name = $this->get_order_item_names( $order ); $this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() ); $line_item_args = $this->get_shipping_cost_line_item( $order, true ); return array_merge( $line_item_args, $this->get_line_items() ); } /** * Get line item args for paypal request. * * @param WC_Order $order Order object. * @param bool $force_one_line_item Create only one item for this order. * @return array */ protected function get_line_item_args( $order, $force_one_line_item = false ) { $line_item_args = array(); if ( $force_one_line_item ) { /** * Send order as a single item. * * For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max). */ $line_item_args = $this->get_line_item_args_single_item( $order ); } else { /** * Passing a line item per product if supported. */ $this->prepare_line_items( $order ); $line_item_args['tax_cart'] = $this->number_format( $order->get_total_tax(), $order ); if ( $order->get_total_discount() > 0 ) { $line_item_args['discount_amount_cart'] = $this->number_format( $this->round( $order->get_total_discount(), $order ), $order ); } $line_item_args = array_merge( $line_item_args, $this->get_shipping_cost_line_item( $order, false ) ); $line_item_args = array_merge( $line_item_args, $this->get_line_items() ); } return $line_item_args; } /** * Get order item names as a string. * * @param WC_Order $order Order object. * @return string */ protected function get_order_item_names( $order ) { $item_names = array(); foreach ( $order->get_items() as $item ) { $item_name = $item->get_name(); $item_meta = wp_strip_all_tags( wc_display_item_meta( $item, array( 'before' => '', 'separator' => ', ', 'after' => '', 'echo' => false, 'autop' => false, ) ) ); if ( $item_meta ) { $item_name .= ' (' . $item_meta . ')'; } $item_names[] = $item_name . ' x ' . $item->get_quantity(); } return apply_filters( 'woocommerce_paypal_get_order_item_names', implode( ', ', $item_names ), $order ); } /** * Get order item names as a string. * * @param WC_Order $order Order object. * @param WC_Order_Item $item Order item object. * @return string */ protected function get_order_item_name( $order, $item ) { $item_name = $item->get_name(); $item_meta = wp_strip_all_tags( wc_display_item_meta( $item, array( 'before' => '', 'separator' => ', ', 'after' => '', 'echo' => false, 'autop' => false, ) ) ); if ( $item_meta ) { $item_name .= ' (' . $item_meta . ')'; } return apply_filters( 'woocommerce_paypal_get_order_item_name', $item_name, $order, $item ); } /** * Return all line items. */ protected function get_line_items() { return $this->line_items; } /** * Remove all line items. */ protected function delete_line_items() { $this->line_items = array(); } /** * Check if the order has valid line items to use for PayPal request. * * The line items are invalid in case of mismatch in totals or if any amount < 0. * * @param WC_Order $order Order to be examined. * @return bool */ protected function line_items_valid( $order ) { $negative_item_amount = false; $calculated_total = 0; // Products. foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { if ( 'fee' === $item['type'] ) { $item_line_total = $this->number_format( $item['line_total'], $order ); $calculated_total += $item_line_total; } else { $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); $calculated_total += $item_line_total * $item->get_quantity(); } if ( $item_line_total < 0 ) { $negative_item_amount = true; } } $mismatched_totals = $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) !== $this->number_format( $order->get_total(), $order ); return ! $negative_item_amount && ! $mismatched_totals; } /** * Get line items to send to paypal. * * @param WC_Order $order Order object. */ protected function prepare_line_items( $order ) { $this->delete_line_items(); // Products. foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { if ( 'fee' === $item['type'] ) { $item_line_total = $this->number_format( $item['line_total'], $order ); $this->add_line_item( $item->get_name(), 1, $item_line_total ); } else { $product = $item->get_product(); $sku = $product ? $product->get_sku() : ''; $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); $this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku ); } } } /** * Add PayPal Line Item. * * @param string $item_name Item name. * @param int $quantity Item quantity. * @param float $amount Amount. * @param string $item_number Item number. */ protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) { $index = ( count( $this->line_items ) / 4 ) + 1; $item = apply_filters( 'woocommerce_paypal_line_item', array( 'item_name' => html_entity_decode( wc_trim_string( $item_name ? wp_strip_all_tags( $item_name ) : __( 'Item', 'woocommerce' ), 127 ), ENT_NOQUOTES, 'UTF-8' ), 'quantity' => (int) $quantity, 'amount' => wc_float_to_string( (float) $amount ), 'item_number' => $item_number, ), $item_name, $quantity, $amount, $item_number ); $this->line_items[ 'item_name_' . $index ] = $this->limit_length( $item['item_name'], 127 ); $this->line_items[ 'quantity_' . $index ] = $item['quantity']; $this->line_items[ 'amount_' . $index ] = $item['amount']; $this->line_items[ 'item_number_' . $index ] = $this->limit_length( $item['item_number'], 127 ); } /** * Get the state to send to paypal. * * @param string $cc Country two letter code. * @param string $state State code. * @return string */ protected function get_paypal_state( $cc, $state ) { if ( 'US' === $cc ) { return $state; } $states = WC()->countries->get_states( $cc ); if ( isset( $states[ $state ] ) ) { return $states[ $state ]; } return $state; } /** * Check if currency has decimals. * * @param string $currency Currency to check. * @return bool */ protected function currency_has_decimals( $currency ) { if ( in_array( $currency, array( 'HUF', 'JPY', 'TWD' ), true ) ) { return false; } return true; } /** * Round prices. * * @param double $price Price to round. * @param WC_Order $order Order object. * @return double */ protected function round( $price, $order ) { $precision = 2; if ( ! $this->currency_has_decimals( $order->get_currency() ) ) { $precision = 0; } return NumberUtil::round( $price, $precision ); } /** * Format prices. * * @param float|int $price Price to format. * @param WC_Order $order Order object. * @return string */ protected function number_format( $price, $order ) { $decimals = 2; if ( ! $this->currency_has_decimals( $order->get_currency() ) ) { $decimals = 0; } return number_format( $price, $decimals, '.', '' ); } } includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php 0000644 00000013043 15132754524 0022635 0 ustar 00 <?php /** * Class WC_Gateway_Paypal_API_Handler file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Handles Refunds and other API requests such as capture. * * @since 3.0.0 */ class WC_Gateway_Paypal_API_Handler { /** * API Username * * @var string */ public static $api_username; /** * API Password * * @var string */ public static $api_password; /** * API Signature * * @var string */ public static $api_signature; /** * Sandbox * * @var bool */ public static $sandbox = false; /** * Get capture request args. * See https://developer.paypal.com/docs/classic/api/merchant/DoCapture_API_Operation_NVP/. * * @param WC_Order $order Order object. * @param float $amount Amount. * @return array */ public static function get_capture_request( $order, $amount = null ) { $request = array( 'VERSION' => '84.0', 'SIGNATURE' => self::$api_signature, 'USER' => self::$api_username, 'PWD' => self::$api_password, 'METHOD' => 'DoCapture', 'AUTHORIZATIONID' => $order->get_transaction_id(), 'AMT' => number_format( is_null( $amount ) ? $order->get_total() : $amount, 2, '.', '' ), 'CURRENCYCODE' => $order->get_currency(), 'COMPLETETYPE' => 'Complete', ); return apply_filters( 'woocommerce_paypal_capture_request', $request, $order, $amount ); } /** * Get refund request args. * * @param WC_Order $order Order object. * @param float $amount Refund amount. * @param string $reason Refund reason. * @return array */ public static function get_refund_request( $order, $amount = null, $reason = '' ) { $request = array( 'VERSION' => '84.0', 'SIGNATURE' => self::$api_signature, 'USER' => self::$api_username, 'PWD' => self::$api_password, 'METHOD' => 'RefundTransaction', 'TRANSACTIONID' => $order->get_transaction_id(), 'NOTE' => html_entity_decode( wc_trim_string( $reason, 255 ), ENT_NOQUOTES, 'UTF-8' ), 'REFUNDTYPE' => 'Full', ); if ( ! is_null( $amount ) ) { $request['AMT'] = number_format( $amount, 2, '.', '' ); $request['CURRENCYCODE'] = $order->get_currency(); $request['REFUNDTYPE'] = 'Partial'; } return apply_filters( 'woocommerce_paypal_refund_request', $request, $order, $amount, $reason ); } /** * Capture an authorization. * * @param WC_Order $order Order object. * @param float $amount Amount. * @return object Either an object of name value pairs for a success, or a WP_ERROR object. */ public static function do_capture( $order, $amount = null ) { $raw_response = wp_safe_remote_post( self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp', array( 'method' => 'POST', 'body' => self::get_capture_request( $order, $amount ), 'timeout' => 70, 'user-agent' => 'WooCommerce/' . WC()->version, 'httpversion' => '1.1', ) ); WC_Gateway_Paypal::log( 'DoCapture Response: ' . wc_print_r( $raw_response, true ) ); if ( is_wp_error( $raw_response ) ) { return $raw_response; } elseif ( empty( $raw_response['body'] ) ) { return new WP_Error( 'paypal-api', 'Empty Response' ); } parse_str( $raw_response['body'], $response ); return (object) $response; } /** * Refund an order via PayPal. * * @param WC_Order $order Order object. * @param float $amount Refund amount. * @param string $reason Refund reason. * @return object Either an object of name value pairs for a success, or a WP_ERROR object. */ public static function refund_transaction( $order, $amount = null, $reason = '' ) { $raw_response = wp_safe_remote_post( self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp', array( 'method' => 'POST', 'body' => self::get_refund_request( $order, $amount, $reason ), 'timeout' => 70, 'user-agent' => 'WooCommerce/' . WC()->version, 'httpversion' => '1.1', ) ); WC_Gateway_Paypal::log( 'Refund Response: ' . wc_print_r( $raw_response, true ) ); if ( is_wp_error( $raw_response ) ) { return $raw_response; } elseif ( empty( $raw_response['body'] ) ) { return new WP_Error( 'paypal-api', 'Empty Response' ); } parse_str( $raw_response['body'], $response ); return (object) $response; } } /** * Here for backwards compatibility. * * @since 3.0.0 */ class WC_Gateway_Paypal_Refund extends WC_Gateway_Paypal_API_Handler { /** * Get refund request args. Proxy to WC_Gateway_Paypal_API_Handler::get_refund_request(). * * @param WC_Order $order Order object. * @param float $amount Refund amount. * @param string $reason Refund reason. * * @return array */ public static function get_request( $order, $amount = null, $reason = '' ) { return self::get_refund_request( $order, $amount, $reason ); } /** * Process an order refund. * * @param WC_Order $order Order object. * @param float $amount Refund amount. * @param string $reason Refund reason. * @param bool $sandbox Whether to use sandbox mode or not. * @return object Either an object of name value pairs for a success, or a WP_ERROR object. */ public static function refund_order( $order, $amount = null, $reason = '', $sandbox = false ) { if ( $sandbox ) { self::$sandbox = $sandbox; } $result = self::refund_transaction( $order, $amount, $reason ); if ( is_wp_error( $result ) ) { return $result; } else { return (array) $result; } } } includes/gateways/paypal/includes/settings-paypal.php 0000644 00000020217 15132754524 0017141 0 ustar 00 <?php /** * Settings for PayPal Standard Gateway. * * @package WooCommerce\Classes\Payment */ defined( 'ABSPATH' ) || exit; return array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable PayPal Standard', 'woocommerce' ), 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), 'default' => __( 'PayPal', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'text', 'desc_tip' => true, 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ), ), 'email' => array( 'title' => __( 'PayPal email', 'woocommerce' ), 'type' => 'email', 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), 'default' => get_option( 'admin_email' ), 'desc_tip' => true, 'placeholder' => 'you@youremail.com', ), 'advanced' => array( 'title' => __( 'Advanced options', 'woocommerce' ), 'type' => 'title', 'description' => '', ), 'testmode' => array( 'title' => __( 'PayPal sandbox', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), 'default' => 'no', /* translators: %s: URL */ 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a <a href="%s">developer account</a>.', 'woocommerce' ), 'https://developer.paypal.com/' ), ), 'debug' => array( 'title' => __( 'Debug log', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable logging', 'woocommerce' ), 'default' => 'no', /* translators: %s: URL */ 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '<code>' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '</code>' ), ), 'ipn_notification' => array( 'title' => __( 'IPN email notifications', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable IPN email notifications', 'woocommerce' ), 'default' => 'yes', 'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ), ), 'receiver_email' => array( 'title' => __( 'Receiver email', 'woocommerce' ), 'type' => 'email', 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => 'you@youremail.com', ), 'identity_token' => array( 'title' => __( 'PayPal identity token', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => '', ), 'invoice_prefix' => array( 'title' => __( 'Invoice prefix', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), 'default' => 'WC-', 'desc_tip' => true, ), 'send_shipping' => array( 'title' => __( 'Shipping details', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), 'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ), 'default' => 'yes', ), 'address_override' => array( 'title' => __( 'Address override', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), 'default' => 'no', ), 'paymentaction' => array( 'title' => __( 'Payment action', 'woocommerce' ), 'type' => 'select', 'class' => 'wc-enhanced-select', 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), 'default' => 'sale', 'desc_tip' => true, 'options' => array( 'sale' => __( 'Capture', 'woocommerce' ), 'authorization' => __( 'Authorize', 'woocommerce' ), ), ), 'image_url' => array( 'title' => __( 'Image url', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'api_details' => array( 'title' => __( 'API credentials', 'woocommerce' ), 'type' => 'title', /* translators: %s: URL */ 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your <a href="%s">PayPal API Credentials</a>.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ), ), 'api_username' => array( 'title' => __( 'Live API username', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'api_password' => array( 'title' => __( 'Live API password', 'woocommerce' ), 'type' => 'password', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'api_signature' => array( 'title' => __( 'Live API signature', 'woocommerce' ), 'type' => 'password', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'sandbox_api_username' => array( 'title' => __( 'Sandbox API username', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'sandbox_api_password' => array( 'title' => __( 'Sandbox API password', 'woocommerce' ), 'type' => 'password', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), 'sandbox_api_signature' => array( 'title' => __( 'Sandbox API signature', 'woocommerce' ), 'type' => 'password', 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), 'default' => '', 'desc_tip' => true, 'placeholder' => __( 'Optional', 'woocommerce' ), ), ); includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php 0000644 00000014343 15132754524 0022657 0 ustar 00 <?php /** * Class WC_Gateway_Paypal_PDT_Handler file. * * @package WooCommerce\Gateways */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php'; /** * Handle PDT Responses from PayPal. */ class WC_Gateway_Paypal_PDT_Handler extends WC_Gateway_Paypal_Response { /** * Identity token for PDT support * * @var string */ protected $identity_token; /** * Receiver email address to validate. * * @var string Receiver email address. */ protected $receiver_email; /** * Constructor. * * @param bool $sandbox Whether to use sandbox mode or not. * @param string $identity_token Identity token for PDT support. */ public function __construct( $sandbox = false, $identity_token = '' ) { add_action( 'woocommerce_thankyou_paypal', array( $this, 'check_response_for_order' ) ); $this->identity_token = $identity_token; $this->sandbox = $sandbox; } /** * Set receiver email to enable more strict validation. * * @param string $receiver_email Email to receive PDT notification from. */ public function set_receiver_email( $receiver_email = '' ) { $this->receiver_email = $receiver_email; } /** * Validate a PDT transaction to ensure its authentic. * * @param string $transaction TX ID. * @return bool|array False or result array if successful and valid. */ protected function validate_transaction( $transaction ) { $pdt = array( 'body' => array( 'cmd' => '_notify-synch', 'tx' => $transaction, 'at' => $this->identity_token, ), 'timeout' => 60, 'httpversion' => '1.1', 'user-agent' => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ), ); // Post back to get a response. $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt ); if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) { return false; } // Parse transaction result data. $transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) ); $transaction_results = array(); foreach ( $transaction_result as $line ) { $line = explode( '=', $line ); $transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : ''; } if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) { foreach ( $transaction_results as $key => $value ) { $transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value ); } } return $transaction_results; } /** * Check Response for PDT, taking the order id from the request. * * @deprecated 6.4 Use check_response_for_order instead. */ public function check_response() { global $wp; $order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $wp->query_vars['order-received'] ) ); $this->check_response_for_order( $order_id ); } /** * Check Response for PDT. * * @since 6.4 * * @param mixed $wc_order_id The order id to check the response against. */ public function check_response_for_order( $wc_order_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_REQUEST['tx'] ) ) { return; } $wc_order = wc_get_order( $wc_order_id ); if ( ! $wc_order->needs_payment() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $transaction = wc_clean( wp_unslash( $_REQUEST['tx'] ) ); $transaction_result = $this->validate_transaction( $transaction ); if ( $transaction_result ) { $status = strtolower( $transaction_result['payment_status'] ); $amount = isset( $transaction_result['mc_gross'] ) ? $transaction_result['mc_gross'] : 0; $order = $this->get_paypal_order( $transaction_result['custom'] ); if ( ! $order ) { // No valid WC order found on tx data. return; } if ( $wc_order->get_id() !== $order->get_id() ) { /* translators: 1: order ID, 2: order ID. */ WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for order %1$d on endpoint for order %2$d.', 'woocommerce' ), $order->get_id(), $wc_order_id ), 'error' ); return; } if ( 0 !== strcasecmp( trim( $transaction_result['receiver_email'] ), trim( $this->receiver_email ) ) ) { /* translators: 1: email address, 2: order ID . */ WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for another account: %1$s. Order ID: %2$d.', 'woocommerce' ), $transaction_result['receiver_email'], $order->get_id() ), 'error' ); return; } // We have a valid response from PayPal. WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) ); $order->add_meta_data( '_paypal_status', $status ); $order->set_transaction_id( $transaction ); if ( 'completed' === $status ) { if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) { WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' ); /* translators: 1: Payment amount */ $this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) ); } else { // Log paypal transaction fee and payment type. if ( ! empty( $transaction_result['mc_fee'] ) ) { $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) ); } if ( ! empty( $transaction_result['payment_type'] ) ) { $order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) ); } $this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) ); } } else { if ( 'authorization' === $transaction_result['pending_reason'] ) { $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); } else { /* translators: 1: Pending reason */ $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) ); } } } else { WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' ); } } } includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php 0000644 00000004077 15132754524 0022316 0 ustar 00 <?php /** * Class WC_Gateway_Paypal_Response file. * * @package WooCommerce\Gateways */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Handles Responses. */ abstract class WC_Gateway_Paypal_Response { /** * Sandbox mode * * @var bool */ protected $sandbox = false; /** * Get the order from the PayPal 'Custom' variable. * * @param string $raw_custom JSON Data passed back by PayPal. * @return bool|WC_Order object */ protected function get_paypal_order( $raw_custom ) { // We have the data in the correct format, so get the order. $custom = json_decode( $raw_custom ); if ( $custom && is_object( $custom ) ) { $order_id = $custom->order_id; $order_key = $custom->order_key; } else { // Nothing was found. WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' ); return false; } $order = wc_get_order( $order_id ); if ( ! $order ) { // We have an invalid $order_id, probably because invoice_prefix has changed. $order_id = wc_get_order_id_by_order_key( $order_key ); $order = wc_get_order( $order_id ); } if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) { WC_Gateway_Paypal::log( 'Order Keys do not match.', 'error' ); return false; } return $order; } /** * Complete order, add transaction ID and note. * * @param WC_Order $order Order object. * @param string $txn_id Transaction ID. * @param string $note Payment note. */ protected function payment_complete( $order, $txn_id = '', $note = '' ) { if ( ! $order->has_status( array( 'processing', 'completed' ) ) ) { $order->add_order_note( $note ); $order->payment_complete( $txn_id ); if ( isset( WC()->cart ) ) { WC()->cart->empty_cart(); } } } /** * Hold order and add note. * * @param WC_Order $order Order object. * @param string $reason Reason why the payment is on hold. */ protected function payment_on_hold( $order, $reason = '' ) { $order->update_status( 'on-hold', $reason ); if ( isset( WC()->cart ) ) { WC()->cart->empty_cart(); } } } includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php 0000644 00000032273 15132754524 0022660 0 ustar 00 <?php /** * Handles responses from PayPal IPN. * * @package WooCommerce\PayPal * @version 3.3.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php'; /** * WC_Gateway_Paypal_IPN_Handler class. */ class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response { /** * Receiver email address to validate. * * @var string Receiver email address. */ protected $receiver_email; /** * Constructor. * * @param bool $sandbox Use sandbox or not. * @param string $receiver_email Email to receive IPN from. */ public function __construct( $sandbox = false, $receiver_email = '' ) { add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_response' ) ); add_action( 'valid-paypal-standard-ipn-request', array( $this, 'valid_response' ) ); $this->receiver_email = $receiver_email; $this->sandbox = $sandbox; } /** * Check for PayPal IPN Response. */ public function check_response() { if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok. $posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok. // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores do_action( 'valid-paypal-standard-ipn-request', $posted ); exit; } wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) ); } /** * There was a valid response. * * @param array $posted Post data after wp_unslash. */ public function valid_response( $posted ) { $order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false; if ( $order ) { // Lowercase returned variables. $posted['payment_status'] = strtolower( $posted['payment_status'] ); WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() ); WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] ); if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) { call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted ); } } } /** * Check PayPal IPN validity. */ public function validate_ipn() { WC_Gateway_Paypal::log( 'Checking IPN response is valid' ); // Get received values from post data. $validate_ipn = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok. $validate_ipn['cmd'] = '_notify-validate'; // Send back post vars to paypal. $params = array( 'body' => $validate_ipn, 'timeout' => 60, 'httpversion' => '1.1', 'compress' => false, 'decompress' => false, 'user-agent' => 'WooCommerce/' . WC()->version, ); // Post back to get a response. $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params ); WC_Gateway_Paypal::log( 'IPN Response: ' . wc_print_r( $response, true ) ); // Check to see if the request was valid. if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) { WC_Gateway_Paypal::log( 'Received valid response from PayPal IPN' ); return true; } WC_Gateway_Paypal::log( 'Received invalid response from PayPal IPN' ); if ( is_wp_error( $response ) ) { WC_Gateway_Paypal::log( 'Error response: ' . $response->get_error_message() ); } return false; } /** * Check for a valid transaction type. * * @param string $txn_type Transaction type. */ protected function validate_transaction_type( $txn_type ) { $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money', 'paypal_here' ); if ( ! in_array( strtolower( $txn_type ), $accepted_types, true ) ) { WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type ); exit; } } /** * Check currency from IPN matches the order. * * @param WC_Order $order Order object. * @param string $currency Currency code. */ protected function validate_currency( $order, $currency ) { if ( $order->get_currency() !== $currency ) { WC_Gateway_Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' ); /* translators: %s: currency code. */ $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $currency ) ); exit; } } /** * Check payment amount from IPN matches the order. * * @param WC_Order $order Order object. * @param int $amount Amount to validate. */ protected function validate_amount( $order, $amount ) { if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) { WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' ); /* translators: %s: Amount. */ $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $amount ) ); exit; } } /** * Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in. * WooCommerce -> Settings -> Checkout -> PayPal, it will log an error about it. * * @param WC_Order $order Order object. * @param string $receiver_email Email to validate. */ protected function validate_receiver_email( $order, $receiver_email ) { if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) !== 0 ) { WC_Gateway_Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" ); /* translators: %s: email address . */ $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $receiver_email ) ); exit; } } /** * Handle a completed payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_completed( $order, $posted ) { if ( $order->has_status( wc_get_is_paid_statuses() ) ) { WC_Gateway_Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' ); exit; } $this->validate_transaction_type( $posted['txn_type'] ); $this->validate_currency( $order, $posted['mc_currency'] ); $this->validate_amount( $order, $posted['mc_gross'] ); $this->validate_receiver_email( $order, $posted['receiver_email'] ); $this->save_paypal_meta_data( $order, $posted ); if ( 'completed' === $posted['payment_status'] ) { if ( $order->has_status( 'cancelled' ) ) { $this->payment_status_paid_cancelled_order( $order, $posted ); } if ( ! empty( $posted['mc_fee'] ) ) { $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) ); } $this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) ); } else { if ( 'authorization' === $posted['pending_reason'] ) { $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); } else { /* translators: %s: pending reason. */ $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) ); } } } /** * Handle a pending payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_pending( $order, $posted ) { $this->payment_status_completed( $order, $posted ); } /** * Handle a failed payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_failed( $order, $posted ) { /* translators: %s: payment status. */ $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); } /** * Handle a denied payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_denied( $order, $posted ) { $this->payment_status_failed( $order, $posted ); } /** * Handle an expired payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_expired( $order, $posted ) { $this->payment_status_failed( $order, $posted ); } /** * Handle a voided payment. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_voided( $order, $posted ) { $this->payment_status_failed( $order, $posted ); } /** * When a user cancelled order is marked paid. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_paid_cancelled_order( $order, $posted ) { $this->send_ipn_email_notification( /* translators: %s: order link. */ sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ), /* translators: %s: order ID. */ sprintf( __( 'Order #%s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'woocommerce' ), $order->get_order_number() ) ); } /** * Handle a refunded order. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_refunded( $order, $posted ) { // Only handle full refunds, not partial. if ( $order->get_total() === wc_format_decimal( $posted['mc_gross'] * -1, wc_get_price_decimals() ) ) { /* translators: %s: payment status. */ $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); $this->send_ipn_email_notification( /* translators: %s: order link. */ sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ), /* translators: %1$s: order ID, %2$s: reason code. */ sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] ) ); } } /** * Handle a reversal. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_reversed( $order, $posted ) { /* translators: %s: payment status. */ $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); $this->send_ipn_email_notification( /* translators: %s: order link. */ sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ), /* translators: %1$s: order ID, %2$s: reason code. */ sprintf( __( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), wc_clean( $posted['reason_code'] ) ) ); } /** * Handle a cancelled reversal. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function payment_status_canceled_reversal( $order, $posted ) { $this->send_ipn_email_notification( /* translators: %s: order link. */ sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ), /* translators: %1$s: order ID, %2$s: order link. */ sprintf( __( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'woocommerce' ), $order->get_order_number(), esc_url( $order->get_edit_order_url() ) ) ); } /** * Save important data from the IPN to the order. * * @param WC_Order $order Order object. * @param array $posted Posted data. */ protected function save_paypal_meta_data( $order, $posted ) { if ( ! empty( $posted['payment_type'] ) ) { update_post_meta( $order->get_id(), 'Payment type', wc_clean( $posted['payment_type'] ) ); } if ( ! empty( $posted['txn_id'] ) ) { update_post_meta( $order->get_id(), '_transaction_id', wc_clean( $posted['txn_id'] ) ); } if ( ! empty( $posted['payment_status'] ) ) { update_post_meta( $order->get_id(), '_paypal_status', wc_clean( $posted['payment_status'] ) ); } } /** * Send a notification to the user handling orders. * * @param string $subject Email subject. * @param string $message Email message. */ protected function send_ipn_email_notification( $subject, $message ) { $new_order_settings = get_option( 'woocommerce_new_order_settings', array() ); $mailer = WC()->mailer(); $message = $mailer->wrap_message( $subject, $message ); $woocommerce_paypal_settings = get_option( 'woocommerce_paypal_settings' ); if ( ! empty( $woocommerce_paypal_settings['ipn_notification'] ) && 'no' === $woocommerce_paypal_settings['ipn_notification'] ) { return; } $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), strip_tags( $subject ), $message ); } } includes/gateways/paypal/assets/images/paypal.png 0000644 00000004626 15132754524 0016247 0 ustar 00 �PNG IHDR 3 ��@ ]IDATX �Yil\W��6o�c�v�f�,ơ�i�� �j�*�� �(BTU�� Z~�� EB?��J"�ҒR��P� (UH�XF���i��n�Lf<�̛���;w:�8Nl��\k�v��~��c�رc*�"C�irWJ��.��X#Dȁ�0a����*��rvh��#Te�m����\��Y����]�Ee�*<_�8~#�F���L���͘�9������'�O��$�R�P�a��&� �cR�5�(a��Y�W;e��^�L���Uа�G�D|(�%�FIs-�͟�>���dcjĒ 50R�@�Gi�_Y32��dy� �$L� # ����_�o_8#j�Jz*E+��,��x���q϶uԧ0-�A��1�K]��93�$^:��HK@ֆ�y�i��ο;N�ۓ&�q���̙��1���}{v}B��* �`pEc%��%�����J��XEw���;�B,0;�������6y#��gJ>�pM��F�߿�l&Km�w�r`�P32w��ij��h��k���M���4�bACq�Y�[d��`\��1���|N����l� �[���x�f�z�}0K�4|!WB`�'�2� Y�|������3=�1Ɔ�9�CS���qΜeVIê0#���<~�\D���MI\�r'S�b�ȑ�4���n���i���~B������53�q� P%{��K\��#|�[�DC�[������Z`o��E���w&��DB�e�U�jf� ��D��+%_J��Ѓ��Y<A���4Q�؉ Չ�ʣ�-�.��RH`@]I� $��E��H���o��)��H!�L�A�m���6X{�9t&�7%��6[���d\33b<���S���~z��D$�J��A���]4;B����om��I`��{E쾹 ��EkÇO3M��Z=i��X����ą�� ���0�Pm���fd���X����ٟ��u1<sk��N�r�R�Z�٢jg�J1�H;��t�| ?�r�^4$�@���YD������p�G:�`WQ�G&��8>Y3WF��fU�'�܌g�(�߉��#11�yFjLP̎�l7'\��XJKܕL�2e"�^����Z�̘�Җ�008B"H }BsPO��&7�aqIq�����% Q�cC���M����E4�m d_�$FU�^�ļך�1����É�x��[�+���S��[�4@>��kC㼍ˇ�yk|���@rB�BS���)M�F8D������y���F�W� ��6��1�aa�DG� �,Y�%L_�L=k����/b�Z�ؙ�3� �pxHk� ��I��M.@1t0���%�XR'`��Y�t6-�ߴ\��u�W���,��ϧ������_����a�K�`3��8,l}���s�h��}p$�ԁK�����8~}���Ū D�R�������?��@� j مTr$��,ުЌ�� j���Gї��/��}� ^=6���q����Z�C���rǭI�\���w�1�4�-�p�f�{x�]���6�{,�� q¸�K�$�F�1 ���1�(�@V��V��Y`�ri����F\|q�~�6�����w��O߄}/�����3�>�� �3�ȑ��Lun@|��(nJ���D]$�;:L�$ԹLJ��"�T.Ϡ�s��e��$$<#;����,6�u��^��=�ȣ�k-^���ؾ9���(�3��r��~�3C�@����8t_�8|��}k�8�%\�Б� iG4��<}JR&Q��m37�L���؈�Lg����?���xpG�w��Ĺa���8����h��-=m������xI���<����űs]=IC̴��F/���Ÿ4:e�5��L�3¾�Yc���s�Fq���l��H] `R;�� :�����)���w��.4�1 a�,�-���y�)>�jb���O�Ǔ_؎oe2<a}�\E;�+����"^% ���G*V������86ƈ�em,��uppf�� � r�*���xJ��琿"煥/W���R��+*��8�#�����F�1a�w�����/߮.�ԓ�-��ɓj�n�w"Ս˳R AIERb�ЧKI��e�HH���ȝ����^וu9�Z��6N)��x��rdV��Q�4- \KYU�ŕ��+ϖ�{���#ץn�ڃ)>+�r���N&g$;��W�½A(w G_"�Lg��}E=�E?r/�-+;�DN����](�����;�w�}�q9��61:c:����"�Ҭ>P�Q�,��1���ʍ�@���<m2@�:yge@�e��p�^���x�r���F���?H�^��W0 IEND�B`� includes/gateways/paypal/assets/js/paypal-admin.min.js 0000644 00000002133 15132754524 0017105 0 ustar 00 jQuery(function($){'use strict';var wc_paypal_admin={isTestMode:function(){return $('#woocommerce_paypal_testmode').is(':checked')},init:function(){$(document.body).on('change','#woocommerce_paypal_testmode',function(){var test_api_username=$('#woocommerce_paypal_sandbox_api_username').parents('tr').eq(0),test_api_password=$('#woocommerce_paypal_sandbox_api_password').parents('tr').eq(0),test_api_signature=$('#woocommerce_paypal_sandbox_api_signature').parents('tr').eq(0),live_api_username=$('#woocommerce_paypal_api_username').parents('tr').eq(0),live_api_password=$('#woocommerce_paypal_api_password').parents('tr').eq(0),live_api_signature=$('#woocommerce_paypal_api_signature').parents('tr').eq(0);if($(this).is(':checked')){test_api_username.show();test_api_password.show();test_api_signature.show();live_api_username.hide();live_api_password.hide();live_api_signature.hide()}else{test_api_username.hide();test_api_password.hide();test_api_signature.hide();live_api_username.show();live_api_password.show();live_api_signature.show()}});$('#woocommerce_paypal_testmode').change()}};wc_paypal_admin.init()}) includes/gateways/paypal/assets/js/paypal-admin.js 0000644 00000002666 15132754524 0016336 0 ustar 00 jQuery( function( $ ) { 'use strict'; /** * Object to handle PayPal admin functions. */ var wc_paypal_admin = { isTestMode: function() { return $( '#woocommerce_paypal_testmode' ).is( ':checked' ); }, /** * Initialize. */ init: function() { $( document.body ).on( 'change', '#woocommerce_paypal_testmode', function() { var test_api_username = $( '#woocommerce_paypal_sandbox_api_username' ).parents( 'tr' ).eq( 0 ), test_api_password = $( '#woocommerce_paypal_sandbox_api_password' ).parents( 'tr' ).eq( 0 ), test_api_signature = $( '#woocommerce_paypal_sandbox_api_signature' ).parents( 'tr' ).eq( 0 ), live_api_username = $( '#woocommerce_paypal_api_username' ).parents( 'tr' ).eq( 0 ), live_api_password = $( '#woocommerce_paypal_api_password' ).parents( 'tr' ).eq( 0 ), live_api_signature = $( '#woocommerce_paypal_api_signature' ).parents( 'tr' ).eq( 0 ); if ( $( this ).is( ':checked' ) ) { test_api_username.show(); test_api_password.show(); test_api_signature.show(); live_api_username.hide(); live_api_password.hide(); live_api_signature.hide(); } else { test_api_username.hide(); test_api_password.hide(); test_api_signature.hide(); live_api_username.show(); live_api_password.show(); live_api_signature.show(); } } ); $( '#woocommerce_paypal_testmode' ).trigger( 'change' ); } }; wc_paypal_admin.init(); }); includes/gateways/paypal/class-wc-gateway-paypal.php 0000644 00000042721 15132754524 0016652 0 ustar 00 <?php /** * PayPal Standard Payment Gateway. * * Provides a PayPal Standard Payment Gateway. * * @class WC_Gateway_Paypal * @extends WC_Payment_Gateway * @version 2.3.0 * @package WooCommerce\Classes\Payment */ use Automattic\Jetpack\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Gateway_Paypal Class. */ class WC_Gateway_Paypal extends WC_Payment_Gateway { /** * Whether or not logging is enabled * * @var bool */ public static $log_enabled = false; /** * Logger instance * * @var WC_Logger */ public static $log = false; /** * Constructor for the gateway. */ public function __construct() { $this->id = 'paypal'; $this->has_fields = false; $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); $this->method_title = __( 'PayPal Standard', 'woocommerce' ); /* translators: %s: Link to WC system status page */ $this->method_description = __( 'PayPal Standard redirects customers to PayPal to enter their payment information.', 'woocommerce' ); $this->supports = array( 'products', 'refunds', ); // Load the settings. $this->init_form_fields(); $this->init_settings(); // Define user set variables. $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); $this->testmode = 'yes' === $this->get_option( 'testmode', 'no' ); $this->debug = 'yes' === $this->get_option( 'debug', 'no' ); $this->email = $this->get_option( 'email' ); $this->receiver_email = $this->get_option( 'receiver_email', $this->email ); $this->identity_token = $this->get_option( 'identity_token' ); self::$log_enabled = $this->debug; if ( $this->testmode ) { /* translators: %s: Link to PayPal sandbox testing guide page */ $this->description .= ' ' . sprintf( __( 'SANDBOX ENABLED. You can use sandbox testing accounts only. See the <a href="%s">PayPal Sandbox Testing Guide</a> for more details.', 'woocommerce' ), 'https://developer.paypal.com/docs/classic/lifecycle/ug_sandbox/' ); $this->description = trim( $this->description ); } add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_order_status_processing', array( $this, 'capture_payment' ) ); add_action( 'woocommerce_order_status_completed', array( $this, 'capture_payment' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); if ( ! $this->is_valid_for_use() ) { $this->enabled = 'no'; } else { include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-ipn-handler.php'; new WC_Gateway_Paypal_IPN_Handler( $this->testmode, $this->receiver_email ); if ( $this->identity_token ) { include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-pdt-handler.php'; $pdt_handler = new WC_Gateway_Paypal_PDT_Handler( $this->testmode, $this->identity_token ); $pdt_handler->set_receiver_email( $this->receiver_email ); } } if ( 'yes' === $this->enabled ) { add_filter( 'woocommerce_thankyou_order_received_text', array( $this, 'order_received_text' ), 10, 2 ); } } /** * Return whether or not this gateway still requires setup to function. * * When this gateway is toggled on via AJAX, if this returns true a * redirect will occur to the settings page instead. * * @since 3.4.0 * @return bool */ public function needs_setup() { return ! is_email( $this->email ); } /** * Logging method. * * @param string $message Log message. * @param string $level Optional. Default 'info'. Possible values: * emergency|alert|critical|error|warning|notice|info|debug. */ public static function log( $message, $level = 'info' ) { if ( self::$log_enabled ) { if ( empty( self::$log ) ) { self::$log = wc_get_logger(); } self::$log->log( $level, $message, array( 'source' => 'paypal' ) ); } } /** * Processes and saves options. * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out. * * @return bool was anything saved? */ public function process_admin_options() { $saved = parent::process_admin_options(); // Maybe clear logs. if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) { if ( empty( self::$log ) ) { self::$log = wc_get_logger(); } self::$log->clear( 'paypal' ); } return $saved; } /** * Get gateway icon. * * @return string */ public function get_icon() { // We need a base country for the link to work, bail if in the unlikely event no country is set. $base_country = WC()->countries->get_base_country(); if ( empty( $base_country ) ) { return ''; } $icon_html = ''; $icon = (array) $this->get_icon_image( $base_country ); foreach ( $icon as $i ) { $icon_html .= '<img src="' . esc_attr( $i ) . '" alt="' . esc_attr__( 'PayPal acceptance mark', 'woocommerce' ) . '" />'; } $icon_html .= sprintf( '<a href="%1$s" class="about_paypal" onclick="javascript:window.open(\'%1$s\',\'WIPaypal\',\'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=1060, height=700\'); return false;">' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '</a>', esc_url( $this->get_icon_url( $base_country ) ) ); return apply_filters( 'woocommerce_gateway_icon', $icon_html, $this->id ); } /** * Get the link for an icon based on country. * * @param string $country Country two letter code. * @return string */ protected function get_icon_url( $country ) { $url = 'https://www.paypal.com/' . strtolower( $country ); $home_counties = array( 'BE', 'CZ', 'DK', 'HU', 'IT', 'JP', 'NL', 'NO', 'ES', 'SE', 'TR', 'IN' ); $countries = array( 'DZ', 'AU', 'BH', 'BQ', 'BW', 'CA', 'CN', 'CW', 'FI', 'FR', 'DE', 'GR', 'HK', 'ID', 'JO', 'KE', 'KW', 'LU', 'MY', 'MA', 'OM', 'PH', 'PL', 'PT', 'QA', 'IE', 'RU', 'BL', 'SX', 'MF', 'SA', 'SG', 'SK', 'KR', 'SS', 'TW', 'TH', 'AE', 'GB', 'US', 'VN' ); if ( in_array( $country, $home_counties, true ) ) { return $url . '/webapps/mpp/home'; } elseif ( in_array( $country, $countries, true ) ) { return $url . '/webapps/mpp/paypal-popup'; } else { return $url . '/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside'; } } /** * Get PayPal images for a country. * * @param string $country Country code. * @return array of image URLs */ protected function get_icon_image( $country ) { switch ( $country ) { case 'US': case 'NZ': case 'CZ': case 'HU': case 'MY': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; break; case 'TR': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg'; break; case 'GB': $icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png'; break; case 'MX': $icon = array( 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png', 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif', ); break; case 'FR': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg'; break; case 'AU': $icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg'; break; case 'DK': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg'; break; case 'RU': $icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg'; break; case 'NO': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg'; break; case 'CA': $icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg'; break; case 'HK': $icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg'; break; case 'SG': $icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg'; break; case 'TW': $icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg'; break; case 'TH': $icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg'; break; case 'JP': $icon = 'https://www.paypal.com/ja_JP/JP/i/bnr/horizontal_solution_4_jcb.gif'; break; case 'IN': $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; break; default: $icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' ); break; } return apply_filters( 'woocommerce_paypal_icon', $icon ); } /** * Check if this gateway is available in the user's country based on currency. * * @return bool */ public function is_valid_for_use() { return in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR' ) ), true ); } /** * Admin Panel Options. * - Options for bits like 'title' and availability on a country-by-country basis. * * @since 1.0.0 */ public function admin_options() { if ( $this->is_valid_for_use() ) { parent::admin_options(); } else { ?> <div class="inline error"> <p> <strong><?php esc_html_e( 'Gateway disabled', 'woocommerce' ); ?></strong>: <?php esc_html_e( 'PayPal Standard does not support your store currency.', 'woocommerce' ); ?> </p> </div> <?php } } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = include __DIR__ . '/includes/settings-paypal.php'; } /** * Get the transaction URL. * * @param WC_Order $order Order object. * @return string */ public function get_transaction_url( $order ) { if ( $this->testmode ) { $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; } else { $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; } return parent::get_transaction_url( $order ); } /** * Process the payment and return the result. * * @param int $order_id Order ID. * @return array */ public function process_payment( $order_id ) { include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php'; $order = wc_get_order( $order_id ); $paypal_request = new WC_Gateway_Paypal_Request( $this ); return array( 'result' => 'success', 'redirect' => $paypal_request->get_request_url( $order, $this->testmode ), ); } /** * Can the order be refunded via PayPal? * * @param WC_Order $order Order object. * @return bool */ public function can_refund_order( $order ) { $has_api_creds = false; if ( $this->testmode ) { $has_api_creds = $this->get_option( 'sandbox_api_username' ) && $this->get_option( 'sandbox_api_password' ) && $this->get_option( 'sandbox_api_signature' ); } else { $has_api_creds = $this->get_option( 'api_username' ) && $this->get_option( 'api_password' ) && $this->get_option( 'api_signature' ); } return $order && $order->get_transaction_id() && $has_api_creds; } /** * Init the API class and set the username/password etc. */ protected function init_api() { include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php'; WC_Gateway_Paypal_API_Handler::$api_username = $this->testmode ? $this->get_option( 'sandbox_api_username' ) : $this->get_option( 'api_username' ); WC_Gateway_Paypal_API_Handler::$api_password = $this->testmode ? $this->get_option( 'sandbox_api_password' ) : $this->get_option( 'api_password' ); WC_Gateway_Paypal_API_Handler::$api_signature = $this->testmode ? $this->get_option( 'sandbox_api_signature' ) : $this->get_option( 'api_signature' ); WC_Gateway_Paypal_API_Handler::$sandbox = $this->testmode; } /** * Process a refund if supported. * * @param int $order_id Order ID. * @param float $amount Refund amount. * @param string $reason Refund reason. * @return bool|WP_Error */ public function process_refund( $order_id, $amount = null, $reason = '' ) { $order = wc_get_order( $order_id ); if ( ! $this->can_refund_order( $order ) ) { return new WP_Error( 'error', __( 'Refund failed.', 'woocommerce' ) ); } $this->init_api(); $result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason ); if ( is_wp_error( $result ) ) { $this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' ); return new WP_Error( 'error', $result->get_error_message() ); } $this->log( 'Refund Result: ' . wc_print_r( $result, true ) ); switch ( strtolower( $result->ACK ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase case 'success': case 'successwithwarning': $order->add_order_note( /* translators: 1: Refund amount, 2: Refund ID */ sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ); return true; } return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } /** * Capture payment when the order is changed from on-hold to complete or processing * * @param int $order_id Order ID. */ public function capture_payment( $order_id ) { $order = wc_get_order( $order_id ); if ( 'paypal' === $order->get_payment_method() && 'pending' === $order->get_meta( '_paypal_status', true ) && $order->get_transaction_id() ) { $this->init_api(); $result = WC_Gateway_Paypal_API_Handler::do_capture( $order ); if ( is_wp_error( $result ) ) { $this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' ); /* translators: %s: Paypal gateway error message */ $order->add_order_note( sprintf( __( 'Payment could not be captured: %s', 'woocommerce' ), $result->get_error_message() ) ); return; } $this->log( 'Capture Result: ' . wc_print_r( $result, true ) ); // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( ! empty( $result->PAYMENTSTATUS ) ) { switch ( $result->PAYMENTSTATUS ) { case 'Completed': /* translators: 1: Amount, 2: Authorization ID, 3: Transaction ID */ $order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) ); update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS ); update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID ); break; default: /* translators: 1: Authorization ID, 2: Payment status */ $order->add_order_note( sprintf( __( 'Payment could not be captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) ); break; } } // phpcs:enable } } /** * Load admin scripts. * * @since 3.3.0 */ public function admin_scripts() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; if ( 'woocommerce_page_wc-settings' !== $screen_id ) { return; } $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); wp_enqueue_script( 'woocommerce_paypal_admin', WC()->plugin_url() . '/includes/gateways/paypal/assets/js/paypal-admin' . $suffix . '.js', array(), $version, true ); } /** * Custom PayPal order received text. * * @since 3.9.0 * @param string $text Default text. * @param WC_Order $order Order data. * @return string */ public function order_received_text( $text, $order ) { if ( $order && $this->id === $order->get_payment_method() ) { return esc_html__( 'Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.', 'woocommerce' ); } return $text; } /** * Determines whether PayPal Standard should be loaded or not. * * By default PayPal Standard isn't loaded on new installs or on existing sites which haven't set up the gateway. * * @since 5.5.0 * * @return bool Whether PayPal Standard should be loaded. */ public function should_load() { $option_key = '_should_load'; $should_load = $this->get_option( $option_key ); if ( '' === $should_load ) { // New installs without PayPal Standard enabled don't load it. if ( 'no' === $this->enabled && WC_Install::is_new_install() ) { $should_load = false; } else { $should_load = true; } $this->update_option( $option_key, wc_bool_to_string( $should_load ) ); } else { $should_load = wc_string_to_bool( $should_load ); } /** * Allow third-parties to filter whether PayPal Standard should be loaded or not. * * @since 5.5.0 * * @param bool $should_load Whether PayPal Standard should be loaded. * @param WC_Gateway_Paypal $this The WC_Gateway_Paypal instance. */ return apply_filters( 'woocommerce_should_load_paypal_standard', $should_load, $this ); } } packages/woocommerce-blocks/vendor/composer/autoload_real.php 0000644 00000003467 15132754524 0020606 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInitb78ca910fe07339d6189615f1734a3e3 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register(array('ComposerAutoloaderInitb78ca910fe07339d6189615f1734a3e3', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInitb78ca910fe07339d6189615f1734a3e3', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitb78ca910fe07339d6189615f1734a3e3::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); return $loader; } } packages/woocommerce-blocks/vendor/composer/installed.json 0000644 00000016073 15132754524 0020131 0 ustar 00 { "packages": [ { "name": "automattic/jetpack-autoloader", "version": "v2.10.4", "version_normalized": "2.10.4.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-autoloader.git", "reference": "70cb300a7a215ae87c671f600f77093518f87bac" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/70cb300a7a215ae87c671f600f77093518f87bac", "reference": "70cb300a7a215ae87c671f600f77093518f87bac", "shasum": "" }, "require": { "composer-plugin-api": "^1.1 || ^2.0" }, "require-dev": { "automattic/jetpack-changelogger": "^1.2", "yoast/phpunit-polyfills": "0.2.0" }, "time": "2021-08-10T06:44:08+00:00", "type": "composer-plugin", "extra": { "autotagger": true, "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", "mirror-repo": "Automattic/jetpack-autoloader", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" }, "branch-alias": { "dev-master": "2.10.x-dev" } }, "installation-source": "dist", "autoload": { "classmap": [ "src/AutoloadGenerator.php" ], "psr-4": { "Automattic\\Jetpack\\Autoloader\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], "description": "Creates a custom autoloader for a plugin or theme.", "support": { "source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.10.4" }, "install-path": "../automattic/jetpack-autoloader" }, { "name": "composer/installers", "version": "v1.11.0", "version_normalized": "1.11.0.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", "reference": "ae03311f45dfe194412081526be2e003960df74b" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/composer/installers/zipball/ae03311f45dfe194412081526be2e003960df74b", "reference": "ae03311f45dfe194412081526be2e003960df74b", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0" }, "replace": { "roundcube/plugin-installer": "*", "shama/baton": "*" }, "require-dev": { "composer/composer": "1.6.* || ^2.0", "composer/semver": "^1 || ^3", "phpstan/phpstan": "^0.12.55", "phpstan/phpstan-phpunit": "^0.12.16", "symfony/phpunit-bridge": "^4.2 || ^5", "symfony/process": "^2.3" }, "time": "2021-04-28T06:42:17+00:00", "type": "composer-plugin", "extra": { "class": "Composer\\Installers\\Plugin", "branch-alias": { "dev-main": "1.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Composer\\Installers\\": "src/Composer/Installers" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "Kyle Robinson Young", "email": "kyle@dontkry.com", "homepage": "https://github.com/shama" } ], "description": "A multi-framework Composer library installer", "homepage": "https://composer.github.io/installers/", "keywords": [ "Craft", "Dolibarr", "Eliasis", "Hurad", "ImageCMS", "Kanboard", "Lan Management System", "MODX Evo", "MantisBT", "Mautic", "Maya", "OXID", "Plentymarkets", "Porto", "RadPHP", "SMF", "Starbug", "Thelia", "Whmcs", "WolfCMS", "agl", "aimeos", "annotatecms", "attogram", "bitrix", "cakephp", "chef", "cockpit", "codeigniter", "concrete5", "croogo", "dokuwiki", "drupal", "eZ Platform", "elgg", "expressionengine", "fuelphp", "grav", "installer", "itop", "joomla", "known", "kohana", "laravel", "lavalite", "lithium", "magento", "majima", "mako", "mediawiki", "miaoxing", "modulework", "modx", "moodle", "osclass", "phpbb", "piwik", "ppi", "processwire", "puppet", "pxcms", "reindex", "roundcube", "shopware", "silverstripe", "sydes", "sylius", "symfony", "tastyigniter", "typo3", "wordpress", "yawik", "zend", "zikula" ], "support": { "issues": "https://github.com/composer/installers/issues", "source": "https://github.com/composer/installers/tree/v1.11.0" }, "funding": [ { "url": "https://packagist.com", "type": "custom" }, { "url": "https://github.com/composer", "type": "github" }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], "install-path": "./installers" } ], "dev": false, "dev-package-names": [] } packages/woocommerce-blocks/vendor/composer/InstalledVersions.php 0000644 00000033147 15132754524 0021441 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require it's presence, you can require `composer-runtime-api ^2.0` */ class InstalledVersions { private static $installed; private static $canGetVendors; private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list<string> */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list<string> */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); } /** * @return array[] * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); if (self::$canGetVendors) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } $installed[] = self::$installed; return $installed; } } packages/woocommerce-blocks/vendor/composer/autoload_namespaces.php 0000644 00000000225 15132754524 0021767 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( ); packages/woocommerce-blocks/vendor/composer/autoload_classmap.php 0000644 00000000555 15132754524 0021461 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); packages/woocommerce-blocks/vendor/composer/installers/phpstan.neon.dist 0000644 00000000325 15132754524 0022730 0 ustar 00 parameters: level: 5 paths: - src - tests excludes_analyse: - tests/Composer/Installers/Test/PolyfillTestCase.php includes: - vendor/phpstan/phpstan-phpunit/extension.neon packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php 0000644 00000001506 15132754524 0027400 0 ustar 00 <?php namespace Composer\Installers; class WHMCSInstaller extends BaseInstaller { protected $locations = array( 'addons' => 'modules/addons/{$vendor}_{$name}/', 'fraud' => 'modules/fraud/{$vendor}_{$name}/', 'gateways' => 'modules/gateways/{$vendor}_{$name}/', 'notifications' => 'modules/notifications/{$vendor}_{$name}/', 'registrars' => 'modules/registrars/{$vendor}_{$name}/', 'reports' => 'modules/reports/{$vendor}_{$name}/', 'security' => 'modules/security/{$vendor}_{$name}/', 'servers' => 'modules/servers/{$vendor}_{$name}/', 'social' => 'modules/social/{$vendor}_{$name}/', 'support' => 'modules/support/{$vendor}_{$name}/', 'templates' => 'templates/{$vendor}_{$name}/', 'includes' => 'includes/{$vendor}_{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MajimaInstaller.php 0000644 00000001502 15132754524 0027711 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin/theme installer for majima * @author David Neustadt */ class MajimaInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Transforms the names * @param array $vars * @return array */ public function inflectPackageVars($vars) { return $this->correctPluginName($vars); } /** * Change hyphenated names to camelcase * @param array $vars * @return array */ private function correctPluginName($vars) { $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); $vars['name'] = ucfirst($camelCasedName); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php 0000644 00000003362 15132754524 0027734 0 ustar 00 <?php namespace Composer\Installers; use Composer\DependencyResolver\Pool; use Composer\Semver\Constraint\Constraint; class CakePHPInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'Plugin/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { if ($this->matchesCakeVersion('>=', '3.0.0')) { return $vars; } $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } /** * Change the default plugin location when cakephp >= 3.0 */ public function getLocations() { if ($this->matchesCakeVersion('>=', '3.0.0')) { $this->locations['plugin'] = $this->composer->getConfig()->get('vendor-dir') . '/{$vendor}/{$name}/'; } return $this->locations; } /** * Check if CakePHP version matches against a version * * @param string $matcher * @param string $version * @return bool */ protected function matchesCakeVersion($matcher, $version) { $repositoryManager = $this->composer->getRepositoryManager(); if (! $repositoryManager) { return false; } $repos = $repositoryManager->getLocalRepository(); if (!$repos) { return false; } return $repos->findPackage('cakephp/cakephp', new Constraint($matcher, $version)) !== null; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php 0000644 00000001231 15132754524 0030106 0 ustar 00 <?php namespace Composer\Installers; class CockpitInstaller extends BaseInstaller { protected $locations = array( 'module' => 'cockpit/modules/addons/{$name}/', ); /** * Format module name. * * Strip `module-` prefix from package name. * * {@inheritDoc} */ public function inflectPackageVars($vars) { if ($vars['type'] == 'cockpit-module') { return $this->inflectModuleVars($vars); } return $vars; } public function inflectModuleVars($vars) { $vars['name'] = ucfirst(preg_replace('/cockpit-/i', '', $vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php 0000644 00000006054 15132754524 0027741 0 ustar 00 <?php namespace Composer\Installers; class MoodleInstaller extends BaseInstaller { protected $locations = array( 'mod' => 'mod/{$name}/', 'admin_report' => 'admin/report/{$name}/', 'atto' => 'lib/editor/atto/plugins/{$name}/', 'tool' => 'admin/tool/{$name}/', 'assignment' => 'mod/assignment/type/{$name}/', 'assignsubmission' => 'mod/assign/submission/{$name}/', 'assignfeedback' => 'mod/assign/feedback/{$name}/', 'auth' => 'auth/{$name}/', 'availability' => 'availability/condition/{$name}/', 'block' => 'blocks/{$name}/', 'booktool' => 'mod/book/tool/{$name}/', 'cachestore' => 'cache/stores/{$name}/', 'cachelock' => 'cache/locks/{$name}/', 'calendartype' => 'calendar/type/{$name}/', 'fileconverter' => 'files/converter/{$name}/', 'format' => 'course/format/{$name}/', 'coursereport' => 'course/report/{$name}/', 'customcertelement' => 'mod/customcert/element/{$name}/', 'datafield' => 'mod/data/field/{$name}/', 'datapreset' => 'mod/data/preset/{$name}/', 'editor' => 'lib/editor/{$name}/', 'enrol' => 'enrol/{$name}/', 'filter' => 'filter/{$name}/', 'gradeexport' => 'grade/export/{$name}/', 'gradeimport' => 'grade/import/{$name}/', 'gradereport' => 'grade/report/{$name}/', 'gradingform' => 'grade/grading/form/{$name}/', 'local' => 'local/{$name}/', 'logstore' => 'admin/tool/log/store/{$name}/', 'ltisource' => 'mod/lti/source/{$name}/', 'ltiservice' => 'mod/lti/service/{$name}/', 'message' => 'message/output/{$name}/', 'mnetservice' => 'mnet/service/{$name}/', 'plagiarism' => 'plagiarism/{$name}/', 'portfolio' => 'portfolio/{$name}/', 'qbehaviour' => 'question/behaviour/{$name}/', 'qformat' => 'question/format/{$name}/', 'qtype' => 'question/type/{$name}/', 'quizaccess' => 'mod/quiz/accessrule/{$name}/', 'quiz' => 'mod/quiz/report/{$name}/', 'report' => 'report/{$name}/', 'repository' => 'repository/{$name}/', 'scormreport' => 'mod/scorm/report/{$name}/', 'search' => 'search/engine/{$name}/', 'theme' => 'theme/{$name}/', 'tinymce' => 'lib/editor/tinymce/plugins/{$name}/', 'profilefield' => 'user/profile/field/{$name}/', 'webservice' => 'webservice/{$name}/', 'workshopallocation' => 'mod/workshop/allocation/{$name}/', 'workshopeval' => 'mod/workshop/eval/{$name}/', 'workshopform' => 'mod/workshop/form/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php 0000644 00000002225 15132754524 0027740 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class MauticInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'themes/{$name}/', 'core' => 'app/', ); private function getDirectoryName() { $extra = $this->package->getExtra(); if (!empty($extra['install-directory-name'])) { return $extra['install-directory-name']; } return $this->toCamelCase($this->package->getPrettyName()); } /** * @param string $packageName * * @return string */ private function toCamelCase($packageName) { return str_replace(' ', '', ucwords(str_replace('-', ' ', basename($packageName)))); } /** * Format package name of mautic-plugins to CamelCase */ public function inflectPackageVars($vars) { if ($vars['type'] == 'mautic-plugin' || $vars['type'] == 'mautic-theme') { $directoryName = $this->getDirectoryName(); $vars['name'] = $directoryName; } return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php 0000644 00000000767 15132754524 0027757 0 ustar 00 <?php namespace Composer\Installers; class CroogoInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'Plugin/{$name}/', 'theme' => 'View/Themed/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(str_replace(array('-', '_'), ' ', $vars['name'])); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/KnownInstaller.php 0000644 00000000411 15132754524 0027605 0 ustar 00 <?php namespace Composer\Installers; class KnownInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'IdnoPlugins/{$name}/', 'theme' => 'Themes/{$name}/', 'console' => 'ConsolePlugins/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php 0000644 00000002652 15132754524 0027425 0 ustar 00 <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class OxidInstaller extends BaseInstaller { const VENDOR_PATTERN = '/^modules\/(?P<vendor>.+)\/.+/'; protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'application/views/{$name}/', 'out' => 'out/{$name}/', ); /** * getInstallPath * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { $installPath = parent::getInstallPath($package, $frameworkType); $type = $this->package->getType(); if ($type === 'oxid-module') { $this->prepareVendorDirectory($installPath); } return $installPath; } /** * prepareVendorDirectory * * Makes sure there is a vendormetadata.php file inside * the vendor folder if there is a vendor folder. * * @param string $installPath * @return void */ protected function prepareVendorDirectory($installPath) { $matches = ''; $hasVendorDirectory = preg_match(self::VENDOR_PATTERN, $installPath, $matches); if (!$hasVendorDirectory) { return; } $vendorDirectory = $matches['vendor']; $vendorPath = getcwd() . '/modules/' . $vendorDirectory; if (!file_exists($vendorPath)) { mkdir($vendorPath, 0755, true); } $vendorMetaDataPath = $vendorPath . '/vendormetadata.php'; touch($vendorMetaDataPath); } } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php 0000644 00000000524 15132754524 0030367 0 ustar 00 packages <?php namespace Composer\Installers; class WordPressInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'wp-content/plugins/{$name}/', 'theme' => 'wp-content/themes/{$name}/', 'muplugin' => 'wp-content/mu-plugins/{$name}/', 'dropin' => 'wp-content/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php 0000644 00000001334 15132754524 0031724 0 ustar 00 packages <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class ExpressionEngineInstaller extends BaseInstaller { protected $locations = array(); private $ee2Locations = array( 'addon' => 'system/expressionengine/third_party/{$name}/', 'theme' => 'themes/third_party/{$name}/', ); private $ee3Locations = array( 'addon' => 'system/user/addons/{$name}/', 'theme' => 'themes/user/{$name}/', ); public function getInstallPath(PackageInterface $package, $frameworkType = '') { $version = "{$frameworkType}Locations"; $this->locations = $this->$version; return parent::getInstallPath($package, $frameworkType); } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php 0000644 00000001276 15132754524 0027566 0 ustar 00 <?php namespace Composer\Installers; class HuradInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } } vendor/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php 0000644 00000001326 15132754524 0032354 0 ustar 00 packages/woocommerce-blocks <?php namespace Composer\Installers; class LanManagementSystemInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'template' => 'templates/{$name}/', 'document-template' => 'documents/templates/{$name}/', 'userpanel-module' => 'userpanel/modules/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/Plugin.php 0000644 00000001214 15132754524 0026073 0 ustar 00 <?php namespace Composer\Installers; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; class Plugin implements PluginInterface { private $installer; public function activate(Composer $composer, IOInterface $io) { $this->installer = new Installer($io, $composer); $composer->getInstallationManager()->addInstaller($this->installer); } public function deactivate(Composer $composer, IOInterface $io) { $composer->getInstallationManager()->removeInstaller($this->installer); } public function uninstall(Composer $composer, IOInterface $io) { } } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php 0000644 00000000256 15132754524 0030271 0 ustar 00 packages <?php namespace Composer\Installers; class MODULEWorkInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/UserFrostingInstaller.php 0000644 00000000265 15132754524 0031073 0 ustar 00 packages <?php namespace Composer\Installers; class UserFrostingInstaller extends BaseInstaller { protected $locations = array( 'sprinkle' => 'app/sprinkles/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php 0000644 00000000376 15132754524 0027423 0 ustar 00 <?php namespace Composer\Installers; class ZendInstaller extends BaseInstaller { protected $locations = array( 'library' => 'library/{$name}/', 'extra' => 'extras/library/{$name}/', 'module' => 'module/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php 0000644 00000000325 15132754524 0030103 0 ustar 00 <?php namespace Composer\Installers; class VanillaInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', 'theme' => 'themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php 0000644 00000000405 15132754524 0027574 0 ustar 00 <?php namespace Composer\Installers; class KirbyInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'site/plugins/{$name}/', 'field' => 'site/fields/{$name}/', 'tag' => 'site/tags/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php 0000644 00000001271 15132754524 0027601 0 ustar 00 <?php namespace Composer\Installers; /** * Class PiwikInstaller * * @package Composer\Installers */ class PiwikInstaller extends BaseInstaller { /** * @var array */ protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php 0000644 00000000272 15132754524 0030045 0 ustar 00 <?php namespace Composer\Installers; class DecibelInstaller extends BaseInstaller { /** @var array */ protected $locations = array( 'app' => 'app/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php 0000644 00000001040 15132754524 0030106 0 ustar 00 <?php namespace Composer\Installers; class PimcoreInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php 0000644 00000000255 15132754524 0027771 0 ustar 00 <?php namespace Composer\Installers; class WolfCMSInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'wolf/plugins/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php0000644 00000001324 15132754524 0030260 0 ustar 00 <?php namespace Composer\Installers; class OntoWikiInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$name}/', 'theme' => 'extensions/themes/{$name}/', 'translation' => 'extensions/translations/{$name}/', ); /** * Format package name to lower case and remove ".ontowiki" suffix */ public function inflectPackageVars($vars) { $vars['name'] = strtolower($vars['name']); $vars['name'] = preg_replace('/.ontowiki$/', '', $vars['name']); $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = preg_replace('/-translation$/', '', $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php 0000644 00000000253 15132754524 0027404 0 ustar 00 <?php namespace Composer\Installers; class MakoInstaller extends BaseInstaller { protected $locations = array( 'package' => 'app/packages/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php 0000644 00000000465 15132754524 0030637 0 ustar 00 packages <?php namespace Composer\Installers; class CodeIgniterInstaller extends BaseInstaller { protected $locations = array( 'library' => 'application/libraries/{$name}/', 'third-party' => 'application/third_party/{$name}/', 'module' => 'application/modules/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php 0000644 00000001446 15132754524 0027561 0 ustar 00 <?php namespace Composer\Installers; /** * Installer for Craft Plugins */ class CraftInstaller extends BaseInstaller { const NAME_PREFIX = 'craft'; const NAME_SUFFIX = 'plugin'; protected $locations = array( 'plugin' => 'craft/plugins/{$name}/', ); /** * Strip `craft-` prefix and/or `-plugin` suffix from package names * * @param array $vars * * @return array */ final public function inflectPackageVars($vars) { return $this->inflectPluginVars($vars); } private function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-' . self::NAME_SUFFIX . '$/i', '', $vars['name']); $vars['name'] = preg_replace('/^' . self::NAME_PREFIX . '-/i', '', $vars['name']); return $vars; } } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php 0000644 00000002300 15132754524 0030137 0 ustar 00 packages <?php namespace Composer\Installers; /** * An installer to handle TYPO3 Flow specifics when installing packages. */ class TYPO3FlowInstaller extends BaseInstaller { protected $locations = array( 'package' => 'Packages/Application/{$name}/', 'framework' => 'Packages/Framework/{$name}/', 'plugin' => 'Packages/Plugins/{$name}/', 'site' => 'Packages/Sites/{$name}/', 'boilerplate' => 'Packages/Boilerplates/{$name}/', 'build' => 'Build/{$name}/', ); /** * Modify the package name to be a TYPO3 Flow style key. * * @param array $vars * @return array */ public function inflectPackageVars($vars) { $autoload = $this->package->getAutoload(); if (isset($autoload['psr-0']) && is_array($autoload['psr-0'])) { $namespace = key($autoload['psr-0']); $vars['name'] = str_replace('\\', '.', $namespace); } if (isset($autoload['psr-4']) && is_array($autoload['psr-4'])) { $namespace = key($autoload['psr-4']); $vars['name'] = rtrim(str_replace('\\', '.', $namespace), '.'); } return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php 0000644 00000000260 15132754524 0027616 0 ustar 00 <?php namespace Composer\Installers; class PortoInstaller extends BaseInstaller { protected $locations = array( 'container' => 'app/Containers/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/WinterInstaller.php 0000644 00000002676 15132754524 0030000 0 ustar 00 <?php namespace Composer\Installers; class WinterInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/' ); /** * Format package name. * * For package type winter-plugin, cut off a trailing '-plugin' if present. * * For package type winter-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'winter-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'winter-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'winter-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectModuleVars($vars) { $vars['name'] = preg_replace('/^wn-|-module$/', '', $vars['name']); return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/^wn-|-plugin$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/^wn-|-theme$/', '', $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php 0000644 00000000640 15132754524 0027736 0 ustar 00 <?php namespace Composer\Installers; class JoomlaInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', 'module' => 'modules/{$name}/', 'template' => 'templates/{$name}/', 'plugin' => 'plugins/{$name}/', 'library' => 'libraries/{$name}/', ); // TODO: Add inflector for mod_ and com_ names } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php 0000644 00000000333 15132754524 0027745 0 ustar 00 <?php namespace Composer\Installers; class KodiCMSInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'cms/plugins/{$name}/', 'media' => 'cms/media/vendor/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php 0000644 00000010251 15132754524 0027755 0 ustar 00 <?php namespace Composer\Installers; use Composer\Util\Filesystem; /** * Installer for Bitrix Framework. Supported types of extensions: * - `bitrix-d7-module` — copy the module to directory `bitrix/modules/<vendor>.<name>`. * - `bitrix-d7-component` — copy the component to directory `bitrix/components/<vendor>/<name>`. * - `bitrix-d7-template` — copy the template to directory `bitrix/templates/<vendor>_<name>`. * * You can set custom path to directory with Bitrix kernel in `composer.json`: * * ```json * { * "extra": { * "bitrix-dir": "s1/bitrix" * } * } * ``` * * @author Nik Samokhvalov <nik@samokhvalov.info> * @author Denis Kulichkin <onexhovia@gmail.com> */ class BitrixInstaller extends BaseInstaller { protected $locations = array( 'module' => '{$bitrix_dir}/modules/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'component' => '{$bitrix_dir}/components/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'theme' => '{$bitrix_dir}/templates/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) 'd7-module' => '{$bitrix_dir}/modules/{$vendor}.{$name}/', 'd7-component' => '{$bitrix_dir}/components/{$vendor}/{$name}/', 'd7-template' => '{$bitrix_dir}/templates/{$vendor}_{$name}/', ); /** * @var array Storage for informations about duplicates at all the time of installation packages. */ private static $checkedDuplicates = array(); /** * {@inheritdoc} */ public function inflectPackageVars($vars) { if ($this->composer->getPackage()) { $extra = $this->composer->getPackage()->getExtra(); if (isset($extra['bitrix-dir'])) { $vars['bitrix_dir'] = $extra['bitrix-dir']; } } if (!isset($vars['bitrix_dir'])) { $vars['bitrix_dir'] = 'bitrix'; } return parent::inflectPackageVars($vars); } /** * {@inheritdoc} */ protected function templatePath($path, array $vars = array()) { $templatePath = parent::templatePath($path, $vars); $this->checkDuplicates($templatePath, $vars); return $templatePath; } /** * Duplicates search packages. * * @param string $path * @param array $vars */ protected function checkDuplicates($path, array $vars = array()) { $packageType = substr($vars['type'], strlen('bitrix') + 1); $localDir = explode('/', $vars['bitrix_dir']); array_pop($localDir); $localDir[] = 'local'; $localDir = implode('/', $localDir); $oldPath = str_replace( array('{$bitrix_dir}', '{$name}'), array($localDir, $vars['name']), $this->locations[$packageType] ); if (in_array($oldPath, static::$checkedDuplicates)) { return; } if ($oldPath !== $path && file_exists($oldPath) && $this->io && $this->io->isInteractive()) { $this->io->writeError(' <error>Duplication of packages:</error>'); $this->io->writeError(' <info>Package ' . $oldPath . ' will be called instead package ' . $path . '</info>'); while (true) { switch ($this->io->ask(' <info>Delete ' . $oldPath . ' [y,n,?]?</info> ', '?')) { case 'y': $fs = new Filesystem(); $fs->removeDirectory($oldPath); break 2; case 'n': break 2; case '?': default: $this->io->writeError(array( ' y - delete package ' . $oldPath . ' and to continue with the installation', ' n - don\'t delete and to continue with the installation', )); $this->io->writeError(' ? - print help'); break; } } } static::$checkedDuplicates[] = $oldPath; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php 0000644 00000000244 15132754524 0027145 0 ustar 00 <?php namespace Composer\Installers; class PPIInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php 0000644 00000002264 15132754524 0027450 0 ustar 00 <?php namespace Composer\Installers; class SyDESInstaller extends BaseInstaller { protected $locations = array( 'module' => 'app/modules/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format module name. * * Strip `sydes-` prefix and a trailing '-theme' or '-module' from package name if present. * * {@inerhitDoc} */ public function inflectPackageVars($vars) { if ($vars['type'] == 'sydes-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'sydes-theme') { return $this->inflectThemeVars($vars); } return $vars; } public function inflectModuleVars($vars) { $vars['name'] = preg_replace('/(^sydes-|-module$)/i', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/(^sydes-|-theme$)/', '', $vars['name']); $vars['name'] = strtolower($vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php 0000644 00000000405 15132754524 0027447 0 ustar 00 <?php namespace Composer\Installers; class PhpBBInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'ext/{$vendor}/{$name}/', 'language' => 'language/{$name}/', 'style' => 'styles/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php 0000644 00000000312 15132754524 0027136 0 ustar 00 <?php namespace Composer\Installers; class SMFInstaller extends BaseInstaller { protected $locations = array( 'module' => 'Sources/{$name}/', 'theme' => 'Themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/Installer.php 0000644 00000024313 15132754524 0026577 0 ustar 00 <?php namespace Composer\Installers; use Composer\Composer; use Composer\Installer\BinaryInstaller; use Composer\Installer\LibraryInstaller; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use React\Promise\PromiseInterface; class Installer extends LibraryInstaller { /** * Package types to installer class map * * @var array */ private $supportedTypes = array( 'aimeos' => 'AimeosInstaller', 'asgard' => 'AsgardInstaller', 'attogram' => 'AttogramInstaller', 'agl' => 'AglInstaller', 'annotatecms' => 'AnnotateCmsInstaller', 'bitrix' => 'BitrixInstaller', 'bonefish' => 'BonefishInstaller', 'cakephp' => 'CakePHPInstaller', 'chef' => 'ChefInstaller', 'civicrm' => 'CiviCrmInstaller', 'ccframework' => 'ClanCatsFrameworkInstaller', 'cockpit' => 'CockpitInstaller', 'codeigniter' => 'CodeIgniterInstaller', 'concrete5' => 'Concrete5Installer', 'craft' => 'CraftInstaller', 'croogo' => 'CroogoInstaller', 'dframe' => 'DframeInstaller', 'dokuwiki' => 'DokuWikiInstaller', 'dolibarr' => 'DolibarrInstaller', 'decibel' => 'DecibelInstaller', 'drupal' => 'DrupalInstaller', 'elgg' => 'ElggInstaller', 'eliasis' => 'EliasisInstaller', 'ee3' => 'ExpressionEngineInstaller', 'ee2' => 'ExpressionEngineInstaller', 'ezplatform' => 'EzPlatformInstaller', 'fuel' => 'FuelInstaller', 'fuelphp' => 'FuelphpInstaller', 'grav' => 'GravInstaller', 'hurad' => 'HuradInstaller', 'tastyigniter' => 'TastyIgniterInstaller', 'imagecms' => 'ImageCMSInstaller', 'itop' => 'ItopInstaller', 'joomla' => 'JoomlaInstaller', 'kanboard' => 'KanboardInstaller', 'kirby' => 'KirbyInstaller', 'known' => 'KnownInstaller', 'kodicms' => 'KodiCMSInstaller', 'kohana' => 'KohanaInstaller', 'lms' => 'LanManagementSystemInstaller', 'laravel' => 'LaravelInstaller', 'lavalite' => 'LavaLiteInstaller', 'lithium' => 'LithiumInstaller', 'magento' => 'MagentoInstaller', 'majima' => 'MajimaInstaller', 'mantisbt' => 'MantisBTInstaller', 'mako' => 'MakoInstaller', 'maya' => 'MayaInstaller', 'mautic' => 'MauticInstaller', 'mediawiki' => 'MediaWikiInstaller', 'miaoxing' => 'MiaoxingInstaller', 'microweber' => 'MicroweberInstaller', 'modulework' => 'MODULEWorkInstaller', 'modx' => 'ModxInstaller', 'modxevo' => 'MODXEvoInstaller', 'moodle' => 'MoodleInstaller', 'october' => 'OctoberInstaller', 'ontowiki' => 'OntoWikiInstaller', 'oxid' => 'OxidInstaller', 'osclass' => 'OsclassInstaller', 'pxcms' => 'PxcmsInstaller', 'phpbb' => 'PhpBBInstaller', 'pimcore' => 'PimcoreInstaller', 'piwik' => 'PiwikInstaller', 'plentymarkets'=> 'PlentymarketsInstaller', 'ppi' => 'PPIInstaller', 'puppet' => 'PuppetInstaller', 'radphp' => 'RadPHPInstaller', 'phifty' => 'PhiftyInstaller', 'porto' => 'PortoInstaller', 'processwire' => 'ProcessWireInstaller', 'redaxo' => 'RedaxoInstaller', 'redaxo5' => 'Redaxo5Installer', 'reindex' => 'ReIndexInstaller', 'roundcube' => 'RoundcubeInstaller', 'shopware' => 'ShopwareInstaller', 'sitedirect' => 'SiteDirectInstaller', 'silverstripe' => 'SilverStripeInstaller', 'smf' => 'SMFInstaller', 'starbug' => 'StarbugInstaller', 'sydes' => 'SyDESInstaller', 'sylius' => 'SyliusInstaller', 'symfony1' => 'Symfony1Installer', 'tao' => 'TaoInstaller', 'thelia' => 'TheliaInstaller', 'tusk' => 'TuskInstaller', 'typo3-cms' => 'TYPO3CmsInstaller', 'typo3-flow' => 'TYPO3FlowInstaller', 'userfrosting' => 'UserFrostingInstaller', 'vanilla' => 'VanillaInstaller', 'whmcs' => 'WHMCSInstaller', 'winter' => 'WinterInstaller', 'wolfcms' => 'WolfCMSInstaller', 'wordpress' => 'WordPressInstaller', 'yawik' => 'YawikInstaller', 'zend' => 'ZendInstaller', 'zikula' => 'ZikulaInstaller', 'prestashop' => 'PrestashopInstaller' ); /** * Installer constructor. * * Disables installers specified in main composer extra installer-disable * list * * @param IOInterface $io * @param Composer $composer * @param string $type * @param Filesystem|null $filesystem * @param BinaryInstaller|null $binaryInstaller */ public function __construct( IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null, BinaryInstaller $binaryInstaller = null ) { parent::__construct($io, $composer, $type, $filesystem, $binaryInstaller); $this->removeDisabledInstallers(); } /** * {@inheritDoc} */ public function getInstallPath(PackageInterface $package) { $type = $package->getType(); $frameworkType = $this->findFrameworkType($type); if ($frameworkType === false) { throw new \InvalidArgumentException( 'Sorry the package type of this package is not yet supported.' ); } $class = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; $installer = new $class($package, $this->composer, $this->getIO()); return $installer->getInstallPath($package, $frameworkType); } public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { $installPath = $this->getPackageBasePath($package); $io = $this->io; $outputStatus = function () use ($io, $installPath) { $io->write(sprintf('Deleting %s - %s', $installPath, !file_exists($installPath) ? '<comment>deleted</comment>' : '<error>not deleted</error>')); }; $promise = parent::uninstall($repo, $package); // Composer v2 might return a promise here if ($promise instanceof PromiseInterface) { return $promise->then($outputStatus); } // If not, execute the code right away as parent::uninstall executed synchronously (composer v1, or v2 without async) $outputStatus(); return null; } /** * {@inheritDoc} */ public function supports($packageType) { $frameworkType = $this->findFrameworkType($packageType); if ($frameworkType === false) { return false; } $locationPattern = $this->getLocationPattern($frameworkType); return preg_match('#' . $frameworkType . '-' . $locationPattern . '#', $packageType, $matches) === 1; } /** * Finds a supported framework type if it exists and returns it * * @param string $type * @return string|false */ protected function findFrameworkType($type) { krsort($this->supportedTypes); foreach ($this->supportedTypes as $key => $val) { if ($key === substr($type, 0, strlen($key))) { return substr($type, 0, strlen($key)); } } return false; } /** * Get the second part of the regular expression to check for support of a * package type * * @param string $frameworkType * @return string */ protected function getLocationPattern($frameworkType) { $pattern = false; if (!empty($this->supportedTypes[$frameworkType])) { $frameworkClass = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; /** @var BaseInstaller $framework */ $framework = new $frameworkClass(null, $this->composer, $this->getIO()); $locations = array_keys($framework->getLocations()); $pattern = $locations ? '(' . implode('|', $locations) . ')' : false; } return $pattern ? : '(\w+)'; } /** * Get I/O object * * @return IOInterface */ private function getIO() { return $this->io; } /** * Look for installers set to be disabled in composer's extra config and * remove them from the list of supported installers. * * Globals: * - true, "all", and "*" - disable all installers. * - false - enable all installers (useful with * wikimedia/composer-merge-plugin or similar) * * @return void */ protected function removeDisabledInstallers() { $extra = $this->composer->getPackage()->getExtra(); if (!isset($extra['installer-disable']) || $extra['installer-disable'] === false) { // No installers are disabled return; } // Get installers to disable $disable = $extra['installer-disable']; // Ensure $disabled is an array if (!is_array($disable)) { $disable = array($disable); } // Check which installers should be disabled $all = array(true, "all", "*"); $intersect = array_intersect($all, $disable); if (!empty($intersect)) { // Disable all installers $this->supportedTypes = array(); } else { // Disable specified installers foreach ($disable as $key => $installer) { if (is_string($installer) && key_exists($installer, $this->supportedTypes)) { unset($this->supportedTypes[$installer]); } } } } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/SyliusInstaller.php 0000644 00000000245 15132754524 0030006 0 ustar 00 <?php namespace Composer\Installers; class SyliusInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php0000644 00000000267 15132754524 0030257 0 ustar 00 <?php namespace Composer\Installers; class BonefishInstaller extends BaseInstaller { protected $locations = array( 'package' => 'Packages/{$vendor}/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php 0000644 00000002457 15132754524 0027601 0 ustar 00 <?php namespace Composer\Installers; class VgmcpInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'src/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/' ); /** * Format package name. * * For package type vgmcp-bundle, cut off a trailing '-bundle' if present. * * For package type vgmcp-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'vgmcp-bundle') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'vgmcp-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-bundle$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TaoInstaller.php 0000644 00000001423 15132754524 0027240 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle TAO extensions. */ class TaoInstaller extends BaseInstaller { const EXTRA_TAO_EXTENSION_NAME = 'tao-extension-name'; protected $locations = array( 'extension' => '{$name}' ); public function inflectPackageVars($vars) { $extra = $this->package->getExtra(); if (array_key_exists(self::EXTRA_TAO_EXTENSION_NAME, $extra)) { $vars['name'] = $extra[self::EXTRA_TAO_EXTENSION_NAME]; return $vars; } $vars['name'] = str_replace('extension-', '', $vars['name']); $vars['name'] = str_replace('-', ' ', $vars['name']); $vars['name'] = lcfirst(str_replace(' ', '', ucwords($vars['name']))); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php 0000644 00000000257 15132754524 0030124 0 ustar 00 <?php namespace Composer\Installers; class FuelphpInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php 0000644 00000000324 15132754524 0030052 0 ustar 00 <?php namespace Composer\Installers; class ReIndexInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'themes/{$name}/', 'plugin' => 'plugins/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php 0000644 00000000341 15132754524 0027752 0 ustar 00 <?php namespace Composer\Installers; class ZikulaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}-{$name}/', 'theme' => 'themes/{$vendor}-{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php 0000644 00000001246 15132754524 0027604 0 ustar 00 <?php /** * Created by PhpStorm. * User: cbleek * Date: 25.03.16 * Time: 20:55 */ namespace Composer\Installers; class YawikInstaller extends BaseInstaller { protected $locations = array( 'module' => 'module/{$name}/', ); /** * Format package name to CamelCase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php0000644 00000000575 15132754524 0030045 0 ustar 00 <?php namespace Composer\Installers; /** * Extension installer for TYPO3 CMS * * @deprecated since 1.0.25, use https://packagist.org/packages/typo3/cms-composer-installers instead * * @author Sascha Egerer <sascha.egerer@dkd.de> */ class TYPO3CmsInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'typo3conf/ext/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php 0000644 00000000556 15132754524 0030273 0 ustar 00 packages <?php namespace Composer\Installers; class Concrete5Installer extends BaseInstaller { protected $locations = array( 'core' => 'concrete/', 'block' => 'application/blocks/{$name}/', 'package' => 'packages/{$name}/', 'theme' => 'application/themes/{$name}/', 'update' => 'updates/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php 0000644 00000001553 15132754524 0031070 0 ustar 00 packages <?php namespace Composer\Installers; class TastyIgniterInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$vendor}/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format package name. * * Cut off leading 'ti-ext-' or 'ti-theme-' if present. * Strip vendor name of characters that is not alphanumeric or an underscore * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'tastyigniter-extension') { $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); $vars['name'] = preg_replace('/^ti-ext-/', '', $vars['name']); } if ($vars['type'] === 'tastyigniter-theme') { $vars['name'] = preg_replace('/^ti-theme-/', '', $vars['name']); } return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php 0000644 00000000417 15132754524 0027412 0 ustar 00 <?php namespace Composer\Installers; class FuelInstaller extends BaseInstaller { protected $locations = array( 'module' => 'fuel/app/modules/{$name}/', 'package' => 'fuel/packages/{$name}/', 'theme' => 'fuel/app/themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/DframeInstaller.php 0000644 00000000263 15132754524 0027714 0 ustar 00 <?php namespace Composer\Installers; class DframeInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ProcessWireInstaller.php 0000644 00000001053 15132754524 0030702 0 ustar 00 packages <?php namespace Composer\Installers; class ProcessWireInstaller extends BaseInstaller { protected $locations = array( 'module' => 'site/modules/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php0000644 00000002354 15132754524 0030247 0 ustar 00 <?php namespace Composer\Installers; class DokuWikiInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'lib/plugins/{$name}/', 'template' => 'lib/tpl/{$name}/', ); /** * Format package name. * * For package type dokuwiki-plugin, cut off a trailing '-plugin', * or leading dokuwiki_ if present. * * For package type dokuwiki-template, cut off a trailing '-template' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'dokuwiki-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'dokuwiki-template') { return $this->inflectTemplateVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-plugin$/', '', $vars['name']); $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); return $vars; } protected function inflectTemplateVars($vars) { $vars['name'] = preg_replace('/-template$/', '', $vars['name']); $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php 0000644 00000002456 15132754524 0027725 0 ustar 00 <?php namespace Composer\Installers; class AsgardInstaller extends BaseInstaller { protected $locations = array( 'module' => 'Modules/{$name}/', 'theme' => 'Themes/{$name}/' ); /** * Format package name. * * For package type asgard-module, cut off a trailing '-plugin' if present. * * For package type asgard-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'asgard-module') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'asgard-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/-module$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php0000644 00000001066 15132754524 0030245 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin installer for symfony 1.x * * @author Jérôme Tamarelle <jerome@tamarelle.net> */ class Symfony1Installer extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/AglInstaller.php 0000644 00000000711 15132754524 0027217 0 ustar 00 <?php namespace Composer\Installers; class AglInstaller extends BaseInstaller { protected $locations = array( 'module' => 'More/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = preg_replace_callback('/(?:^|_|-)(.?)/', function ($matches) { return strtoupper($matches[1]); }, $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php0000644 00000000542 15132754524 0030254 0 ustar 00 <?php namespace Composer\Installers; /** * Class DolibarrInstaller * * @package Composer\Installers * @author Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr> */ class DolibarrInstaller extends BaseInstaller { //TODO: Add support for scripts and themes protected $locations = array( 'module' => 'htdocs/custom/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php 0000644 00000000604 15132754524 0027723 0 ustar 00 <?php namespace Composer\Installers; class TheliaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'local/modules/{$name}/', 'frontoffice-template' => 'templates/frontOffice/{$name}/', 'backoffice-template' => 'templates/backOffice/{$name}/', 'email-template' => 'templates/email/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php 0000644 00000002377 15132754524 0030123 0 ustar 00 <?php namespace Composer\Installers; class OctoberInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/', 'theme' => 'themes/{$vendor}-{$name}/' ); /** * Format package name. * * For package type october-plugin, cut off a trailing '-plugin' if present. * * For package type october-theme, cut off a trailing '-theme' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'october-plugin') { return $this->inflectPluginVars($vars); } if ($vars['type'] === 'october-theme') { return $this->inflectThemeVars($vars); } return $vars; } protected function inflectPluginVars($vars) { $vars['name'] = preg_replace('/^oc-|-plugin$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } protected function inflectThemeVars($vars) { $vars['name'] = preg_replace('/^oc-|-theme$/', '', $vars['name']); $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/StarbugInstaller.php 0000644 00000000461 15132754524 0030125 0 ustar 00 <?php namespace Composer\Installers; class StarbugInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', 'custom-module' => 'app/modules/{$name}/', 'custom-theme' => 'app/themes/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php 0000644 00000000741 15132754524 0027740 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle MODX Evolution specifics when installing packages. */ class MODXEvoInstaller extends BaseInstaller { protected $locations = array( 'snippet' => 'assets/snippets/{$name}/', 'plugin' => 'assets/plugins/{$name}/', 'module' => 'assets/modules/{$name}/', 'template' => 'assets/templates/{$name}/', 'lib' => 'assets/lib/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php 0000644 00000000421 15132754524 0030104 0 ustar 00 <?php namespace Composer\Installers; class MagentoInstaller extends BaseInstaller { protected $locations = array( 'theme' => 'app/design/frontend/{$name}/', 'skin' => 'skin/frontend/default/{$name}/', 'library' => 'lib/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/EzPlatformInstaller.php 0000644 00000000354 15132754524 0030523 0 ustar 00 packages <?php namespace Composer\Installers; class EzPlatformInstaller extends BaseInstaller { protected $locations = array( 'meta-assets' => 'web/assets/ezplatform/', 'assets' => 'web/assets/ezplatform/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php 0000644 00000000461 15132754524 0030107 0 ustar 00 <?php namespace Composer\Installers; class EliasisInstaller extends BaseInstaller { protected $locations = array( 'component' => 'components/{$name}/', 'module' => 'modules/{$name}/', 'plugin' => 'plugins/{$name}/', 'template' => 'templates/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PxcmsInstaller.php 0000644 00000003734 15132754524 0027616 0 ustar 00 <?php namespace Composer\Installers; class PxcmsInstaller extends BaseInstaller { protected $locations = array( 'module' => 'app/Modules/{$name}/', 'theme' => 'themes/{$name}/', ); /** * Format package name. * * @param array $vars * * @return array */ public function inflectPackageVars($vars) { if ($vars['type'] === 'pxcms-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'pxcms-theme') { return $this->inflectThemeVars($vars); } return $vars; } /** * For package type pxcms-module, cut off a trailing '-plugin' if present. * * return string */ protected function inflectModuleVars($vars) { $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) $vars['name'] = str_replace('module-', '', $vars['name']); // strip out module- $vars['name'] = preg_replace('/-module$/', '', $vars['name']); // strip out -module $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s $vars['name'] = ucwords($vars['name']); // make module name camelcased return $vars; } /** * For package type pxcms-module, cut off a trailing '-plugin' if present. * * return string */ protected function inflectThemeVars($vars) { $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) $vars['name'] = str_replace('theme-', '', $vars['name']); // strip out theme- $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); // strip out -theme $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s $vars['name'] = ucwords($vars['name']); // make module name camelcased return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php 0000644 00000000640 15132754524 0027443 0 ustar 00 <?php namespace Composer\Installers; /** * Composer installer for 3rd party Tusk utilities * @author Drew Ewing <drew@phenocode.com> */ class TuskInstaller extends BaseInstaller { protected $locations = array( 'task' => '.tusk/tasks/{$name}/', 'command' => '.tusk/commands/{$name}/', 'asset' => 'assets/tusk/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php 0000644 00000000400 15132754524 0027752 0 ustar 00 <?php namespace Composer\Installers; class PhiftyInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'bundles/{$name}/', 'library' => 'libraries/{$name}/', 'framework' => 'frameworks/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ModxInstaller.php 0000644 00000000364 15132754524 0027427 0 ustar 00 <?php namespace Composer\Installers; /** * An installer to handle MODX specifics when installing packages. */ class ModxInstaller extends BaseInstaller { protected $locations = array( 'extra' => 'core/packages/{$name}/' ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php 0000644 00000002127 15132754524 0031073 0 ustar 00 packages <?php namespace Composer\Installers; use Composer\Package\PackageInterface; class SilverStripeInstaller extends BaseInstaller { protected $locations = array( 'module' => '{$name}/', 'theme' => 'themes/{$name}/', ); /** * Return the install path based on package type. * * Relies on built-in BaseInstaller behaviour with one exception: silverstripe/framework * must be installed to 'sapphire' and not 'framework' if the version is <3.0.0 * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { if ( $package->getName() == 'silverstripe/framework' && preg_match('/^\d+\.\d+\.\d+/', $package->getVersion()) && version_compare($package->getVersion(), '2.999.999') < 0 ) { return $this->templatePath($this->locations['module'], array('name' => 'sapphire')); } return parent::getInstallPath($package, $frameworkType); } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php 0000644 00000001543 15132754524 0027747 0 ustar 00 <?php namespace Composer\Installers; class DrupalInstaller extends BaseInstaller { protected $locations = array( 'core' => 'core/', 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', 'library' => 'libraries/{$name}/', 'profile' => 'profiles/{$name}/', 'database-driver' => 'drivers/lib/Drupal/Driver/Database/{$name}/', 'drush' => 'drush/{$name}/', 'custom-theme' => 'themes/custom/{$name}/', 'custom-module' => 'modules/custom/{$name}/', 'custom-profile' => 'profiles/custom/{$name}/', 'drupal-multisite' => 'sites/{$name}/', 'console' => 'console/{$name}/', 'console-language' => 'console/language/{$name}/', 'config' => 'config/sync/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php0000644 00000000251 15132754524 0030271 0 ustar 00 <?php namespace Composer\Installers; class AttogramInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php 0000644 00000000322 15132754524 0030563 0 ustar 00 packages <?php namespace Composer\Installers; class PrestashopInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', 'theme' => 'themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php0000644 00000003156 15132754524 0030312 0 ustar 00 <?php namespace Composer\Installers; /** * Plugin/theme installer for shopware * @author Benjamin Boit */ class ShopwareInstaller extends BaseInstaller { protected $locations = array( 'backend-plugin' => 'engine/Shopware/Plugins/Local/Backend/{$name}/', 'core-plugin' => 'engine/Shopware/Plugins/Local/Core/{$name}/', 'frontend-plugin' => 'engine/Shopware/Plugins/Local/Frontend/{$name}/', 'theme' => 'templates/{$name}/', 'plugin' => 'custom/plugins/{$name}/', 'frontend-theme' => 'themes/Frontend/{$name}/', ); /** * Transforms the names * @param array $vars * @return array */ public function inflectPackageVars($vars) { if ($vars['type'] === 'shopware-theme') { return $this->correctThemeName($vars); } return $this->correctPluginName($vars); } /** * Changes the name to a camelcased combination of vendor and name * @param array $vars * @return array */ private function correctPluginName($vars) { $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { return strtoupper($matches[0][1]); }, $vars['name']); $vars['name'] = ucfirst($vars['vendor']) . ucfirst($camelCasedName); return $vars; } /** * Changes the name to a underscore separated name * @param array $vars * @return array */ private function correctThemeName($vars) { $vars['name'] = str_replace('-', '_', $vars['name']); return $vars; } } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/SiteDirectInstaller.php 0000644 00000001216 15132754524 0030475 0 ustar 00 packages <?php namespace Composer\Installers; class SiteDirectInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$vendor}/{$name}/', 'plugin' => 'plugins/{$vendor}/{$name}/' ); public function inflectPackageVars($vars) { return $this->parseVars($vars); } protected function parseVars($vars) { $vars['vendor'] = strtolower($vars['vendor']) == 'sitedirect' ? 'SiteDirect' : $vars['vendor']; $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php 0000644 00000001223 15132754524 0027571 0 ustar 00 <?php namespace Composer\Installers; class RadPHPInstaller extends BaseInstaller { protected $locations = array( 'bundle' => 'src/{$name}/' ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $nameParts = explode('/', $vars['name']); foreach ($nameParts as &$value) { $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); $value = str_replace(array('-', '_'), ' ', $value); $value = str_replace(' ', '', ucwords($value)); } $vars['name'] = implode('/', $nameParts); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php 0000644 00000001427 15132754524 0027410 0 ustar 00 <?php namespace Composer\Installers; class MayaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); /** * Format package name. * * For package type maya-module, cut off a trailing '-module' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'maya-module') { return $this->inflectModuleVars($vars); } return $vars; } protected function inflectModuleVars($vars) { $vars['name'] = preg_replace('/-module$/', '', $vars['name']); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/Redaxo5Installer.php 0000644 00000000404 15132754524 0030022 0 ustar 00 <?php namespace Composer\Installers; class Redaxo5Installer extends BaseInstaller { protected $locations = array( 'addon' => 'redaxo/src/addons/{$name}/', 'bestyle-plugin' => 'redaxo/src/addons/be_style/plugins/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php 0000644 00000000336 15132754524 0027364 0 ustar 00 <?php namespace Composer\Installers; class ChefInstaller extends BaseInstaller { protected $locations = array( 'cookbook' => 'Chef/{$vendor}/{$name}/', 'role' => 'Chef/roles/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php0000644 00000000326 15132754524 0032005 0 ustar 00 packages <?php namespace Composer\Installers; class ClanCatsFrameworkInstaller extends BaseInstaller { protected $locations = array( 'ship' => 'CCF/orbit/{$name}/', 'theme' => 'CCF/app/themes/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MantisBTInstaller.php0000644 00000001110 15132754524 0030167 0 ustar 00 <?php namespace Composer\Installers; use Composer\DependencyResolver\Pool; class MantisBTInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Format package name to CamelCase */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php 0000644 00000000250 15132754524 0027727 0 ustar 00 <?php namespace Composer\Installers; class AimeosInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'ext/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php 0000644 00000000253 15132754524 0030103 0 ustar 00 <?php namespace Composer\Installers; class LaravelInstaller extends BaseInstaller { protected $locations = array( 'library' => 'libraries/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php0000644 00000000344 15132754524 0030217 0 ustar 00 <?php namespace Composer\Installers; class LavaLiteInstaller extends BaseInstaller { protected $locations = array( 'package' => 'packages/{$vendor}/{$name}/', 'theme' => 'public/themes/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php 0000644 00000001311 15132754524 0031274 0 ustar 00 packages <?php namespace Composer\Installers; class PlentymarketsInstaller extends BaseInstaller { protected $locations = array( 'plugin' => '{$name}/' ); /** * Remove hyphen, "plugin" and format to camelcase * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $vars['name'] = explode("-", $vars['name']); foreach ($vars['name'] as $key => $name) { $vars['name'][$key] = ucfirst($vars['name'][$key]); if (strcasecmp($name, "Plugin") == 0) { unset($vars['name'][$key]); } } $vars['name'] = implode("",$vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php 0000644 00000000241 15132754524 0027370 0 ustar 00 <?php namespace Composer\Installers; class ElggInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'mod/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php 0000644 00000000247 15132754524 0027721 0 ustar 00 <?php namespace Composer\Installers; class KohanaInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php 0000644 00000000251 15132754524 0027770 0 ustar 00 <?php namespace Composer\Installers; class PuppetInstaller extends BaseInstaller { protected $locations = array( 'module' => 'modules/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php 0000644 00000000436 15132754524 0030655 0 ustar 00 packages <?php namespace Composer\Installers; class AnnotateCmsInstaller extends BaseInstaller { protected $locations = array( 'module' => 'addons/modules/{$name}/', 'component' => 'addons/components/{$name}/', 'service' => 'addons/services/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php 0000644 00000010340 15132754524 0030532 0 ustar 00 packages <?php namespace Composer\Installers; class MicroweberInstaller extends BaseInstaller { protected $locations = array( 'module' => 'userfiles/modules/{$install_item_dir}/', 'module-skin' => 'userfiles/modules/{$install_item_dir}/templates/', 'template' => 'userfiles/templates/{$install_item_dir}/', 'element' => 'userfiles/elements/{$install_item_dir}/', 'vendor' => 'vendor/{$install_item_dir}/', 'components' => 'components/{$install_item_dir}/' ); /** * Format package name. * * For package type microweber-module, cut off a trailing '-module' if present * * For package type microweber-template, cut off a trailing '-template' if present. * */ public function inflectPackageVars($vars) { if ($this->package->getTargetDir()) { $vars['install_item_dir'] = $this->package->getTargetDir(); } else { $vars['install_item_dir'] = $vars['name']; if ($vars['type'] === 'microweber-template') { return $this->inflectTemplateVars($vars); } if ($vars['type'] === 'microweber-templates') { return $this->inflectTemplatesVars($vars); } if ($vars['type'] === 'microweber-core') { return $this->inflectCoreVars($vars); } if ($vars['type'] === 'microweber-adapter') { return $this->inflectCoreVars($vars); } if ($vars['type'] === 'microweber-module') { return $this->inflectModuleVars($vars); } if ($vars['type'] === 'microweber-modules') { return $this->inflectModulesVars($vars); } if ($vars['type'] === 'microweber-skin') { return $this->inflectSkinVars($vars); } if ($vars['type'] === 'microweber-element' or $vars['type'] === 'microweber-elements') { return $this->inflectElementVars($vars); } } return $vars; } protected function inflectTemplateVars($vars) { $vars['install_item_dir'] = preg_replace('/-template$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/template-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectTemplatesVars($vars) { $vars['install_item_dir'] = preg_replace('/-templates$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/templates-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectCoreVars($vars) { $vars['install_item_dir'] = preg_replace('/-providers$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-provider$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-adapter$/', '', $vars['install_item_dir']); return $vars; } protected function inflectModuleVars($vars) { $vars['install_item_dir'] = preg_replace('/-module$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/module-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectModulesVars($vars) { $vars['install_item_dir'] = preg_replace('/-modules$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/modules-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectSkinVars($vars) { $vars['install_item_dir'] = preg_replace('/-skin$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/skin-$/', '', $vars['install_item_dir']); return $vars; } protected function inflectElementVars($vars) { $vars['install_item_dir'] = preg_replace('/-elements$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/elements-$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/-element$/', '', $vars['install_item_dir']); $vars['install_item_dir'] = preg_replace('/element-$/', '', $vars['install_item_dir']); return $vars; } } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php 0000644 00000000711 15132754524 0030363 0 ustar 00 packages <?php namespace Composer\Installers; class RoundcubeInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); /** * Lowercase name and changes the name to a underscores * * @param array $vars * @return array */ public function inflectPackageVars($vars) { $vars['name'] = strtolower(str_replace('-', '_', $vars['name'])); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/CiviCrmInstaller.php 0000644 00000000243 15132754524 0030050 0 ustar 00 <?php namespace Composer\Installers; class CiviCrmInstaller extends BaseInstaller { protected $locations = array( 'ext' => 'ext/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php0000644 00000000444 15132754524 0030104 0 ustar 00 <?php namespace Composer\Installers; class ImageCMSInstaller extends BaseInstaller { protected $locations = array( 'template' => 'templates/{$name}/', 'module' => 'application/modules/{$name}/', 'library' => 'application/libraries/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php 0000644 00000000256 15132754524 0027433 0 ustar 00 <?php namespace Composer\Installers; class ItopInstaller extends BaseInstaller { protected $locations = array( 'extension' => 'extensions/{$name}/', ); } woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php 0000644 00000002422 15132754524 0030301 0 ustar 00 packages <?php namespace Composer\Installers; class MediaWikiInstaller extends BaseInstaller { protected $locations = array( 'core' => 'core/', 'extension' => 'extensions/{$name}/', 'skin' => 'skins/{$name}/', ); /** * Format package name. * * For package type mediawiki-extension, cut off a trailing '-extension' if present and transform * to CamelCase keeping existing uppercase chars. * * For package type mediawiki-skin, cut off a trailing '-skin' if present. * */ public function inflectPackageVars($vars) { if ($vars['type'] === 'mediawiki-extension') { return $this->inflectExtensionVars($vars); } if ($vars['type'] === 'mediawiki-skin') { return $this->inflectSkinVars($vars); } return $vars; } protected function inflectExtensionVars($vars) { $vars['name'] = preg_replace('/-extension$/', '', $vars['name']); $vars['name'] = str_replace('-', ' ', $vars['name']); $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); return $vars; } protected function inflectSkinVars($vars) { $vars['name'] = preg_replace('/-skin$/', '', $vars['name']); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php 0000644 00000007753 15132754524 0027403 0 ustar 00 <?php namespace Composer\Installers; use Composer\IO\IOInterface; use Composer\Composer; use Composer\Package\PackageInterface; abstract class BaseInstaller { protected $locations = array(); protected $composer; protected $package; protected $io; /** * Initializes base installer. * * @param PackageInterface $package * @param Composer $composer * @param IOInterface $io */ public function __construct(PackageInterface $package = null, Composer $composer = null, IOInterface $io = null) { $this->composer = $composer; $this->package = $package; $this->io = $io; } /** * Return the install path based on package type. * * @param PackageInterface $package * @param string $frameworkType * @return string */ public function getInstallPath(PackageInterface $package, $frameworkType = '') { $type = $this->package->getType(); $prettyName = $this->package->getPrettyName(); if (strpos($prettyName, '/') !== false) { list($vendor, $name) = explode('/', $prettyName); } else { $vendor = ''; $name = $prettyName; } $availableVars = $this->inflectPackageVars(compact('name', 'vendor', 'type')); $extra = $package->getExtra(); if (!empty($extra['installer-name'])) { $availableVars['name'] = $extra['installer-name']; } if ($this->composer->getPackage()) { $extra = $this->composer->getPackage()->getExtra(); if (!empty($extra['installer-paths'])) { $customPath = $this->mapCustomInstallPaths($extra['installer-paths'], $prettyName, $type, $vendor); if ($customPath !== false) { return $this->templatePath($customPath, $availableVars); } } } $packageType = substr($type, strlen($frameworkType) + 1); $locations = $this->getLocations(); if (!isset($locations[$packageType])) { throw new \InvalidArgumentException(sprintf('Package type "%s" is not supported', $type)); } return $this->templatePath($locations[$packageType], $availableVars); } /** * For an installer to override to modify the vars per installer. * * @param array<string, string> $vars This will normally receive array{name: string, vendor: string, type: string} * @return array<string, string> */ public function inflectPackageVars($vars) { return $vars; } /** * Gets the installer's locations * * @return array<string, string> map of package types => install path */ public function getLocations() { return $this->locations; } /** * Replace vars in a path * * @param string $path * @param array<string, string> $vars * @return string */ protected function templatePath($path, array $vars = array()) { if (strpos($path, '{') !== false) { extract($vars); preg_match_all('@\{\$([A-Za-z0-9_]*)\}@i', $path, $matches); if (!empty($matches[1])) { foreach ($matches[1] as $var) { $path = str_replace('{$' . $var . '}', $$var, $path); } } } return $path; } /** * Search through a passed paths array for a custom install path. * * @param array $paths * @param string $name * @param string $type * @param string $vendor = NULL * @return string|false */ protected function mapCustomInstallPaths(array $paths, $name, $type, $vendor = NULL) { foreach ($paths as $path => $names) { $names = (array) $names; if (in_array($name, $names) || in_array('type:' . $type, $names) || in_array('vendor:' . $vendor, $names)) { return $path; } } return false; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/GravInstaller.php 0000644 00000001274 15132754524 0027420 0 ustar 00 <?php namespace Composer\Installers; class GravInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'user/plugins/{$name}/', 'theme' => 'user/themes/{$name}/', ); /** * Format package name * * @param array $vars * * @return array */ public function inflectPackageVars($vars) { $restrictedWords = implode('|', array_keys($this->locations)); $vars['name'] = strtolower($vars['name']); $vars['name'] = preg_replace('/^(?:grav-)?(?:(?:'.$restrictedWords.')-)?(.*?)(?:-(?:'.$restrictedWords.'))?$/ui', '$1', $vars['name'] ); return $vars; } } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/OsclassInstaller.php 0000644 00000000447 15132754524 0030131 0 ustar 00 <?php namespace Composer\Installers; class OsclassInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'oc-content/plugins/{$name}/', 'theme' => 'oc-content/themes/{$name}/', 'language' => 'oc-content/languages/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php 0000644 00000000336 15132754524 0030132 0 ustar 00 <?php namespace Composer\Installers; class LithiumInstaller extends BaseInstaller { protected $locations = array( 'library' => 'libraries/{$name}/', 'source' => 'libraries/_source/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/MiaoxingInstaller.php0000644 00000000252 15132754524 0030267 0 ustar 00 <?php namespace Composer\Installers; class MiaoxingInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php 0000644 00000000413 15132754524 0027735 0 ustar 00 <?php namespace Composer\Installers; class RedaxoInstaller extends BaseInstaller { protected $locations = array( 'addon' => 'redaxo/include/addons/{$name}/', 'bestyle-plugin' => 'redaxo/include/addons/be_style/plugins/{$name}/' ); } packages/woocommerce-blocks/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php0000644 00000000450 15132754524 0030235 0 ustar 00 <?php namespace Composer\Installers; /** * * Installer for kanboard plugins * * kanboard.net * * Class KanboardInstaller * @package Composer\Installers */ class KanboardInstaller extends BaseInstaller { protected $locations = array( 'plugin' => 'plugins/{$name}/', ); } packages/woocommerce-blocks/vendor/composer/installers/src/bootstrap.php 0000644 00000000724 15132754524 0022750 0 ustar 00 <?php function includeIfExists($file) { if (file_exists($file)) { return include $file; } } if ((!$loader = includeIfExists(__DIR__ . '/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__ . '/../../../autoload.php'))) { die('You must set up the project dependencies, run the following commands:'.PHP_EOL. 'curl -s http://getcomposer.org/installer | php'.PHP_EOL. 'php composer.phar install'.PHP_EOL); } return $loader; packages/woocommerce-blocks/vendor/composer/installers/LICENSE 0000644 00000002046 15132754524 0020437 0 ustar 00 Copyright (c) 2012 Kyle Robinson Young Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. packages/woocommerce-blocks/vendor/composer/jetpack_autoload_psr4.php 0000644 00000001343 15132754524 0022243 0 ustar 00 <?php // This file `jetpack_autoload_psr4.php` was auto generated by automattic/jetpack-autoloader. $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Composer\\Installers\\' => array( 'version' => '1.11.0.0', 'path' => array( $vendorDir . '/composer/installers/src/Composer/Installers' ) ), 'Automattic\\WooCommerce\\Blocks\\Tests\\' => array( 'version' => '6.1.0.0', 'path' => array( $baseDir . '/tests/php' ) ), 'Automattic\\WooCommerce\\Blocks\\' => array( 'version' => '6.1.0.0', 'path' => array( $baseDir . '/src' ) ), 'Automattic\\Jetpack\\Autoloader\\' => array( 'version' => '2.10.4.0', 'path' => array( $vendorDir . '/automattic/jetpack-autoloader/src' ) ), ); packages/woocommerce-blocks/vendor/composer/autoload_psr4.php 0000644 00000000635 15132754524 0020545 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'), 'Automattic\\WooCommerce\\Blocks\\' => array($baseDir . '/src'), 'Automattic\\Jetpack\\Autoloader\\' => array($vendorDir . '/automattic/jetpack-autoloader/src'), ); packages/woocommerce-blocks/vendor/composer/jetpack_autoload_classmap.php 0000644 00000000563 15132754524 0023161 0 ustar 00 <?php // This file `jetpack_autoload_classmap.php` was auto generated by automattic/jetpack-autoloader. $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => array( 'version' => '2.10.4.0', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php' ), ); packages/woocommerce-blocks/vendor/composer/ClassLoader.php 0000644 00000034065 15132754524 0020165 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { private $vendorDir; // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); private $fallbackDirsPsr4 = array(); // PSR-0 private $prefixesPsr0 = array(); private $fallbackDirsPsr0 = array(); private $useIncludePath = false; private $classMap = array(); private $classMapAuthoritative = false; private $missingClasses = array(); private $apcuPrefix; private static $registeredLoaders = array(); public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; } public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } public function getFallbackDirs() { return $this->fallbackDirsPsr0; } public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 base directories */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders indexed by their corresponding vendor directories. * * @return self[] */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. */ function includeFile($file) { include $file; } packages/woocommerce-blocks/vendor/composer/installed.php 0000644 00000003562 15132754524 0017746 0 ustar 00 <?php return array( 'root' => array( 'pretty_version' => 'dev-trunk', 'version' => 'dev-trunk', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'reference' => '8967acb720260debeb0b3991589cc3d3f1403d86', 'name' => 'woocommerce/woocommerce-blocks', 'dev' => false, ), 'versions' => array( 'automattic/jetpack-autoloader' => array( 'pretty_version' => 'v2.10.4', 'version' => '2.10.4.0', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../automattic/jetpack-autoloader', 'aliases' => array(), 'reference' => '70cb300a7a215ae87c671f600f77093518f87bac', 'dev_requirement' => false, ), 'composer/installers' => array( 'pretty_version' => 'v1.11.0', 'version' => '1.11.0.0', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/./installers', 'aliases' => array(), 'reference' => 'ae03311f45dfe194412081526be2e003960df74b', 'dev_requirement' => false, ), 'roundcube/plugin-installer' => array( 'dev_requirement' => false, 'replaced' => array( 0 => '*', ), ), 'shama/baton' => array( 'dev_requirement' => false, 'replaced' => array( 0 => '*', ), ), 'woocommerce/woocommerce-blocks' => array( 'pretty_version' => 'dev-trunk', 'version' => 'dev-trunk', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'reference' => '8967acb720260debeb0b3991589cc3d3f1403d86', 'dev_requirement' => false, ), ), ); packages/woocommerce-blocks/vendor/composer/LICENSE 0000644 00000002056 15132754524 0016260 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. packages/woocommerce-blocks/vendor/composer/autoload_static.php 0000644 00000003215 15132754524 0021141 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInitb78ca910fe07339d6189615f1734a3e3 { public static $prefixLengthsPsr4 = array ( 'C' => array ( 'Composer\\Installers\\' => 20, ), 'A' => array ( 'Automattic\\WooCommerce\\Blocks\\' => 30, 'Automattic\\Jetpack\\Autoloader\\' => 30, ), ); public static $prefixDirsPsr4 = array ( 'Composer\\Installers\\' => array ( 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers', ), 'Automattic\\WooCommerce\\Blocks\\' => array ( 0 => __DIR__ . '/../..' . '/src', ), 'Automattic\\Jetpack\\Autoloader\\' => array ( 0 => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src', ), ); public static $classMap = array ( 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitb78ca910fe07339d6189615f1734a3e3::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitb78ca910fe07339d6189615f1734a3e3::$prefixDirsPsr4; $loader->classMap = ComposerStaticInitb78ca910fe07339d6189615f1734a3e3::$classMap; }, null, ClassLoader::class); } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-path-processor.php 0000644 00000012557 15132754524 0026737 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles dealing with paths for the autoloader. */ class Path_Processor { /** * Given a path this will replace any of the path constants with a token to represent it. * * @param string $path The path we want to process. * * @return string The tokenized path. */ public function tokenize_path_constants( $path ) { $path = wp_normalize_path( $path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $len = strlen( $constant_path ); if ( substr( $path, 0, $len ) !== $constant_path ) { continue; } return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); } return $path; } /** * Given a path this will replace any of the path constant tokens with the expanded path. * * @param string $tokenized_path The path we want to process. * * @return string The expanded path. */ public function untokenize_path_constants( $tokenized_path ) { $tokenized_path = wp_normalize_path( $tokenized_path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $constant = '{{' . $constant . '}}'; $len = strlen( $constant ); if ( substr( $tokenized_path, 0, $len ) !== $constant ) { continue; } return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); } return $tokenized_path; } /** * Given a file and an array of places it might be, this will find the absolute path and return it. * * @param string $file The plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|false Returns the absolute path to the directory, otherwise false. */ public function find_directory_with_autoloader( $file, $directories_to_check ) { $file = wp_normalize_path( $file ); if ( ! $this->is_absolute_path( $file ) ) { $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); if ( ! isset( $file ) ) { return false; } } // We need the real path for consistency with __DIR__ paths. $file = $this->get_real_path( $file ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged $directory = @is_file( $file ) ? dirname( $file ) : $file; if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { return false; } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged return $directory; } /** * Fetches an array of normalized paths keyed by the constant they came from. * * @return string[] The normalized paths keyed by the constant. */ private function get_normalized_constants() { $raw_constants = array( // Order the constants from most-specific to least-specific. 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH', ); $constants = array(); foreach ( $raw_constants as $raw ) { if ( ! defined( $raw ) ) { continue; } $path = wp_normalize_path( constant( $raw ) ); if ( isset( $path ) ) { $constants[ $raw ] = $path; } } return $constants; } /** * Indicates whether or not a path is absolute. * * @param string $path The path to check. * * @return bool True if the path is absolute, otherwise false. */ private function is_absolute_path( $path ) { if ( 0 === strlen( $path ) || '.' === $path[0] ) { return false; } // Absolute paths on Windows may begin with a drive letter. if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { return true; } // A path starting with / or \ is absolute; anything else is relative. return ( '/' === $path[0] || '\\' === $path[0] ); } /** * Given a file and a list of directories to check, this method will try to figure out * the absolute path to the file in question. * * @param string $normalized_path The normalized path to the plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|null The absolute path to the plugin file, otherwise null. */ private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { // We're only able to find the absolute path for plugin/theme PHP files. if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { return null; } foreach ( $directories_to_check as $directory ) { $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_file( $normalized_check ) ) { return $normalized_check; } } return null; } /** * Given a path this will figure out the real path that we should be using. * * @param string $path The path to resolve. * * @return string The resolved path. */ private function get_real_path( $path ) { // We want to resolve symbolic links for consistency with __DIR__ paths. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $real_path = @realpath( $path ); if ( false === $real_path ) { // Let the autoloader deal with paths that don't exist. $real_path = $path; } // Using realpath will make it platform-specific so we must normalize it after. if ( $path !== $real_path ) { $real_path = wp_normalize_path( $real_path ); } return $real_path; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-plugins-handler.php 0000644 00000013071 15132754524 0027052 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles locating and caching all of the active plugins. */ class Plugins_Handler { /** * The transient key for plugin paths. */ const TRANSIENT_KEY = 'jetpack_autoloader_plugin_paths'; /** * The locator for finding plugins in different locations. * * @var Plugin_Locator */ private $plugin_locator; /** * The processor for transforming cached paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Plugin_Locator $plugin_locator The locator for finding active plugins. * @param Path_Processor $path_processor The processor for transforming cached paths. */ public function __construct( $plugin_locator, $path_processor ) { $this->plugin_locator = $plugin_locator; $this->path_processor = $path_processor; } /** * Gets all of the active plugins we can find. * * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. * * @return string[] */ public function get_active_plugins( $include_deactivating, $record_unknown ) { global $jetpack_autoloader_activating_plugins_paths; // We're going to build a unique list of plugins from a few different sources // to find all of our "active" plugins. While we need to return an integer // array, we're going to use an associative array internally to reduce // the amount of time that we're going to spend checking uniqueness // and merging different arrays together to form the output. $active_plugins = array(); // Make sure that plugins which have activated this request are considered as "active" even though // they probably won't be present in any option. if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { $active_plugins[ $path ] = $path; } } // This option contains all of the plugins that have been activated. $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // This option contains all of the multisite plugins that have been activated. if ( is_multisite() ) { $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } } // These actions contain plugins that are being activated/deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // When the current plugin isn't considered "active" there's a problem. // Since we're here, the plugin is active and currently being loaded. // We can support this case (mu-plugins and non-standard activation) // by adding the current plugin to the active list and marking it // as an unknown (activating) plugin. This also has the benefit // of causing a reset because the active plugins list has // been changed since it was saved in the global. $current_plugin = $this->plugin_locator->find_current_plugin(); if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { $active_plugins[ $current_plugin ] = $current_plugin; $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; } // When deactivating plugins aren't desired we should entirely remove them from the active list. if ( ! $include_deactivating ) { // These actions contain plugins that are being deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { unset( $active_plugins[ $path ] ); } } // Transform the array so that we don't have to worry about the keys interacting with other array types later. return array_values( $active_plugins ); } /** * Gets all of the cached plugins if there are any. * * @return string[] */ public function get_cached_plugins() { $cached = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $cached ) || empty( $cached ) ) { return array(); } // We need to expand the tokens to an absolute path for this webserver. return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); } /** * Saves the plugin list to the cache. * * @param array $plugins The plugin list to save to the cache. */ public function cache_plugins( $plugins ) { // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); set_transient( self::TRANSIENT_KEY, $plugins ); } /** * Checks to see whether or not the plugin list given has changed when compared to the * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal * with cases where the active list may change due to filtering.. * * @param string[] $plugins The plugins list to check against the global cache. * * @return bool True if the plugins have changed, otherwise false. */ public function have_plugins_changed( $plugins ) { global $jetpack_autoloader_cached_plugin_paths; if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { $jetpack_autoloader_cached_plugin_paths = $plugins; return true; } return false; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-shutdown-handler.php 0000644 00000005203 15132754524 0027242 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles the shutdown of the autoloader. */ class Shutdown_Handler { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The plugins cached by this autoloader. * * @var string[] */ private $cached_plugins; /** * Indicates whether or not this autoloader was included by another. * * @var bool */ private $was_included_by_autoloader; /** * Constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance to use. * @param string[] $cached_plugins The plugins cached by the autoloaer. * @param bool $was_included_by_autoloader Indicates whether or not the autoloader was included by another. */ public function __construct( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) { $this->plugins_handler = $plugins_handler; $this->cached_plugins = $cached_plugins; $this->was_included_by_autoloader = $was_included_by_autoloader; } /** * Handles the shutdown of the autoloader. */ public function __invoke() { // Don't save a broken cache if an error happens during some plugin's initialization. if ( ! did_action( 'plugins_loaded' ) ) { // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // Load the active plugins fresh since the list we pulled earlier might not contain // plugins that were activated but did not reset the autoloader. This happens // when a plugin is in the cache but not "active" when the autoloader loads. // We also want to make sure that plugins which are deactivating are not // considered "active" so that they will be removed from the cache now. try { $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); } catch ( \Exception $ex ) { // When the package is deleted before shutdown it will throw an exception. // In the event this happens we should erase the cache. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // The paths should be sorted for easy comparisons with those loaded from the cache. // Note we don't need to sort the cached entries because they're already sorted. sort( $active_plugins ); // We don't want to waste time saving a cache that hasn't changed. if ( $this->cached_plugins === $active_plugins ) { return; } $this->plugins_handler->cache_plugins( $active_plugins ); } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/ManifestGenerator.php 0000644 00000007132 15132754524 0026271 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Manifest Generator. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export namespace Automattic\Jetpack\Autoloader; /** * Class ManifestGenerator. */ class ManifestGenerator { /** * Builds a manifest file for the given autoloader type. * * @param string $autoloaderType The type of autoloader to build a manifest for. * @param string $fileName The filename of the manifest. * @param array $content The manifest content to generate using. * * @return string|null $manifestFile * @throws \InvalidArgumentException When an invalid autoloader type is given. */ public static function buildManifest( $autoloaderType, $fileName, $content ) { if ( empty( $content ) ) { return null; } switch ( $autoloaderType ) { case 'classmap': case 'files': return self::buildStandardManifest( $fileName, $content ); case 'psr-4': return self::buildPsr4Manifest( $fileName, $content ); } throw new \InvalidArgumentException( 'An invalid manifest type of ' . $autoloaderType . ' was passed!' ); } /** * Builds the contents for the standard manifest file. * * @param string $fileName The filename we are building. * @param array $manifestData The formatted data for the manifest. * * @return string|null $manifestFile */ private static function buildStandardManifest( $fileName, $manifestData ) { $fileContent = PHP_EOL; foreach ( $manifestData as $key => $data ) { $key = var_export( $key, true ); $versionCode = var_export( $data['version'], true ); $fileContent .= <<<MANIFEST_CODE $key => array( 'version' => $versionCode, 'path' => {$data['path']} ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Builds the contents for the PSR-4 manifest file. * * @param string $fileName The filename we are building. * @param array $namespaces The formatted PSR-4 data for the manifest. * * @return string|null $manifestFile */ private static function buildPsr4Manifest( $fileName, $namespaces ) { $fileContent = PHP_EOL; foreach ( $namespaces as $namespace => $data ) { $namespaceCode = var_export( $namespace, true ); $versionCode = var_export( $data['version'], true ); $pathCode = 'array( ' . implode( ', ', $data['path'] ) . ' )'; $fileContent .= <<<MANIFEST_CODE $namespaceCode => array( 'version' => $versionCode, 'path' => $pathCode ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Generate the PHP that will be used in the file. * * @param string $fileName The filename we are building. * @param string $content The content to be written into the file. * * @return string $fileContent */ private static function buildFile( $fileName, $content ) { return <<<INCLUDE_FILE <?php // This file `$fileName` was auto generated by automattic/jetpack-autoloader. \$vendorDir = dirname(__DIR__); \$baseDir = dirname(\$vendorDir); return array($content); INCLUDE_FILE; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-version-loader.php 0000644 00000007664 15132754524 0026722 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class loads other classes based on given parameters. */ class Version_Loader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * A map of available classes and their version and file path. * * @var array */ private $classmap; /** * A map of PSR-4 namespaces and their version and directory path. * * @var array */ private $psr4_map; /** * A map of all the files that we should load. * * @var array */ private $filemap; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. * @param array $classmap The verioned classmap to load using. * @param array $psr4_map The versioned PSR-4 map to load using. * @param array $filemap The versioned filemap to load. */ public function __construct( $version_selector, $classmap, $psr4_map, $filemap ) { $this->version_selector = $version_selector; $this->classmap = $classmap; $this->psr4_map = $psr4_map; $this->filemap = $filemap; } /** * Finds the file path for the given class. * * @param string $class_name The class to find. * * @return string|null $file_path The path to the file if found, null if no class was found. */ public function find_class_file( $class_name ) { $data = $this->select_newest_file( isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, $this->find_psr4_file( $class_name ) ); if ( ! isset( $data ) ) { return null; } return $data['path']; } /** * Load all of the files in the filemap. */ public function load_filemap() { if ( empty( $this->filemap ) ) { return; } foreach ( $this->filemap as $file_identifier => $file_data ) { if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { require_once $file_data['path']; $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; } } } /** * Compares different class sources and returns the newest. * * @param array|null $classmap_data The classmap class data. * @param array|null $psr4_data The PSR-4 class data. * * @return array|null $data */ private function select_newest_file( $classmap_data, $psr4_data ) { if ( ! isset( $classmap_data ) ) { return $psr4_data; } elseif ( ! isset( $psr4_data ) ) { return $classmap_data; } if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { return $psr4_data; } return $classmap_data; } /** * Finds the file for a given class in a PSR-4 namespace. * * @param string $class_name The class to find. * * @return array|null $data The version and path path to the file if found, null otherwise. */ private function find_psr4_file( $class_name ) { if ( ! isset( $this->psr4_map ) ) { return null; } // Don't bother with classes that have no namespace. $class_index = strrpos( $class_name, '\\' ); if ( ! $class_index ) { return null; } $class_for_path = str_replace( '\\', '/', $class_name ); // Search for the namespace by iteratively cutting off the last segment until // we find a match. This allows us to check the most-specific namespaces // first as well as minimize the amount of time spent looking. for ( $class_namespace = substr( $class_name, 0, $class_index ); ! empty( $class_namespace ); $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) ) { $namespace = $class_namespace . '\\'; if ( ! isset( $this->psr4_map[ $namespace ] ) ) { continue; } $data = $this->psr4_map[ $namespace ]; foreach ( $data['path'] as $path ) { $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; if ( file_exists( $path ) ) { return array( 'version' => $data['version'], 'path' => $path, ); } } } return null; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php 0000644 00000014057 15132754524 0027331 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase /** * Custom Autoloader Composer Plugin, hooks into composer events to generate the custom autoloader. * * @package automattic/jetpack-autoloader */ // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound // phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound // phpcs:disable WordPress.Files.FileName.NotHyphenatedLowercase // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase namespace Automattic\Jetpack\Autoloader; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; use Composer\Script\Event; use Composer\Script\ScriptEvents; /** * Class CustomAutoloaderPlugin. * * @package automattic/jetpack-autoloader */ class CustomAutoloaderPlugin implements PluginInterface, EventSubscriberInterface { /** * IO object. * * @var IOInterface IO object. */ private $io; /** * Composer object. * * @var Composer Composer object. */ private $composer; /** * Do nothing. * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function activate( Composer $composer, IOInterface $io ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->composer = $composer; $this->io = $io; } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function deactivate( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function uninstall( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Tell composer to listen for events and do something with them. * * @return array List of subscribed events. */ public static function getSubscribedEvents() { return array( ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', ); } /** * Generate the custom autolaoder. * * @param Event $event Script event object. */ public function postAutoloadDump( Event $event ) { // When the autoloader is not required by the root package we don't want to execute it. // This prevents unwanted transitive execution that generates unused autoloaders or // at worst throws fatal executions. if ( ! $this->isRequiredByRoot() ) { return; } $config = $this->composer->getConfig(); if ( 'vendor' !== $config->raw()['config']['vendor-dir'] ) { $this->io->writeError( "\n<error>An error occurred while generating the autoloader files:", true ); $this->io->writeError( 'The project\'s composer.json or composer environment set a non-default vendor directory.', true ); $this->io->writeError( 'The default composer vendor directory must be used.</error>', true ); exit(); } $installationManager = $this->composer->getInstallationManager(); $repoManager = $this->composer->getRepositoryManager(); $localRepo = $repoManager->getLocalRepository(); $package = $this->composer->getPackage(); $optimize = $event->getFlags()['optimize']; $suffix = $this->determineSuffix(); $generator = new AutoloadGenerator( $this->io ); $generator->dump( $this->composer, $config, $localRepo, $package, $installationManager, 'composer', $optimize, $suffix ); $this->generated = true; } /** * Determine the suffix for the autoloader class. * * Reuses an existing suffix from vendor/autoload_packages.php or vendor/autoload.php if possible. * * @return string Suffix. */ private function determineSuffix() { $config = $this->composer->getConfig(); $vendorPath = $config->get( 'vendor-dir' ); // Command line. $suffix = $config->get( 'autoloader-suffix' ); if ( $suffix ) { return $suffix; } // Reuse our own suffix, if any. if ( is_readable( $vendorPath . '/autoload_packages.php' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $vendorPath . '/autoload_packages.php' ); if ( preg_match( '/^namespace Automattic\\\\Jetpack\\\\Autoloader\\\\jp([^;\s]+);/m', $content, $match ) ) { return $match[1]; } } // Reuse Composer's suffix, if any. if ( is_readable( $vendorPath . '/autoload.php' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $vendorPath . '/autoload.php' ); if ( preg_match( '{ComposerAutoloaderInit([^:\s]+)::}', $content, $match ) ) { return $match[1]; } } // Generate a random suffix. return md5( uniqid( '', true ) ); } /** * Checks to see whether or not the root package is the one that required the autoloader. * * @return bool */ private function isRequiredByRoot() { $package = $this->composer->getPackage(); $requires = $package->getRequires(); if ( ! is_array( $requires ) ) { $requires = array(); } $devRequires = $package->getDevRequires(); if ( ! is_array( $devRequires ) ) { $devRequires = array(); } $requires = array_merge( $requires, $devRequires ); if ( empty( $requires ) ) { $this->io->writeError( "\n<error>The package is not required and this should never happen?</error>", true ); exit(); } foreach ( $requires as $require ) { if ( 'automattic/jetpack-autoloader' === $require->getTarget() ) { return true; } } return false; } } woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-latest-autoloader-guard.php 0000644 00000005062 15132754524 0030431 0 ustar 00 packages <?php /* HEADER */ // phpcs:ignore /** * This class ensures that we're only executing the latest autoloader. */ class Latest_Autoloader_Guard { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The Autoloader_Handler instance. * * @var Autoloader_Handler */ private $autoloader_handler; /** * The Autoloader_locator instance. * * @var Autoloader_Locator */ private $autoloader_locator; /** * The constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance. * @param Autoloader_Handler $autoloader_handler The Autoloader_Handler instance. * @param Autoloader_Locator $autoloader_locator The Autoloader_Locator instance. */ public function __construct( $plugins_handler, $autoloader_handler, $autoloader_locator ) { $this->plugins_handler = $plugins_handler; $this->autoloader_handler = $autoloader_handler; $this->autoloader_locator = $autoloader_locator; } /** * Indicates whether or not the autoloader should be initialized. Note that this function * has the side-effect of actually loading the latest autoloader in the event that this * is not it. * * @param string $current_plugin The current plugin we're checking. * @param string[] $plugins The active plugins to check for autoloaders in. * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. * * @return bool True if we should stop initialization, otherwise false. */ public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { global $jetpack_autoloader_latest_version; // We need to reset the autoloader when the plugins change because // that means the autoloader was generated with a different list. if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { $this->autoloader_handler->reset_autoloader(); } // When the latest autoloader has already been found we don't need to search for it again. // We should take care however because this will also trigger if the autoloader has been // included by an older one. if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { return true; } $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); return true; } return false; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-container.php 0000644 00000011157 15132754524 0025743 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class manages the files and dependencies of the autoloader. */ class Container { /** * Since each autoloader's class files exist within their own namespace we need a map to * convert between the local class and a shared key. Note that no version checking is * performed on these dependencies and the first autoloader to register will be the * one that is utilized. */ const SHARED_DEPENDENCY_KEYS = array( Hook_Manager::class => 'Hook_Manager', ); /** * A map of all the dependencies we've registered with the container and created. * * @var array */ protected $dependencies; /** * The constructor. */ public function __construct() { $this->dependencies = array(); $this->register_shared_dependencies(); $this->register_dependencies(); $this->initialize_globals(); } /** * Gets a dependency out of the container. * * @param string $class The class to fetch. * * @return mixed * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. */ public function get( $class ) { if ( ! isset( $this->dependencies[ $class ] ) ) { throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); } return $this->dependencies[ $class ]; } /** * Registers all of the dependencies that are shared between all instances of the autoloader. */ private function register_shared_dependencies() { global $jetpack_autoloader_container_shared; if ( ! isset( $jetpack_autoloader_container_shared ) ) { $jetpack_autoloader_container_shared = array(); } $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { require_once __DIR__ . '/class-hook-manager.php'; $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); } $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; } /** * Registers all of the dependencies with the container. */ private function register_dependencies() { require_once __DIR__ . '/class-path-processor.php'; $this->dependencies[ Path_Processor::class ] = new Path_Processor(); require_once __DIR__ . '/class-plugin-locator.php'; $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-version-selector.php'; $this->dependencies[ Version_Selector::class ] = new Version_Selector(); require_once __DIR__ . '/class-autoloader-locator.php'; $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-php-autoloader.php'; $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); require_once __DIR__ . '/class-manifest-reader.php'; $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-plugins-handler.php'; $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( $this->get( Plugin_Locator::class ), $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-autoloader-handler.php'; $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( $this->get( PHP_Autoloader::class ), $this->get( Hook_Manager::class ), $this->get( Manifest_Reader::class ), $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-latest-autoloader-guard.php'; $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( $this->get( Plugins_Handler::class ), $this->get( Autoloader_Handler::class ), $this->get( Autoloader_Locator::class ) ); // Register any classes that we will use elsewhere. require_once __DIR__ . '/class-version-loader.php'; require_once __DIR__ . '/class-shutdown-handler.php'; } /** * Initializes any of the globals needed by the autoloader. */ private function initialize_globals() { /* * This global was retired in version 2.9. The value is set to 'false' to maintain * compatibility with older versions of the autoloader. */ global $jetpack_autoloader_including_latest; $jetpack_autoloader_including_latest = false; // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader // but was not discoverable, we will record them in this array to track them as "active". global $jetpack_autoloader_activating_plugins_paths; if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { $jetpack_autoloader_activating_plugins_paths = array(); } } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-plugin-locator.php 0000644 00000010174 15132754524 0026716 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class scans the WordPress installation to find active plugins. */ class Plugin_Locator { /** * The path processor for finding plugin paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Path_Processor $path_processor The Path_Processor instance. */ public function __construct( $path_processor ) { $this->path_processor = $path_processor; } /** * Finds the path to the current plugin. * * @return string $path The path to the current plugin. * * @throws \RuntimeException If the current plugin does not have an autoloader. */ public function find_current_plugin() { // Escape from `vendor/__DIR__` to root plugin directory. $plugin_directory = dirname( dirname( __DIR__ ) ); // Use the path processor to ensure that this is an autoloader we're referencing. $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); if ( false === $path ) { throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); } return $path; } /** * Checks a given option for plugin paths. * * @param string $option_name The option that we want to check for plugin information. * @param bool $site_option Indicates whether or not we want to check the site option. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_option( $option_name, $site_option = false ) { $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); if ( false === $raw ) { return array(); } return $this->convert_plugins_to_paths( $raw ); } /** * Checks for plugins in the `action` request parameter. * * @param string[] $allowed_actions The actions that we're allowed to return plugins for. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_request_action( $allowed_actions ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended /** * Note: we're not actually checking the nonce here because it's too early * in the execution. The pluggable functions are not yet loaded to give * plugins a chance to plug their versions. Therefore we're doing the bare * minimum: checking whether the nonce exists and it's in the right place. * The request will fail later if the nonce doesn't pass the check. */ if ( empty( $_REQUEST['_wpnonce'] ) ) { return array(); } $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; if ( ! in_array( $action, $allowed_actions, true ) ) { return array(); } $plugin_slugs = array(); switch ( $action ) { case 'activate': case 'deactivate': if ( empty( $_REQUEST['plugin'] ) ) { break; } $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); break; case 'activate-selected': case 'deactivate-selected': if ( empty( $_REQUEST['checked'] ) ) { break; } $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); break; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $this->convert_plugins_to_paths( $plugin_slugs ); } /** * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter * out the plugins that are not directory plugins. Note that array keys will also be included * if they are plugin paths! * * @param string[] $plugins Plugin paths or slugs to filter. * * @return string[] */ private function convert_plugins_to_paths( $plugins ) { if ( ! is_array( $plugins ) || empty( $plugins ) ) { return array(); } // We're going to look for plugins in the standard directories. $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); $plugin_paths = array(); foreach ( $plugins as $key => $value ) { $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } } return $plugin_paths; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/autoload.php 0000644 00000000173 15132754524 0024462 0 ustar 00 <?php /* HEADER */ // phpcs:ignore require_once __DIR__ . '/jetpack-autoloader/class-autoloader.php'; Autoloader::init(); packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/AutoloadFileWriter.php 0000644 00000006211 15132754524 0026416 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoloader file writer. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite namespace Automattic\Jetpack\Autoloader; /** * Class AutoloadFileWriter. */ class AutoloadFileWriter { /** * The file comment to use. */ const COMMENT = <<<AUTOLOADER_COMMENT /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ AUTOLOADER_COMMENT; /** * Copies autoloader files and replaces any placeholders in them. * * @param IOInterface|null $io An IO for writing to. * @param string $outDir The directory to place the autoloader files in. * @param string $suffix The suffix to use in the autoloader's namespace. */ public static function copyAutoloaderFiles( $io, $outDir, $suffix ) { $renameList = array( 'autoload.php' => '../autoload_packages.php', ); $ignoreList = array( 'AutoloadGenerator.php', 'AutoloadProcessor.php', 'CustomAutoloaderPlugin.php', 'ManifestGenerator.php', 'AutoloadFileWriter.php', ); // Copy all of the autoloader files. $files = scandir( __DIR__ ); foreach ( $files as $file ) { // Only PHP files will be copied. if ( substr( $file, -4 ) !== '.php' ) { continue; } if ( in_array( $file, $ignoreList, true ) ) { continue; } $newFile = isset( $renameList[ $file ] ) ? $renameList[ $file ] : $file; $content = self::prepareAutoloaderFile( $file, $suffix ); $written = file_put_contents( $outDir . '/' . $newFile, $content ); if ( $io ) { if ( $written ) { $io->writeError( " <info>Generated: $newFile</info>" ); } else { $io->writeError( " <error>Error: $newFile</error>" ); } } } } /** * Prepares an autoloader file to be written to the destination. * * @param String $filename a file to prepare. * @param String $suffix Unique suffix used in the namespace. * * @return string */ private static function prepareAutoloaderFile( $filename, $suffix ) { $header = self::COMMENT; $header .= PHP_EOL; $header .= 'namespace Automattic\Jetpack\Autoloader\jp' . $suffix . ';'; $header .= PHP_EOL . PHP_EOL; $sourceLoader = fopen( __DIR__ . '/' . $filename, 'r' ); $file_contents = stream_get_contents( $sourceLoader ); return str_replace( '/* HEADER */', $header, $file_contents ); } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-version-selector.php 0000644 00000003165 15132754524 0027264 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Used to select package versions. */ class Version_Selector { /** * Checks whether the selected package version should be updated. Composer development * package versions ('9999999-dev' or versions that start with 'dev-') are favored * when the JETPACK_AUTOLOAD_DEV constant is set to true. * * @param String $selected_version The currently selected package version. * @param String $compare_version The package version that is being evaluated to * determine if the version needs to be updated. * * @return bool Returns true if the selected package version should be updated, * else false. */ public function is_version_update_required( $selected_version, $compare_version ) { $use_dev_versions = defined( 'JETPACK_AUTOLOAD_DEV' ) && JETPACK_AUTOLOAD_DEV; if ( is_null( $selected_version ) ) { return true; } if ( $use_dev_versions && $this->is_dev_version( $selected_version ) ) { return false; } if ( $this->is_dev_version( $compare_version ) ) { if ( $use_dev_versions ) { return true; } else { return false; } } if ( version_compare( $selected_version, $compare_version, '<' ) ) { return true; } return false; } /** * Checks whether the given package version is a development version. * * @param String $version The package version. * * @return bool True if the version is a dev version, else false. */ public function is_dev_version( $version ) { if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { return true; } return false; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-autoloader.php 0000644 00000007573 15132754524 0026127 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class Autoloader { /** * Checks to see whether or not the autoloader should be initialized and then initializes it if so. * * @param Container|null $container The container we want to use for autoloader initialization. If none is given * then a container will be created automatically. */ public static function init( $container = null ) { // The container holds and manages the lifecycle of our dependencies // to make them easier to work with and increase flexibility. if ( ! isset( $container ) ) { require_once __DIR__ . '/class-container.php'; $container = new Container(); } // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @var Autoloader_Handler $autoloader_handler */ $autoloader_handler = $container->get( Autoloader_Handler::class ); // If the autoloader is already initializing it means that it has included us as the latest. $was_included_by_autoloader = $autoloader_handler->is_initializing(); /** @var Plugin_Locator $plugin_locator */ $plugin_locator = $container->get( Plugin_Locator::class ); /** @var Plugins_Handler $plugins_handler */ $plugins_handler = $container->get( Plugins_Handler::class ); // The current plugin is the one that we are attempting to initialize here. $current_plugin = $plugin_locator->find_current_plugin(); // The active plugins are those that we were able to discover on the site. This list will not // include mu-plugins, those activated by code, or those who are hidden by filtering. We also // want to take care to not consider the current plugin unknown if it was included by an // autoloader. This avoids the case where a plugin will be marked "active" while deactivated // due to it having the latest autoloader. $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. // Note that it's possible this list will include plugins that have since been deactivated, but after a request // the cache should be updated and the deactivated plugins will be removed. $cached_plugins = $plugins_handler->get_cached_plugins(); // We combine the active list and cached list to preemptively load classes for plugins that are // presently unknown but will be loaded during the request. While this may result in us considering packages in // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. $all_plugins = array_merge( $active_plugins, $cached_plugins ); // In particular we also include the current plugin to address the case where it is the latest autoloader // but also unknown (and not cached). We don't want it in the active list because we don't know that it // is active but we need it in the all plugins list so that it is considered by the autoloader. $all_plugins[] = $current_plugin; // We require uniqueness in the array to avoid processing the same plugin more than once. $all_plugins = array_values( array_unique( $all_plugins ) ); /** @var Latest_Autoloader_Guard $guard */ $guard = $container->get( Latest_Autoloader_Guard::class ); if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { return; } // Initialize the autoloader using the handler now that we're ready. $autoloader_handler->activate_autoloader( $all_plugins ); /** @var Hook_Manager $hook_manager */ $hook_manager = $container->get( Hook_Manager::class ); // Register a shutdown handler to clean up the autoloader. $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); // phpcs:enable Generic.Commenting.DocComment.MissingShort } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-php-autoloader.php 0000644 00000005071 15132754524 0026703 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class PHP_Autoloader { /** * Registers the autoloader with PHP so that it can begin autoloading classes. * * @param Version_Loader $version_loader The class loader to use in the autoloader. */ public function register_autoloader( $version_loader ) { // Make sure no other autoloaders are registered. $this->unregister_autoloader(); // Set the global so that it can be used to load classes. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = $version_loader; // Ensure that the autoloader is first to avoid contention with others. spl_autoload_register( array( self::class, 'load_class' ), true, true ); } /** * Unregisters the active autoloader so that it will no longer autoload classes. */ public function unregister_autoloader() { // Remove any v2 autoloader that we've already registered. $autoload_chain = spl_autoload_functions(); foreach ( $autoload_chain as $autoloader ) { // We can identify a v2 autoloader using the namespace. $namespace_check = null; // Functions are recorded as strings. if ( is_string( $autoloader ) ) { $namespace_check = $autoloader; } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { // Static method calls have the class as the first array element. $namespace_check = $autoloader[0]; } else { // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. continue; } // Check for the namespace without the generated suffix. if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { spl_autoload_unregister( $autoloader ); } } // Clear the global now that the autoloader has been unregistered. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = null; } /** * Loads a class file if one could be found. * * Note: This function is static so that the autoloader can be easily unregistered. If * it was a class method we would have to unwrap the object to check the namespace. * * @param string $class_name The name of the class to autoload. * * @return bool Indicates whether or not a class file was loaded. */ public static function load_class( $class_name ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return; } $file = $jetpack_autoloader_loader->find_class_file( $class_name ); if ( ! isset( $file ) ) { return false; } require $file; return true; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-manifest-reader.php 0000644 00000004673 15132754524 0027034 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class reads autoloader manifest files. */ class Manifest_Reader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Reads all of the manifests in the given plugin paths. * * @param array $plugin_paths The paths to the plugins we're loading the manifest in. * @param string $manifest_path The path that we're loading the manifest from in each plugin. * @param array $path_map The path map to add the contents of the manifests to. * * @return array $path_map The path map we've built using the manifests in each plugin. */ public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { $file_paths = array_map( function ( $path ) use ( $manifest_path ) { return trailingslashit( $path ) . $manifest_path; }, $plugin_paths ); foreach ( $file_paths as $path ) { $this->register_manifest( $path, $path_map ); } return $path_map; } /** * Registers a plugin's manifest file with the path map. * * @param string $manifest_path The absolute path to the manifest that we're loading. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_manifest( $manifest_path, &$path_map ) { if ( ! is_readable( $manifest_path ) ) { return; } $manifest = require $manifest_path; if ( ! is_array( $manifest ) ) { return; } foreach ( $manifest as $key => $data ) { $this->register_record( $key, $data, $path_map ); } } /** * Registers an entry from the manifest in the path map. * * @param string $key The identifier for the entry we're registering. * @param array $data The data for the entry we're registering. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_record( $key, $data, &$path_map ) { if ( isset( $path_map[ $key ]['version'] ) ) { $selected_version = $path_map[ $key ]['version']; } else { $selected_version = null; } if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { $path_map[ $key ] = array( 'version' => $data['version'], 'path' => $data['path'], ); } } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-autoloader-locator.php 0000644 00000003616 15132754524 0027562 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class locates autoloaders. */ class Autoloader_Locator { /** * The object for comparing autoloader versions. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The version selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Finds the path to the plugin with the latest autoloader. * * @param array $plugin_paths An array of plugin paths. * @param string $latest_version The latest version reference. * * @return string|null */ public function find_latest_autoloader( $plugin_paths, &$latest_version ) { $latest_plugin = null; foreach ( $plugin_paths as $plugin_path ) { $version = $this->get_autoloader_version( $plugin_path ); if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { continue; } $latest_version = $version; $latest_plugin = $plugin_path; } return $latest_plugin; } /** * Gets the path to the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string */ public function get_autoloader_path( $plugin_path ) { return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; } /** * Gets the version for the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string|null */ public function get_autoloader_version( $plugin_path ) { $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; if ( ! file_exists( $classmap ) ) { return null; } $classmap = require $classmap; if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { return $classmap[ AutoloadGenerator::class ]['version']; } return null; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/AutoloadProcessor.php 0000644 00000012357 15132754524 0026331 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoload Processor. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase namespace Automattic\Jetpack\Autoloader; /** * Class AutoloadProcessor. */ class AutoloadProcessor { /** * A callable for scanning a directory for all of its classes. * * @var callable */ private $classmapScanner; /** * A callable for transforming a path into one to be used in code. * * @var callable */ private $pathCodeTransformer; /** * The constructor. * * @param callable $classmapScanner A callable for scanning a directory for all of its classes. * @param callable $pathCodeTransformer A callable for transforming a path into one to be used in code. */ public function __construct( $classmapScanner, $pathCodeTransformer ) { $this->classmapScanner = $classmapScanner; $this->pathCodeTransformer = $pathCodeTransformer; } /** * Processes the classmap autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array $processed */ public function processClassmap( $autoloads, $scanPsrPackages ) { // We can't scan PSR packages if we don't actually have any. if ( empty( $autoloads['psr-4'] ) ) { $scanPsrPackages = false; } if ( empty( $autoloads['classmap'] ) && ! $scanPsrPackages ) { return null; } $excludedClasses = null; if ( ! empty( $autoloads['exclude-from-classmap'] ) ) { $excludedClasses = '{(' . implode( '|', $autoloads['exclude-from-classmap'] ) . ')}'; } $processed = array(); if ( $scanPsrPackages ) { foreach ( $autoloads['psr-4'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } /* * PSR-0 namespaces are converted to classmaps for both optimized and unoptimized autoloaders because any new * development should use classmap or PSR-4 autoloading. */ if ( ! empty( $autoloads['psr-0'] ) ) { foreach ( $autoloads['psr-0'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } if ( ! empty( $autoloads['classmap'] ) ) { foreach ( $autoloads['classmap'] as $package ) { $classmap = call_user_func( $this->classmapScanner, $package['path'], $excludedClasses, null ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } return $processed; } /** * Processes the PSR-4 autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array $processed */ public function processPsr4Packages( $autoloads, $scanPsrPackages ) { if ( $scanPsrPackages || empty( $autoloads['psr-4'] ) ) { return null; } $processed = array(); foreach ( $autoloads['psr-4'] as $namespace => $packages ) { $namespace = empty( $namespace ) ? null : $namespace; $paths = array(); foreach ( $packages as $package ) { $paths[] = call_user_func( $this->pathCodeTransformer, $package['path'] ); } $processed[ $namespace ] = array( 'version' => $package['version'], 'path' => $paths, ); } return $processed; } /** * Processes the file autoloads into a relative format including the version for each file. * * @param array $autoloads The autoloads we are processing. * * @return array|null $processed */ public function processFiles( $autoloads ) { if ( empty( $autoloads['files'] ) ) { return null; } $processed = array(); foreach ( $autoloads['files'] as $file_id => $package ) { $processed[ $file_id ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $package['path'] ), ); } return $processed; } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-hook-manager.php 0000644 00000003643 15132754524 0026332 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Allows the latest autoloader to register hooks that can be removed when the autoloader is reset. */ class Hook_Manager { /** * An array containing all of the hooks that we've registered. * * @var array */ private $registered_hooks; /** * The constructor. */ public function __construct() { $this->registered_hooks = array(); } /** * Adds an action to WordPress and registers it internally. * * @param string $tag The name of the action which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the action. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_action( $tag, $callable, $priority, $accepted_args ); } /** * Adds a filter to WordPress and registers it internally. * * @param string $tag The name of the filter which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the filter. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_filter( $tag, $callable, $priority, $accepted_args ); } /** * Removes all of the registered hooks. */ public function reset() { foreach ( $this->registered_hooks as $tag => $hooks ) { foreach ( $hooks as $hook ) { remove_filter( $tag, $hook['callable'], $hook['priority'] ); } } $this->registered_hooks = array(); } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/class-autoloader-handler.php 0000644 00000010465 15132754524 0027534 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class selects the package version for the autoloader. */ class Autoloader_Handler { /** * The PHP_Autoloader instance. * * @var PHP_Autoloader */ private $php_autoloader; /** * The Hook_Manager instance. * * @var Hook_Manager */ private $hook_manager; /** * The Manifest_Reader instance. * * @var Manifest_Reader */ private $manifest_reader; /** * The Version_Selector instance. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param PHP_Autoloader $php_autoloader The PHP_Autoloader instance. * @param Hook_Manager $hook_manager The Hook_Manager instance. * @param Manifest_Reader $manifest_reader The Manifest_Reader instance. * @param Version_Selector $version_selector The Version_Selector instance. */ public function __construct( $php_autoloader, $hook_manager, $manifest_reader, $version_selector ) { $this->php_autoloader = $php_autoloader; $this->hook_manager = $hook_manager; $this->manifest_reader = $manifest_reader; $this->version_selector = $version_selector; } /** * Checks to see whether or not an autoloader is currently in the process of initializing. * * @return bool */ public function is_initializing() { // If no version has been set it means that no autoloader has started initializing yet. global $jetpack_autoloader_latest_version; if ( ! isset( $jetpack_autoloader_latest_version ) ) { return false; } // When the version is set but the classmap is not it ALWAYS means that this is the // latest autoloader and is being included by an older one. global $jetpack_packages_classmap; if ( empty( $jetpack_packages_classmap ) ) { return true; } // Version 2.4.0 added a new global and altered the reset semantics. We need to check // the other global as well since it may also point at initialization. // Note: We don't need to check for the class first because every autoloader that // will set the latest version global requires this class in the classmap. $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return true; } } return false; } /** * Activates an autoloader using the given plugins and activates it. * * @param string[] $plugins The plugins to initialize the autoloader for. */ public function activate_autoloader( $plugins ) { global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); $loader = new Version_Loader( $this->version_selector, $jetpack_packages_classmap, $jetpack_packages_psr4, $jetpack_packages_filemap ); $this->php_autoloader->register_autoloader( $loader ); // Now that the autoloader is active we can load the filemap. $loader->load_filemap(); } /** * Resets the active autoloader and all related global state. */ public function reset_autoloader() { $this->php_autoloader->unregister_autoloader(); $this->hook_manager->reset(); // Clear all of the autoloader globals so that older autoloaders don't do anything strange. global $jetpack_autoloader_latest_version; $jetpack_autoloader_latest_version = null; global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/src/AutoloadGenerator.php 0000644 00000033623 15132754524 0026277 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * Autoloader Generator. * * @package automattic/jetpack-autoloader */ // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound // phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound // phpcs:disable PHPCompatibility.FunctionDeclarations.NewClosure.Found // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound // phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_dirFound // phpcs:disable WordPress.Files.FileName.InvalidClassFileName // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase // phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase namespace Automattic\Jetpack\Autoloader; use Composer\Autoload\ClassMapGenerator; use Composer\Composer; use Composer\Config; use Composer\Installer\InstallationManager; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\PackageSorter; /** * Class AutoloadGenerator. */ class AutoloadGenerator { /** * The filesystem utility. * * @var Filesystem */ private $filesystem; /** * Instantiate an AutoloadGenerator object. * * @param IOInterface $io IO object. */ public function __construct( IOInterface $io = null ) { $this->io = $io; $this->filesystem = new Filesystem(); } /** * Dump the Jetpack autoloader files. * * @param Composer $composer The Composer object. * @param Config $config Config object. * @param InstalledRepositoryInterface $localRepo Installed Repository object. * @param PackageInterface $mainPackage Main Package object. * @param InstallationManager $installationManager Manager for installing packages. * @param string $targetDir Path to the current target directory. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * @param string $suffix The autoloader suffix. */ public function dump( Composer $composer, Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = null ) { $this->filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) ); $packageMap = $composer->getAutoloadGenerator()->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() ); $autoloads = $this->parseAutoloads( $packageMap, $mainPackage ); // Convert the autoloads into a format that the manifest generator can consume more easily. $basePath = $this->filesystem->normalizePath( realpath( getcwd() ) ); $vendorPath = $this->filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) ); $processedAutoloads = $this->processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ); unset( $packageMap, $autoloads ); // Make sure none of the legacy files remain that can lead to problems with the autoloader. $this->removeLegacyFiles( $vendorPath ); // Write all of the files now that we're done. $this->writeAutoloaderFiles( $vendorPath . '/jetpack-autoloader/', $suffix ); $this->writeManifests( $vendorPath . '/' . $targetDir, $processedAutoloads ); if ( ! $scanPsrPackages ) { $this->io->writeError( '<warning>You are generating an unoptimized autoloader. If this is a production build, consider using the -o option.</warning>' ); } } /** * Compiles an ordered list of namespace => path mappings * * @param array $packageMap Array of array(package, installDir-relative-to-composer.json). * @param PackageInterface $mainPackage Main package instance. * * @return array The list of path mappings. */ public function parseAutoloads( array $packageMap, PackageInterface $mainPackage ) { $rootPackageMap = array_shift( $packageMap ); $sortedPackageMap = $this->sortPackageMap( $packageMap ); $sortedPackageMap[] = $rootPackageMap; array_unshift( $packageMap, $rootPackageMap ); $psr0 = $this->parseAutoloadsType( $packageMap, 'psr-0', $mainPackage ); $psr4 = $this->parseAutoloadsType( $packageMap, 'psr-4', $mainPackage ); $classmap = $this->parseAutoloadsType( array_reverse( $sortedPackageMap ), 'classmap', $mainPackage ); $files = $this->parseAutoloadsType( $sortedPackageMap, 'files', $mainPackage ); krsort( $psr0 ); krsort( $psr4 ); return array( 'psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files, ); } /** * Sorts packages by dependency weight * * Packages of equal weight retain the original order * * @param array $packageMap The package map. * * @return array */ protected function sortPackageMap( array $packageMap ) { $packages = array(); $paths = array(); foreach ( $packageMap as $item ) { list( $package, $path ) = $item; $name = $package->getName(); $packages[ $name ] = $package; $paths[ $name ] = $path; } $sortedPackages = PackageSorter::sortPackages( $packages ); $sortedPackageMap = array(); foreach ( $sortedPackages as $package ) { $name = $package->getName(); $sortedPackageMap[] = array( $packages[ $name ], $paths[ $name ] ); } return $sortedPackageMap; } /** * Returns the file identifier. * * @param PackageInterface $package The package instance. * @param string $path The path. */ protected function getFileIdentifier( PackageInterface $package, $path ) { return md5( $package->getName() . ':' . $path ); } /** * Returns the path code for the given path. * * @param Filesystem $filesystem The filesystem instance. * @param string $basePath The base path. * @param string $vendorPath The vendor path. * @param string $path The path. * * @return string The path code. */ protected function getPathCode( Filesystem $filesystem, $basePath, $vendorPath, $path ) { if ( ! $filesystem->isAbsolutePath( $path ) ) { $path = $basePath . '/' . $path; } $path = $filesystem->normalizePath( $path ); $baseDir = ''; if ( 0 === strpos( $path . '/', $vendorPath . '/' ) ) { $path = substr( $path, strlen( $vendorPath ) ); $baseDir = '$vendorDir'; if ( false !== $path ) { $baseDir .= ' . '; } } else { $path = $filesystem->normalizePath( $filesystem->findShortestPath( $basePath, $path, true ) ); if ( ! $filesystem->isAbsolutePath( $path ) ) { $baseDir = '$baseDir . '; $path = '/' . $path; } } if ( strpos( $path, '.phar' ) !== false ) { $baseDir = "'phar://' . " . $baseDir; } return $baseDir . ( ( false !== $path ) ? var_export( $path, true ) : '' ); } /** * This function differs from the composer parseAutoloadsType in that beside returning the path. * It also return the path and the version of a package. * * Supports PSR-4, PSR-0, and classmap parsing. * * @param array $packageMap Map of all the packages. * @param string $type Type of autoloader to use. * @param PackageInterface $mainPackage Instance of the Package Object. * * @return array */ protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) { $autoloads = array(); foreach ( $packageMap as $item ) { list($package, $installPath) = $item; $autoload = $package->getAutoload(); if ( $package === $mainPackage ) { $autoload = array_merge_recursive( $autoload, $package->getDevAutoload() ); } if ( null !== $package->getTargetDir() && $package !== $mainPackage ) { $installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) ); } if ( in_array( $type, array( 'psr-4', 'psr-0' ), true ) && isset( $autoload[ $type ] ) && is_array( $autoload[ $type ] ) ) { foreach ( $autoload[ $type ] as $namespace => $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $namespace ][] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? ); } } } if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) { foreach ( $autoload['classmap'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? ); } } } if ( 'files' === $type && isset( $autoload['files'] ) && is_array( $autoload['files'] ) ) { foreach ( $autoload['files'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $this->getFileIdentifier( $package, $path ) ] = array( 'path' => $relativePath, 'version' => $package->getVersion(), // Version of the file comes from the package - should we try to parse it? ); } } } } return $autoloads; } /** * Given Composer's autoloads this will convert them to a version that we can use to generate the manifests. * * When the $scanPsrPackages argument is true, PSR-4 namespaces are converted to classmaps. When $scanPsrPackages * is false, PSR-4 namespaces are not converted to classmaps. * * PSR-0 namespaces are always converted to classmaps. * * @param array $autoloads The autoloads we want to process. * @param bool $scanPsrPackages Whether or not PSR-4 packages should be converted to a classmap. * @param string $vendorPath The path to the vendor directory. * @param string $basePath The path to the current directory. * * @return array $processedAutoloads */ private function processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ) { $processor = new AutoloadProcessor( function ( $path, $excludedClasses, $namespace ) use ( $basePath ) { $dir = $this->filesystem->normalizePath( $this->filesystem->isAbsolutePath( $path ) ? $path : $basePath . '/' . $path ); return ClassMapGenerator::createMap( $dir, $excludedClasses, null, // Don't pass the IOInterface since the normal autoload generation will have reported already. empty( $namespace ) ? null : $namespace ); }, function ( $path ) use ( $basePath, $vendorPath ) { return $this->getPathCode( $this->filesystem, $basePath, $vendorPath, $path ); } ); return array( 'psr-4' => $processor->processPsr4Packages( $autoloads, $scanPsrPackages ), 'classmap' => $processor->processClassmap( $autoloads, $scanPsrPackages ), 'files' => $processor->processFiles( $autoloads ), ); } /** * Removes all of the legacy autoloader files so they don't cause any problems. * * @param string $outDir The directory legacy files are written to. */ private function removeLegacyFiles( $outDir ) { $files = array( 'autoload_functions.php', 'class-autoloader-handler.php', 'class-classes-handler.php', 'class-files-handler.php', 'class-plugins-handler.php', 'class-version-selector.php', ); foreach ( $files as $file ) { $this->filesystem->remove( $outDir . '/' . $file ); } } /** * Writes all of the autoloader files to disk. * * @param string $outDir The directory to write to. * @param string $suffix The unique autoloader suffix. */ private function writeAutoloaderFiles( $outDir, $suffix ) { $this->io->writeError( "<info>Generating jetpack autoloader ($outDir)</info>" ); // We will remove all autoloader files to generate this again. $this->filesystem->emptyDirectory( $outDir ); // Write the autoloader files. AutoloadFileWriter::copyAutoloaderFiles( $this->io, $outDir, $suffix ); } /** * Writes all of the manifest files to disk. * * @param string $outDir The directory to write to. * @param array $processedAutoloads The processed autoloads. */ private function writeManifests( $outDir, $processedAutoloads ) { $this->io->writeError( "<info>Generating jetpack autoloader manifests ($outDir)</info>" ); $manifestFiles = array( 'classmap' => 'jetpack_autoload_classmap.php', 'psr-4' => 'jetpack_autoload_psr4.php', 'files' => 'jetpack_autoload_filemap.php', ); foreach ( $manifestFiles as $key => $file ) { // Make sure the file doesn't exist so it isn't there if we don't write it. $this->filesystem->remove( $outDir . '/' . $file ); if ( empty( $processedAutoloads[ $key ] ) ) { continue; } $content = ManifestGenerator::buildManifest( $key, $file, $processedAutoloads[ $key ] ); if ( empty( $content ) ) { continue; } if ( file_put_contents( $outDir . '/' . $file, $content ) ) { $this->io->writeError( " <info>Generated: $file</info>" ); } else { $this->io->writeError( " <error>Error: $file</error>" ); } } } } packages/woocommerce-blocks/vendor/automattic/jetpack-autoloader/LICENSE.txt 0000644 00000043760 15132754524 0023206 0 ustar 00 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =================================== GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. packages/woocommerce-blocks/vendor/autoload_packages.php 0000644 00000000475 15132754524 0017606 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore require_once __DIR__ . '/jetpack-autoloader/class-autoloader.php'; Autoloader::init(); packages/woocommerce-blocks/vendor/jetpack-autoloader/class-container.php 0000644 00000011461 15132754524 0023000 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class manages the files and dependencies of the autoloader. */ class Container { /** * Since each autoloader's class files exist within their own namespace we need a map to * convert between the local class and a shared key. Note that no version checking is * performed on these dependencies and the first autoloader to register will be the * one that is utilized. */ const SHARED_DEPENDENCY_KEYS = array( Hook_Manager::class => 'Hook_Manager', ); /** * A map of all the dependencies we've registered with the container and created. * * @var array */ protected $dependencies; /** * The constructor. */ public function __construct() { $this->dependencies = array(); $this->register_shared_dependencies(); $this->register_dependencies(); $this->initialize_globals(); } /** * Gets a dependency out of the container. * * @param string $class The class to fetch. * * @return mixed * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. */ public function get( $class ) { if ( ! isset( $this->dependencies[ $class ] ) ) { throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); } return $this->dependencies[ $class ]; } /** * Registers all of the dependencies that are shared between all instances of the autoloader. */ private function register_shared_dependencies() { global $jetpack_autoloader_container_shared; if ( ! isset( $jetpack_autoloader_container_shared ) ) { $jetpack_autoloader_container_shared = array(); } $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { require_once __DIR__ . '/class-hook-manager.php'; $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); } $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; } /** * Registers all of the dependencies with the container. */ private function register_dependencies() { require_once __DIR__ . '/class-path-processor.php'; $this->dependencies[ Path_Processor::class ] = new Path_Processor(); require_once __DIR__ . '/class-plugin-locator.php'; $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-version-selector.php'; $this->dependencies[ Version_Selector::class ] = new Version_Selector(); require_once __DIR__ . '/class-autoloader-locator.php'; $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-php-autoloader.php'; $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); require_once __DIR__ . '/class-manifest-reader.php'; $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-plugins-handler.php'; $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( $this->get( Plugin_Locator::class ), $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-autoloader-handler.php'; $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( $this->get( PHP_Autoloader::class ), $this->get( Hook_Manager::class ), $this->get( Manifest_Reader::class ), $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-latest-autoloader-guard.php'; $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( $this->get( Plugins_Handler::class ), $this->get( Autoloader_Handler::class ), $this->get( Autoloader_Locator::class ) ); // Register any classes that we will use elsewhere. require_once __DIR__ . '/class-version-loader.php'; require_once __DIR__ . '/class-shutdown-handler.php'; } /** * Initializes any of the globals needed by the autoloader. */ private function initialize_globals() { /* * This global was retired in version 2.9. The value is set to 'false' to maintain * compatibility with older versions of the autoloader. */ global $jetpack_autoloader_including_latest; $jetpack_autoloader_including_latest = false; // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader // but was not discoverable, we will record them in this array to track them as "active". global $jetpack_autoloader_activating_plugins_paths; if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { $jetpack_autoloader_activating_plugins_paths = array(); } } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-latest-autoloader-guard.php 0000644 00000005364 15132754524 0025554 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class ensures that we're only executing the latest autoloader. */ class Latest_Autoloader_Guard { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The Autoloader_Handler instance. * * @var Autoloader_Handler */ private $autoloader_handler; /** * The Autoloader_locator instance. * * @var Autoloader_Locator */ private $autoloader_locator; /** * The constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance. * @param Autoloader_Handler $autoloader_handler The Autoloader_Handler instance. * @param Autoloader_Locator $autoloader_locator The Autoloader_Locator instance. */ public function __construct( $plugins_handler, $autoloader_handler, $autoloader_locator ) { $this->plugins_handler = $plugins_handler; $this->autoloader_handler = $autoloader_handler; $this->autoloader_locator = $autoloader_locator; } /** * Indicates whether or not the autoloader should be initialized. Note that this function * has the side-effect of actually loading the latest autoloader in the event that this * is not it. * * @param string $current_plugin The current plugin we're checking. * @param string[] $plugins The active plugins to check for autoloaders in. * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. * * @return bool True if we should stop initialization, otherwise false. */ public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { global $jetpack_autoloader_latest_version; // We need to reset the autoloader when the plugins change because // that means the autoloader was generated with a different list. if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { $this->autoloader_handler->reset_autoloader(); } // When the latest autoloader has already been found we don't need to search for it again. // We should take care however because this will also trigger if the autoloader has been // included by an older one. if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { return true; } $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); return true; } return false; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-autoloader.php 0000644 00000010075 15132754524 0023155 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class Autoloader { /** * Checks to see whether or not the autoloader should be initialized and then initializes it if so. * * @param Container|null $container The container we want to use for autoloader initialization. If none is given * then a container will be created automatically. */ public static function init( $container = null ) { // The container holds and manages the lifecycle of our dependencies // to make them easier to work with and increase flexibility. if ( ! isset( $container ) ) { require_once __DIR__ . '/class-container.php'; $container = new Container(); } // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @var Autoloader_Handler $autoloader_handler */ $autoloader_handler = $container->get( Autoloader_Handler::class ); // If the autoloader is already initializing it means that it has included us as the latest. $was_included_by_autoloader = $autoloader_handler->is_initializing(); /** @var Plugin_Locator $plugin_locator */ $plugin_locator = $container->get( Plugin_Locator::class ); /** @var Plugins_Handler $plugins_handler */ $plugins_handler = $container->get( Plugins_Handler::class ); // The current plugin is the one that we are attempting to initialize here. $current_plugin = $plugin_locator->find_current_plugin(); // The active plugins are those that we were able to discover on the site. This list will not // include mu-plugins, those activated by code, or those who are hidden by filtering. We also // want to take care to not consider the current plugin unknown if it was included by an // autoloader. This avoids the case where a plugin will be marked "active" while deactivated // due to it having the latest autoloader. $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. // Note that it's possible this list will include plugins that have since been deactivated, but after a request // the cache should be updated and the deactivated plugins will be removed. $cached_plugins = $plugins_handler->get_cached_plugins(); // We combine the active list and cached list to preemptively load classes for plugins that are // presently unknown but will be loaded during the request. While this may result in us considering packages in // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. $all_plugins = array_merge( $active_plugins, $cached_plugins ); // In particular we also include the current plugin to address the case where it is the latest autoloader // but also unknown (and not cached). We don't want it in the active list because we don't know that it // is active but we need it in the all plugins list so that it is considered by the autoloader. $all_plugins[] = $current_plugin; // We require uniqueness in the array to avoid processing the same plugin more than once. $all_plugins = array_values( array_unique( $all_plugins ) ); /** @var Latest_Autoloader_Guard $guard */ $guard = $container->get( Latest_Autoloader_Guard::class ); if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { return; } // Initialize the autoloader using the handler now that we're ready. $autoloader_handler->activate_autoloader( $all_plugins ); /** @var Hook_Manager $hook_manager */ $hook_manager = $container->get( Hook_Manager::class ); // Register a shutdown handler to clean up the autoloader. $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); // phpcs:enable Generic.Commenting.DocComment.MissingShort } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-php-autoloader.php 0000644 00000005373 15132754524 0023747 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class PHP_Autoloader { /** * Registers the autoloader with PHP so that it can begin autoloading classes. * * @param Version_Loader $version_loader The class loader to use in the autoloader. */ public function register_autoloader( $version_loader ) { // Make sure no other autoloaders are registered. $this->unregister_autoloader(); // Set the global so that it can be used to load classes. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = $version_loader; // Ensure that the autoloader is first to avoid contention with others. spl_autoload_register( array( self::class, 'load_class' ), true, true ); } /** * Unregisters the active autoloader so that it will no longer autoload classes. */ public function unregister_autoloader() { // Remove any v2 autoloader that we've already registered. $autoload_chain = spl_autoload_functions(); foreach ( $autoload_chain as $autoloader ) { // We can identify a v2 autoloader using the namespace. $namespace_check = null; // Functions are recorded as strings. if ( is_string( $autoloader ) ) { $namespace_check = $autoloader; } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { // Static method calls have the class as the first array element. $namespace_check = $autoloader[0]; } else { // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. continue; } // Check for the namespace without the generated suffix. if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { spl_autoload_unregister( $autoloader ); } } // Clear the global now that the autoloader has been unregistered. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = null; } /** * Loads a class file if one could be found. * * Note: This function is static so that the autoloader can be easily unregistered. If * it was a class method we would have to unwrap the object to check the namespace. * * @param string $class_name The name of the class to autoload. * * @return bool Indicates whether or not a class file was loaded. */ public static function load_class( $class_name ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return; } $file = $jetpack_autoloader_loader->find_class_file( $class_name ); if ( ! isset( $file ) ) { return false; } require $file; return true; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-plugins-handler.php 0000644 00000013373 15132754524 0024116 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class handles locating and caching all of the active plugins. */ class Plugins_Handler { /** * The transient key for plugin paths. */ const TRANSIENT_KEY = 'jetpack_autoloader_plugin_paths'; /** * The locator for finding plugins in different locations. * * @var Plugin_Locator */ private $plugin_locator; /** * The processor for transforming cached paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Plugin_Locator $plugin_locator The locator for finding active plugins. * @param Path_Processor $path_processor The processor for transforming cached paths. */ public function __construct( $plugin_locator, $path_processor ) { $this->plugin_locator = $plugin_locator; $this->path_processor = $path_processor; } /** * Gets all of the active plugins we can find. * * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. * * @return string[] */ public function get_active_plugins( $include_deactivating, $record_unknown ) { global $jetpack_autoloader_activating_plugins_paths; // We're going to build a unique list of plugins from a few different sources // to find all of our "active" plugins. While we need to return an integer // array, we're going to use an associative array internally to reduce // the amount of time that we're going to spend checking uniqueness // and merging different arrays together to form the output. $active_plugins = array(); // Make sure that plugins which have activated this request are considered as "active" even though // they probably won't be present in any option. if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { $active_plugins[ $path ] = $path; } } // This option contains all of the plugins that have been activated. $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // This option contains all of the multisite plugins that have been activated. if ( is_multisite() ) { $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } } // These actions contain plugins that are being activated/deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // When the current plugin isn't considered "active" there's a problem. // Since we're here, the plugin is active and currently being loaded. // We can support this case (mu-plugins and non-standard activation) // by adding the current plugin to the active list and marking it // as an unknown (activating) plugin. This also has the benefit // of causing a reset because the active plugins list has // been changed since it was saved in the global. $current_plugin = $this->plugin_locator->find_current_plugin(); if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { $active_plugins[ $current_plugin ] = $current_plugin; $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; } // When deactivating plugins aren't desired we should entirely remove them from the active list. if ( ! $include_deactivating ) { // These actions contain plugins that are being deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { unset( $active_plugins[ $path ] ); } } // Transform the array so that we don't have to worry about the keys interacting with other array types later. return array_values( $active_plugins ); } /** * Gets all of the cached plugins if there are any. * * @return string[] */ public function get_cached_plugins() { $cached = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $cached ) || empty( $cached ) ) { return array(); } // We need to expand the tokens to an absolute path for this webserver. return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); } /** * Saves the plugin list to the cache. * * @param array $plugins The plugin list to save to the cache. */ public function cache_plugins( $plugins ) { // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); set_transient( self::TRANSIENT_KEY, $plugins ); } /** * Checks to see whether or not the plugin list given has changed when compared to the * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal * with cases where the active list may change due to filtering.. * * @param string[] $plugins The plugins list to check against the global cache. * * @return bool True if the plugins have changed, otherwise false. */ public function have_plugins_changed( $plugins ) { global $jetpack_autoloader_cached_plugin_paths; if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { $jetpack_autoloader_cached_plugin_paths = $plugins; return true; } return false; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-version-loader.php 0000644 00000010166 15132754524 0023750 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class loads other classes based on given parameters. */ class Version_Loader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * A map of available classes and their version and file path. * * @var array */ private $classmap; /** * A map of PSR-4 namespaces and their version and directory path. * * @var array */ private $psr4_map; /** * A map of all the files that we should load. * * @var array */ private $filemap; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. * @param array $classmap The verioned classmap to load using. * @param array $psr4_map The versioned PSR-4 map to load using. * @param array $filemap The versioned filemap to load. */ public function __construct( $version_selector, $classmap, $psr4_map, $filemap ) { $this->version_selector = $version_selector; $this->classmap = $classmap; $this->psr4_map = $psr4_map; $this->filemap = $filemap; } /** * Finds the file path for the given class. * * @param string $class_name The class to find. * * @return string|null $file_path The path to the file if found, null if no class was found. */ public function find_class_file( $class_name ) { $data = $this->select_newest_file( isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, $this->find_psr4_file( $class_name ) ); if ( ! isset( $data ) ) { return null; } return $data['path']; } /** * Load all of the files in the filemap. */ public function load_filemap() { if ( empty( $this->filemap ) ) { return; } foreach ( $this->filemap as $file_identifier => $file_data ) { if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { require_once $file_data['path']; $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; } } } /** * Compares different class sources and returns the newest. * * @param array|null $classmap_data The classmap class data. * @param array|null $psr4_data The PSR-4 class data. * * @return array|null $data */ private function select_newest_file( $classmap_data, $psr4_data ) { if ( ! isset( $classmap_data ) ) { return $psr4_data; } elseif ( ! isset( $psr4_data ) ) { return $classmap_data; } if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { return $psr4_data; } return $classmap_data; } /** * Finds the file for a given class in a PSR-4 namespace. * * @param string $class_name The class to find. * * @return array|null $data The version and path path to the file if found, null otherwise. */ private function find_psr4_file( $class_name ) { if ( ! isset( $this->psr4_map ) ) { return null; } // Don't bother with classes that have no namespace. $class_index = strrpos( $class_name, '\\' ); if ( ! $class_index ) { return null; } $class_for_path = str_replace( '\\', '/', $class_name ); // Search for the namespace by iteratively cutting off the last segment until // we find a match. This allows us to check the most-specific namespaces // first as well as minimize the amount of time spent looking. for ( $class_namespace = substr( $class_name, 0, $class_index ); ! empty( $class_namespace ); $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) ) { $namespace = $class_namespace . '\\'; if ( ! isset( $this->psr4_map[ $namespace ] ) ) { continue; } $data = $this->psr4_map[ $namespace ]; foreach ( $data['path'] as $path ) { $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; if ( file_exists( $path ) ) { return array( 'version' => $data['version'], 'path' => $path, ); } } } return null; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-autoloader-locator.php 0000644 00000004120 15132754524 0024610 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class locates autoloaders. */ class Autoloader_Locator { /** * The object for comparing autoloader versions. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The version selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Finds the path to the plugin with the latest autoloader. * * @param array $plugin_paths An array of plugin paths. * @param string $latest_version The latest version reference. * * @return string|null */ public function find_latest_autoloader( $plugin_paths, &$latest_version ) { $latest_plugin = null; foreach ( $plugin_paths as $plugin_path ) { $version = $this->get_autoloader_version( $plugin_path ); if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { continue; } $latest_version = $version; $latest_plugin = $plugin_path; } return $latest_plugin; } /** * Gets the path to the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string */ public function get_autoloader_path( $plugin_path ) { return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; } /** * Gets the version for the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string|null */ public function get_autoloader_version( $plugin_path ) { $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; if ( ! file_exists( $classmap ) ) { return null; } $classmap = require $classmap; if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { return $classmap[ AutoloadGenerator::class ]['version']; } return null; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-shutdown-handler.php 0000644 00000005505 15132754524 0024306 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class handles the shutdown of the autoloader. */ class Shutdown_Handler { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The plugins cached by this autoloader. * * @var string[] */ private $cached_plugins; /** * Indicates whether or not this autoloader was included by another. * * @var bool */ private $was_included_by_autoloader; /** * Constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance to use. * @param string[] $cached_plugins The plugins cached by the autoloaer. * @param bool $was_included_by_autoloader Indicates whether or not the autoloader was included by another. */ public function __construct( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) { $this->plugins_handler = $plugins_handler; $this->cached_plugins = $cached_plugins; $this->was_included_by_autoloader = $was_included_by_autoloader; } /** * Handles the shutdown of the autoloader. */ public function __invoke() { // Don't save a broken cache if an error happens during some plugin's initialization. if ( ! did_action( 'plugins_loaded' ) ) { // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // Load the active plugins fresh since the list we pulled earlier might not contain // plugins that were activated but did not reset the autoloader. This happens // when a plugin is in the cache but not "active" when the autoloader loads. // We also want to make sure that plugins which are deactivating are not // considered "active" so that they will be removed from the cache now. try { $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); } catch ( \Exception $ex ) { // When the package is deleted before shutdown it will throw an exception. // In the event this happens we should erase the cache. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // The paths should be sorted for easy comparisons with those loaded from the cache. // Note we don't need to sort the cached entries because they're already sorted. sort( $active_plugins ); // We don't want to waste time saving a cache that hasn't changed. if ( $this->cached_plugins === $active_plugins ) { return; } $this->plugins_handler->cache_plugins( $active_plugins ); } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-manifest-reader.php 0000644 00000005175 15132754524 0024071 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class reads autoloader manifest files. */ class Manifest_Reader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Reads all of the manifests in the given plugin paths. * * @param array $plugin_paths The paths to the plugins we're loading the manifest in. * @param string $manifest_path The path that we're loading the manifest from in each plugin. * @param array $path_map The path map to add the contents of the manifests to. * * @return array $path_map The path map we've built using the manifests in each plugin. */ public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { $file_paths = array_map( function ( $path ) use ( $manifest_path ) { return trailingslashit( $path ) . $manifest_path; }, $plugin_paths ); foreach ( $file_paths as $path ) { $this->register_manifest( $path, $path_map ); } return $path_map; } /** * Registers a plugin's manifest file with the path map. * * @param string $manifest_path The absolute path to the manifest that we're loading. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_manifest( $manifest_path, &$path_map ) { if ( ! is_readable( $manifest_path ) ) { return; } $manifest = require $manifest_path; if ( ! is_array( $manifest ) ) { return; } foreach ( $manifest as $key => $data ) { $this->register_record( $key, $data, $path_map ); } } /** * Registers an entry from the manifest in the path map. * * @param string $key The identifier for the entry we're registering. * @param array $data The data for the entry we're registering. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_record( $key, $data, &$path_map ) { if ( isset( $path_map[ $key ]['version'] ) ) { $selected_version = $path_map[ $key ]['version']; } else { $selected_version = null; } if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { $path_map[ $key ] = array( 'version' => $data['version'], 'path' => $data['path'], ); } } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-path-processor.php 0000644 00000013061 15132754524 0023765 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class handles dealing with paths for the autoloader. */ class Path_Processor { /** * Given a path this will replace any of the path constants with a token to represent it. * * @param string $path The path we want to process. * * @return string The tokenized path. */ public function tokenize_path_constants( $path ) { $path = wp_normalize_path( $path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $len = strlen( $constant_path ); if ( substr( $path, 0, $len ) !== $constant_path ) { continue; } return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); } return $path; } /** * Given a path this will replace any of the path constant tokens with the expanded path. * * @param string $tokenized_path The path we want to process. * * @return string The expanded path. */ public function untokenize_path_constants( $tokenized_path ) { $tokenized_path = wp_normalize_path( $tokenized_path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $constant = '{{' . $constant . '}}'; $len = strlen( $constant ); if ( substr( $tokenized_path, 0, $len ) !== $constant ) { continue; } return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); } return $tokenized_path; } /** * Given a file and an array of places it might be, this will find the absolute path and return it. * * @param string $file The plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|false Returns the absolute path to the directory, otherwise false. */ public function find_directory_with_autoloader( $file, $directories_to_check ) { $file = wp_normalize_path( $file ); if ( ! $this->is_absolute_path( $file ) ) { $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); if ( ! isset( $file ) ) { return false; } } // We need the real path for consistency with __DIR__ paths. $file = $this->get_real_path( $file ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged $directory = @is_file( $file ) ? dirname( $file ) : $file; if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { return false; } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged return $directory; } /** * Fetches an array of normalized paths keyed by the constant they came from. * * @return string[] The normalized paths keyed by the constant. */ private function get_normalized_constants() { $raw_constants = array( // Order the constants from most-specific to least-specific. 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH', ); $constants = array(); foreach ( $raw_constants as $raw ) { if ( ! defined( $raw ) ) { continue; } $path = wp_normalize_path( constant( $raw ) ); if ( isset( $path ) ) { $constants[ $raw ] = $path; } } return $constants; } /** * Indicates whether or not a path is absolute. * * @param string $path The path to check. * * @return bool True if the path is absolute, otherwise false. */ private function is_absolute_path( $path ) { if ( 0 === strlen( $path ) || '.' === $path[0] ) { return false; } // Absolute paths on Windows may begin with a drive letter. if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { return true; } // A path starting with / or \ is absolute; anything else is relative. return ( '/' === $path[0] || '\\' === $path[0] ); } /** * Given a file and a list of directories to check, this method will try to figure out * the absolute path to the file in question. * * @param string $normalized_path The normalized path to the plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|null The absolute path to the plugin file, otherwise null. */ private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { // We're only able to find the absolute path for plugin/theme PHP files. if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { return null; } foreach ( $directories_to_check as $directory ) { $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_file( $normalized_check ) ) { return $normalized_check; } } return null; } /** * Given a path this will figure out the real path that we should be using. * * @param string $path The path to resolve. * * @return string The resolved path. */ private function get_real_path( $path ) { // We want to resolve symbolic links for consistency with __DIR__ paths. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $real_path = @realpath( $path ); if ( false === $real_path ) { // Let the autoloader deal with paths that don't exist. $real_path = $path; } // Using realpath will make it platform-specific so we must normalize it after. if ( $path !== $real_path ) { $real_path = wp_normalize_path( $real_path ); } return $real_path; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-hook-manager.php 0000644 00000004145 15132754524 0023367 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * Allows the latest autoloader to register hooks that can be removed when the autoloader is reset. */ class Hook_Manager { /** * An array containing all of the hooks that we've registered. * * @var array */ private $registered_hooks; /** * The constructor. */ public function __construct() { $this->registered_hooks = array(); } /** * Adds an action to WordPress and registers it internally. * * @param string $tag The name of the action which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the action. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_action( $tag, $callable, $priority, $accepted_args ); } /** * Adds a filter to WordPress and registers it internally. * * @param string $tag The name of the filter which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the filter. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_filter( $tag, $callable, $priority, $accepted_args ); } /** * Removes all of the registered hooks. */ public function reset() { foreach ( $this->registered_hooks as $tag => $hooks ) { foreach ( $hooks as $hook ) { remove_filter( $tag, $hook['callable'], $hook['priority'] ); } } $this->registered_hooks = array(); } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-version-selector.php 0000644 00000003467 15132754524 0024330 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * Used to select package versions. */ class Version_Selector { /** * Checks whether the selected package version should be updated. Composer development * package versions ('9999999-dev' or versions that start with 'dev-') are favored * when the JETPACK_AUTOLOAD_DEV constant is set to true. * * @param String $selected_version The currently selected package version. * @param String $compare_version The package version that is being evaluated to * determine if the version needs to be updated. * * @return bool Returns true if the selected package version should be updated, * else false. */ public function is_version_update_required( $selected_version, $compare_version ) { $use_dev_versions = defined( 'JETPACK_AUTOLOAD_DEV' ) && JETPACK_AUTOLOAD_DEV; if ( is_null( $selected_version ) ) { return true; } if ( $use_dev_versions && $this->is_dev_version( $selected_version ) ) { return false; } if ( $this->is_dev_version( $compare_version ) ) { if ( $use_dev_versions ) { return true; } else { return false; } } if ( version_compare( $selected_version, $compare_version, '<' ) ) { return true; } return false; } /** * Checks whether the given package version is a development version. * * @param String $version The package version. * * @return bool True if the version is a dev version, else false. */ public function is_dev_version( $version ) { if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { return true; } return false; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-plugin-locator.php 0000644 00000010476 15132754524 0023762 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore /** * This class scans the WordPress installation to find active plugins. */ class Plugin_Locator { /** * The path processor for finding plugin paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Path_Processor $path_processor The Path_Processor instance. */ public function __construct( $path_processor ) { $this->path_processor = $path_processor; } /** * Finds the path to the current plugin. * * @return string $path The path to the current plugin. * * @throws \RuntimeException If the current plugin does not have an autoloader. */ public function find_current_plugin() { // Escape from `vendor/__DIR__` to root plugin directory. $plugin_directory = dirname( dirname( __DIR__ ) ); // Use the path processor to ensure that this is an autoloader we're referencing. $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); if ( false === $path ) { throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); } return $path; } /** * Checks a given option for plugin paths. * * @param string $option_name The option that we want to check for plugin information. * @param bool $site_option Indicates whether or not we want to check the site option. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_option( $option_name, $site_option = false ) { $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); if ( false === $raw ) { return array(); } return $this->convert_plugins_to_paths( $raw ); } /** * Checks for plugins in the `action` request parameter. * * @param string[] $allowed_actions The actions that we're allowed to return plugins for. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_request_action( $allowed_actions ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended /** * Note: we're not actually checking the nonce here because it's too early * in the execution. The pluggable functions are not yet loaded to give * plugins a chance to plug their versions. Therefore we're doing the bare * minimum: checking whether the nonce exists and it's in the right place. * The request will fail later if the nonce doesn't pass the check. */ if ( empty( $_REQUEST['_wpnonce'] ) ) { return array(); } $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; if ( ! in_array( $action, $allowed_actions, true ) ) { return array(); } $plugin_slugs = array(); switch ( $action ) { case 'activate': case 'deactivate': if ( empty( $_REQUEST['plugin'] ) ) { break; } $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); break; case 'activate-selected': case 'deactivate-selected': if ( empty( $_REQUEST['checked'] ) ) { break; } $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); break; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $this->convert_plugins_to_paths( $plugin_slugs ); } /** * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter * out the plugins that are not directory plugins. Note that array keys will also be included * if they are plugin paths! * * @param string[] $plugins Plugin paths or slugs to filter. * * @return string[] */ private function convert_plugins_to_paths( $plugins ) { if ( ! is_array( $plugins ) || empty( $plugins ) ) { return array(); } // We're going to look for plugins in the standard directories. $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); $plugin_paths = array(); foreach ( $plugins as $key => $value ) { $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } } return $plugin_paths; } } packages/woocommerce-blocks/vendor/jetpack-autoloader/class-autoloader-handler.php 0000644 00000010767 15132754524 0024600 0 ustar 00 <?php /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader\jpb78ca910fe07339d6189615f1734a3e3; // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class selects the package version for the autoloader. */ class Autoloader_Handler { /** * The PHP_Autoloader instance. * * @var PHP_Autoloader */ private $php_autoloader; /** * The Hook_Manager instance. * * @var Hook_Manager */ private $hook_manager; /** * The Manifest_Reader instance. * * @var Manifest_Reader */ private $manifest_reader; /** * The Version_Selector instance. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param PHP_Autoloader $php_autoloader The PHP_Autoloader instance. * @param Hook_Manager $hook_manager The Hook_Manager instance. * @param Manifest_Reader $manifest_reader The Manifest_Reader instance. * @param Version_Selector $version_selector The Version_Selector instance. */ public function __construct( $php_autoloader, $hook_manager, $manifest_reader, $version_selector ) { $this->php_autoloader = $php_autoloader; $this->hook_manager = $hook_manager; $this->manifest_reader = $manifest_reader; $this->version_selector = $version_selector; } /** * Checks to see whether or not an autoloader is currently in the process of initializing. * * @return bool */ public function is_initializing() { // If no version has been set it means that no autoloader has started initializing yet. global $jetpack_autoloader_latest_version; if ( ! isset( $jetpack_autoloader_latest_version ) ) { return false; } // When the version is set but the classmap is not it ALWAYS means that this is the // latest autoloader and is being included by an older one. global $jetpack_packages_classmap; if ( empty( $jetpack_packages_classmap ) ) { return true; } // Version 2.4.0 added a new global and altered the reset semantics. We need to check // the other global as well since it may also point at initialization. // Note: We don't need to check for the class first because every autoloader that // will set the latest version global requires this class in the classmap. $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return true; } } return false; } /** * Activates an autoloader using the given plugins and activates it. * * @param string[] $plugins The plugins to initialize the autoloader for. */ public function activate_autoloader( $plugins ) { global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); $loader = new Version_Loader( $this->version_selector, $jetpack_packages_classmap, $jetpack_packages_psr4, $jetpack_packages_filemap ); $this->php_autoloader->register_autoloader( $loader ); // Now that the autoloader is active we can load the filemap. $loader->load_filemap(); } /** * Resets the active autoloader and all related global state. */ public function reset_autoloader() { $this->php_autoloader->unregister_autoloader(); $this->hook_manager->reset(); // Clear all of the autoloader globals so that older autoloaders don't do anything strange. global $jetpack_autoloader_latest_version; $jetpack_autoloader_latest_version = null; global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! } } packages/woocommerce-blocks/vendor/autoload.php 0000644 00000000262 15132754524 0015742 0 ustar 00 <?php // autoload.php @generated by Composer require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInitb78ca910fe07339d6189615f1734a3e3::getLoader(); packages/woocommerce-blocks/packages/checkout/style.scss 0000644 00000000030 15132754524 0017535 0 ustar 00 /* stylelint-disable */ packages/woocommerce-blocks/packages/checkout/totals/index.js 0000644 00000000304 15132754524 0020457 0 ustar 00 export { default as TotalsItem } from './item'; export { default as Subtotal } from './subtotal'; export { default as TotalsTaxes } from './taxes'; export { default as TotalsFees } from './fees'; packages/woocommerce-blocks/packages/checkout/totals/item/style.scss 0000644 00000000610 15132754524 0022005 0 ustar 00 .wc-block-components-totals-item { display: flex; flex-wrap: wrap; margin: em($gap-small) 0 0; width: 100%; &:first-child { margin-top: 0; } } .wc-block-components-totals-item__label { flex-grow: 1; } .wc-block-components-totals-item__value { font-weight: bold; white-space: nowrap; } .wc-block-components-totals-item__description { @include font-size(small); width: 100%; } packages/woocommerce-blocks/packages/checkout/totals/item/index.tsx 0000644 00000003044 15132754524 0021623 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; import { isValidElement } from '@wordpress/element'; import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; import type { ReactElement, ReactNode } from 'react'; import type { Currency } from '@woocommerce/price-format'; /** * Internal dependencies */ import './style.scss'; interface TotalsItemProps { className?: string; currency: Currency; label: string; // Value may be a number, or react node. Numbers are passed to FormattedMonetaryAmount. value: number | ReactNode; description?: ReactNode; } const TotalsItemValue = ( { value, currency, }: Partial< TotalsItemProps > ): ReactElement | null => { if ( isValidElement( value ) ) { return ( <div className="wc-block-components-totals-item__value"> { value } </div> ); } return Number.isFinite( value ) ? ( <FormattedMonetaryAmount className="wc-block-components-totals-item__value" currency={ currency || {} } value={ value as number } /> ) : null; }; const TotalsItem = ( { className, currency, label, value, description, }: TotalsItemProps ): ReactElement => { return ( <div className={ classnames( 'wc-block-components-totals-item', className ) } > <span className="wc-block-components-totals-item__label"> { label } </span> <TotalsItemValue value={ value } currency={ currency } /> <div className="wc-block-components-totals-item__description"> { description } </div> </div> ); }; export default TotalsItem; packages/woocommerce-blocks/packages/checkout/totals/item/stories/index.js 0000644 00000001253 15132754524 0023111 0 ustar 00 /** * External dependencies */ import { text } from '@storybook/addon-knobs'; import { currencyKnob } from '@woocommerce/knobs'; /** * Internal dependencies */ import TotalsItem from '../'; export default { title: 'WooCommerce Blocks/@blocks-checkout/TotalsItem', component: TotalsItem, }; export const Default = () => { const currency = currencyKnob(); const description = text( 'Description', 'This is the amount that will be charged to your card.' ); const label = text( 'Label', 'Amount' ); const value = text( 'Value', '1000' ); return ( <TotalsItem currency={ currency } description={ description } label={ label } value={ value } /> ); }; packages/woocommerce-blocks/packages/checkout/totals/subtotal/index.tsx 0000644 00000002032 15132754524 0022516 0 ustar 00 /** * External dependencies */ import { __ } from '@wordpress/i18n'; import type { Currency } from '@woocommerce/price-format'; import type { ReactElement } from 'react'; import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ import TotalsItem from '../item'; interface Values { total_items: string; total_items_tax: string; } interface SubtotalProps { className?: string; currency: Currency; values: Values | Record< string, never >; } const Subtotal = ( { currency, values, className, }: SubtotalProps ): ReactElement => { const { total_items: totalItems, total_items_tax: totalItemsTax } = values; const itemsValue = parseInt( totalItems, 10 ); const itemsTaxValue = parseInt( totalItemsTax, 10 ); return ( <TotalsItem className={ className } currency={ currency } label={ __( 'Subtotal', 'woo-gutenberg-products-block' ) } value={ getSetting( 'displayCartPricesIncludingTax', false ) ? itemsValue + itemsTaxValue : itemsValue } /> ); }; export default Subtotal; packages/woocommerce-blocks/packages/checkout/totals/subtotal/stories/index.js 0000644 00000001144 15132754524 0024007 0 ustar 00 /** * External dependencies */ import { text } from '@storybook/addon-knobs'; import { currencyKnob } from '@woocommerce/knobs'; /** * Internal dependencies */ import Subtotal from '../'; export default { title: 'WooCommerce Blocks/@blocks-checkout/Subtotal', component: Subtotal, }; export const Default = () => { const currency = currencyKnob(); const totalItems = text( 'Total items', '1000' ); const totalItemsTax = text( 'Total items tax', '200' ); return ( <Subtotal currency={ currency } values={ { total_items: totalItems, total_items_tax: totalItemsTax, } } /> ); }; packages/woocommerce-blocks/packages/checkout/totals/fees/index.tsx 0000644 00000002443 15132754524 0021611 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { getSetting } from '@woocommerce/settings'; import type { Currency } from '@woocommerce/price-format'; import type { CartFeeItem } from '@woocommerce/type-defs/cart'; import type { ReactElement } from 'react'; /** * Internal dependencies */ import TotalsItem from '../item'; interface TotalsFeesProps { currency: Currency; cartFees: CartFeeItem[]; className?: string; } const TotalsFees = ( { currency, cartFees, className, }: TotalsFeesProps ): ReactElement | null => { return ( <> { cartFees.map( ( { id, name, totals }, index ) => { const feesValue = parseInt( totals.total, 10 ); if ( ! feesValue ) { return null; } const feesTaxValue = parseInt( totals.total_tax, 10 ); return ( <TotalsItem key={ id || `${ index }-${ name }` } className={ classnames( 'wc-block-components-totals-fees', className ) } currency={ currency } label={ name || __( 'Fee', 'woo-gutenberg-products-block' ) } value={ getSetting( 'displayCartPricesIncludingTax', false ) ? feesValue + feesTaxValue : feesValue } /> ); } ) } </> ); }; export default TotalsFees; packages/woocommerce-blocks/packages/checkout/totals/fees/stories/index.js 0000644 00000001244 15132754524 0023075 0 ustar 00 /** * External dependencies */ import { text } from '@storybook/addon-knobs'; import { currencyKnob } from '@woocommerce/knobs'; /** * Internal dependencies */ import TotalsFees from '../'; export default { title: 'WooCommerce Blocks/@blocks-checkout/TotalsFees', component: TotalsFees, }; export const Default = () => { const currency = currencyKnob(); const totalFees = text( 'Total fee', '1000' ); const totalFeesTax = text( 'Total fee tax', '200' ); return ( <TotalsFees currency={ currency } cartFees={ [ { id: 'fee', name: 'Fee', totals: { total: totalFees, total_tax: totalFeesTax, }, }, ] } /> ); }; packages/woocommerce-blocks/packages/checkout/totals/taxes/style.scss 0000644 00000000273 15132754524 0022200 0 ustar 00 .wc-block-components-totals-item.wc-block-components-totals-taxes__grouped-rate { margin: $gap-smallest 0; &:first-child { margin-top: 0; } &:last-child { margin-bottom: 0; } } packages/woocommerce-blocks/packages/checkout/totals/taxes/stories/index.js 0000644 00000001021 15132754524 0023270 0 ustar 00 /** * External dependencies */ import { text } from '@storybook/addon-knobs'; import { currencyKnob } from '@woocommerce/knobs'; /** * Internal dependencies */ import TotalsTaxes from '../'; export default { title: 'WooCommerce Blocks/@blocks-checkout/TotalsTaxes', component: TotalsTaxes, }; export const Default = () => { const currency = currencyKnob(); const totalTaxes = text( 'Total taxes', '1000' ); return ( <TotalsTaxes currency={ currency } values={ { total_tax: totalTaxes, } } /> ); }; packages/woocommerce-blocks/packages/checkout/totals/taxes/index.tsx 0000644 00000003744 15132754524 0022020 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { getSetting } from '@woocommerce/settings'; import type { Currency } from '@woocommerce/price-format'; import type { CartTotalsTaxLineItem } from '@woocommerce/type-defs/cart'; import { ReactElement } from 'react'; /** * Internal dependencies */ import TotalsItem from '../item'; import './style.scss'; interface Values { tax_lines: CartTotalsTaxLineItem[]; total_tax: string; } interface TotalsTaxesProps { className?: string; currency: Currency; showRateAfterTaxName: boolean; values: Values | Record< string, never >; } const TotalsTaxes = ( { currency, values, className, showRateAfterTaxName, }: TotalsTaxesProps ): ReactElement | null => { const { total_tax: totalTax, tax_lines: taxLines } = values; if ( ! getSetting( 'taxesEnabled', true ) && parseInt( totalTax, 10 ) <= 0 ) { return null; } const showItemisedTaxes = getSetting( 'displayItemizedTaxes', false ) as boolean; const itemisedTaxItems: ReactElement | null = showItemisedTaxes && taxLines.length > 0 ? ( <div className={ classnames( 'wc-block-components-totals-taxes', className ) } > { taxLines.map( ( { name, rate, price }, i ) => { const label = `${ name }${ showRateAfterTaxName ? ` ${ rate }` : '' }`; return ( <TotalsItem key={ `tax-line-${ i }` } className="wc-block-components-totals-taxes__grouped-rate" currency={ currency } label={ label } value={ parseInt( price, 10 ) } /> ); } ) }{ ' ' } </div> ) : null; return showItemisedTaxes ? ( itemisedTaxItems ) : ( <> <TotalsItem className={ classnames( 'wc-block-components-totals-taxes', className ) } currency={ currency } label={ __( 'Taxes', 'woo-gutenberg-products-block' ) } value={ parseInt( totalTax, 10 ) } description={ null } /> </> ); }; export default TotalsTaxes; packages/woocommerce-blocks/packages/checkout/order-shipping-packages/index.js 0000644 00000001721 15132754524 0023663 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; /** * Internal dependencies */ import { createSlotFill, useSlot } from '../slot'; const slotName = '__experimentalOrderShippingPackages'; const { Fill: ExperimentalOrderShippingPackages, Slot: OrderShippingPackagesSlot, } = createSlotFill( slotName ); const Slot = ( { className, collapsible, noResultsMessage, renderOption, extensions, cart, components, } ) => { const { fills } = useSlot( slotName ); const hasMultiplePackages = fills.length > 1; return ( <OrderShippingPackagesSlot className={ classnames( 'wc-block-components-shipping-rates-control', className ) } fillProps={ { collapsible, collapse: hasMultiplePackages, showItems: hasMultiplePackages, noResultsMessage, renderOption, extensions, cart, components, } } /> ); }; ExperimentalOrderShippingPackages.Slot = Slot; export default ExperimentalOrderShippingPackages; packages/woocommerce-blocks/packages/checkout/discounts-meta/index.js 0000644 00000001461 15132754524 0022115 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; /** * Internal dependencies */ import { createSlotFill, hasValidFills, useSlot } from '../slot'; import TotalsWrapper from '../wrapper'; const slotName = '__experimentalDiscountsMeta'; const { Fill: ExperimentalDiscountsMeta, Slot: DiscountsMetaSlot, } = createSlotFill( slotName ); const Slot = ( { className, extensions, cart } ) => { const { fills } = useSlot( slotName ); return ( hasValidFills( fills ) && ( <TotalsWrapper slotWrapper={ true }> <DiscountsMetaSlot className={ classnames( className, 'wc-block-components-discounts-meta' ) } fillProps={ { extensions, cart } } /> </TotalsWrapper> ) ); }; ExperimentalDiscountsMeta.Slot = Slot; export default ExperimentalDiscountsMeta; packages/woocommerce-blocks/packages/checkout/panel/style.scss 0000644 00000003156 15132754524 0020650 0 ustar 00 .wc-block-components-panel.has-border { @include with-translucent-border(1px 0); + .wc-block-components-panel.has-border::after { border-top-width: 0; } } .wc-block-components-panel.has-border.no-top-border { @include with-translucent-border(1px 0); &::after { border-top-width: 0; } } .wc-block-components-panel__button { @include reset-box(); height: auto; line-height: 1; margin-top: em(6px); padding-right: #{24px + $gap-smaller}; padding-top: em($gap-small - 6px); position: relative; text-align: left; width: 100%; word-break: break-word; &[aria-expanded="true"] { padding-bottom: em($gap-small - 6px); margin-bottom: em(6px); } &, &:hover, &:focus, &:active { @include reset-typography(); background: transparent; box-shadow: none; } > .wc-block-components-panel__button-icon { fill: currentColor; position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: auto; } } .wc-block-components-panel__content { padding-bottom: em($gap); // Ensures the panel contents are not visible for any theme that tweaked the // `display` property of div elements. &[hidden] { display: none; } } // Extra classes for specificity. .theme-twentytwentyone.theme-twentytwentyone.theme-twentytwentyone .wc-block-components-panel__button { background-color: inherit; color: inherit; } .theme-twentytwenty .wc-block-components-panel__button, .theme-twentyseventeen .wc-block-components-panel__button { background: none transparent; color: inherit; &.wc-block-components-panel__button:hover, &.wc-block-components-panel__button:focus { background: none transparent; } } packages/woocommerce-blocks/packages/checkout/panel/index.tsx 0000644 00000002462 15132754524 0020461 0 ustar 00 /** * External dependencies */ import { useState } from '@wordpress/element'; import type { ReactNode, ReactElement } from 'react'; import classNames from 'classnames'; import { Icon, chevronUp, chevronDown } from '@woocommerce/icons'; /** * Internal dependencies */ import './style.scss'; interface PanelProps { children?: ReactNode; className?: string; initialOpen?: boolean; hasBorder?: boolean; title?: ReactNode; titleTag?: keyof JSX.IntrinsicElements; } const Panel = ( { children, className, initialOpen = false, hasBorder = false, title, titleTag: TitleTag = 'div', }: PanelProps ): ReactElement => { const [ isOpen, setIsOpen ] = useState< boolean >( initialOpen ); return ( <div className={ classNames( className, 'wc-block-components-panel', { 'has-border': hasBorder, } ) } > <TitleTag> <button aria-expanded={ isOpen } className="wc-block-components-panel__button" onClick={ () => setIsOpen( ! isOpen ) } > <Icon aria-hidden="true" className="wc-block-components-panel__button-icon" srcElement={ isOpen ? chevronUp : chevronDown } /> { title } </button> </TitleTag> { isOpen && ( <div className="wc-block-components-panel__content"> { children } </div> ) } </div> ); }; export default Panel; packages/woocommerce-blocks/packages/checkout/wrapper/style.scss 0000644 00000000770 15132754524 0021230 0 ustar 00 .wc-block-components-totals-wrapper { @include with-translucent-border(1px 0 0); padding: $gap 0; &.has-bottom-border { &::after { border-bottom-width: 1px; } } &.slot-wrapper { padding: 0; > * > * { @include with-translucent-border(0 0 1px); padding: $gap 0; &:last-child::after { border-bottom-width: 0; } } } } .wc-block-components-discounts-meta { .wc-block-components-totals-wrapper { &:first-child { @include with-translucent-border(1px 0 0); } } } packages/woocommerce-blocks/packages/checkout/wrapper/index.tsx 0000644 00000001132 15132754524 0021033 0 ustar 00 /** * External dependencies */ import { Children, ReactNode } from 'react'; /** * Internal dependencies */ import './style.scss'; interface TotalsWrapperProps { children: ReactNode; /* If this TotalsWrapper is being used to wrap a Slot */ slotWrapper?: boolean; } const TotalsWrapper = ( { children, slotWrapper = false, }: TotalsWrapperProps ): JSX.Element | null => { return Children.count( children ) ? ( <div className={ `wc-block-components-totals-wrapper${ slotWrapper ? ' slot-wrapper' : '' }` } > { children } </div> ) : null; }; export default TotalsWrapper; packages/woocommerce-blocks/packages/checkout/button/index.ts 0000644 00000000163 15132754524 0020501 0 ustar 00 /** * External dependencies */ import Button from '@woocommerce/base-components/button'; export default Button; packages/woocommerce-blocks/packages/checkout/error-boundary/index.js 0000644 00000001540 15132754524 0022126 0 ustar 00 /** * External dependencies */ import { Component } from 'react'; class CheckoutSlotErrorBoundary extends Component { state = { errorMessage: '', hasError: false }; static getDerivedStateFromError( error ) { if ( typeof error.statusText !== 'undefined' && typeof error.status !== 'undefined' ) { return { errorMessage: ( <> <strong>{ error.status }</strong> { ': ' + error.statusText } </> ), hasError: true, }; } return { errorMessage: error.message, hasError: true }; } render() { const { renderError } = this.props; const { errorMessage, hasError } = this.state; if ( hasError ) { if ( typeof renderError === 'function' ) { return renderError( errorMessage ); } return <p>{ errorMessage }</p>; } return this.props.children; } } export default CheckoutSlotErrorBoundary; packages/woocommerce-blocks/packages/checkout/blocks-registry/utils.ts 0000644 00000006543 15132754524 0022352 0 ustar 00 /** * External dependencies */ import { isObject } from '@woocommerce/types'; /** * Internal dependencies */ import { hasInnerBlocks } from './get-registered-blocks'; /** * Asserts that an option is of the given type. Otherwise, throws an error. * * @throws Will throw an error if the type of the option doesn't match the expected type. */ export const assertType = ( optionName: string, option: unknown, expectedType: unknown ): void => { const actualType = typeof option; if ( actualType !== expectedType ) { throw new Error( `Incorrect value for the ${ optionName } argument when registering a checkout block. It was a ${ actualType }, but must be a ${ expectedType }.` ); } }; /** * Validate the block name. * * @throws Will throw an error if the block name is invalid. */ export const assertBlockName = ( blockName: string ): void => { assertType( 'blockName', blockName, 'string' ); if ( ! blockName ) { throw new Error( `Value for the blockName argument must not be empty.` ); } }; /** * Validate the block parent. * * @throws Will throw an error if the block name is invalid. */ export const assertBlockParent = ( blockParent: string | string[] ): void => { if ( typeof blockParent !== 'string' && ! Array.isArray( blockParent ) ) { throw new Error( `Incorrect value for the parent argument when registering a checkout block. It was a ${ typeof blockParent }, but must be a string or array of strings.` ); } if ( typeof blockParent === 'string' && ! hasInnerBlocks( blockParent ) ) { throw new Error( `When registering a checkout block, the parent must be a valid inner block area.` ); } if ( Array.isArray( blockParent ) && ! blockParent.some( ( parent ) => hasInnerBlocks( parent ) ) ) { throw new Error( `When registering a checkout block, the parent must be a valid inner block area.` ); } }; /** * Asserts that an option is of the given type. Otherwise, throws an error. * * @throws Will throw an error if the type of the option doesn't match the expected type. * @param {Object} options Object containing the option to validate. * @param {string} optionName Name of the option to validate. * @param {string} expectedType Type expected for the option. */ export const assertOption = ( options: unknown, optionName: string, expectedType: string ): void => { if ( ! isObject( options ) ) { return; } const actualType = typeof options[ optionName ]; if ( actualType !== expectedType ) { throw new Error( `Incorrect value for the ${ optionName } argument when registering a block component. It was a ${ actualType }, but must be a ${ expectedType }.` ); } }; /** * Asserts that an option is a valid react element or lazy callback. Otherwise, throws an error. * * @throws Will throw an error if the type of the option doesn't match the expected type. */ export const assertBlockComponent = ( options: Record< string, unknown >, optionName: string ): void => { const optionValue = options[ optionName ]; if ( optionValue ) { if ( typeof optionValue === 'function' ) { return; } if ( isObject( optionValue ) && optionValue.$$typeof && optionValue.$$typeof === Symbol.for( 'react.lazy' ) ) { return; } } throw new Error( `Incorrect value for the ${ optionName } argument when registering a block component. Component must be a valid React Element or Lazy callback.` ); }; packages/woocommerce-blocks/packages/checkout/blocks-registry/register-checkout-block.ts 0000644 00000002210 15132754524 0025714 0 ustar 00 /** * External dependencies */ import { registerBlockComponent } from '@woocommerce/blocks-registry'; /** * Internal dependencies */ import type { CheckoutBlockOptions } from './types'; import { assertBlockName, assertBlockParent, assertOption, assertBlockComponent, } from './utils'; import { registeredBlocks } from './registered-blocks'; /** * Main API for registering a new checkout block within areas. */ export const registerCheckoutBlock = ( options: CheckoutBlockOptions ): void => { assertOption( options, 'metadata', 'object' ); assertBlockName( options.metadata.name ); assertBlockParent( options.metadata.parent ); assertBlockComponent( options, 'component' ); /** * This ensures the frontend component for the checkout block is available. */ registerBlockComponent( { blockName: options.metadata.name as string, component: options.component, } ); /** * Store block metadata for later lookup. */ registeredBlocks[ options.metadata.name ] = { blockName: options.metadata.name, metadata: options.metadata, component: options.component, force: !! options.metadata?.attributes?.lock?.default?.remove, }; }; packages/woocommerce-blocks/packages/checkout/blocks-registry/test/index.js 0000644 00000004072 15132754524 0023261 0 ustar 00 /** * Internal dependencies */ import { getRegisteredBlocks, registerCheckoutBlock, innerBlockAreas, } from '../index'; describe( 'checkout blocks registry', () => { const component = () => { return null; }; describe( 'registerCheckoutBlock', () => { const invokeTest = ( blockName, options ) => () => { return registerCheckoutBlock( blockName, options ); }; it( 'throws an error when registered block is missing `blockName`', () => { expect( invokeTest( { metadata: { name: null, parent: innerBlockAreas.CHECKOUT_FIELDS, }, component, } ) ).toThrowError( /blockName/ ); expect( invokeTest( { metadata: { name: '', parent: innerBlockAreas.CHECKOUT_FIELDS, }, component, } ) ).toThrowError( /blockName/ ); } ); it( 'throws an error when registered block is missing a valid parent', () => { expect( invokeTest( { metadata: { name: 'test/block-name', parent: [], }, component, } ) ).toThrowError( /parent/ ); expect( invokeTest( { metadata: { name: 'test/block-name', parent: 'invalid-parent', }, component, } ) ).toThrowError( /parent/ ); expect( invokeTest( { metadata: { name: 'test/block-name', parent: [ 'invalid-parent', innerBlockAreas.CHECKOUT_FIELDS, ], }, component, } ) ).not.toThrowError( /parent/ ); } ); it( 'throws an error when registered block is missing `component`', () => { expect( invokeTest( { metadata: { name: 'test/block-name', parent: innerBlockAreas.CHECKOUT_FIELDS, }, } ) ).toThrowError( /component/ ); } ); } ); describe( 'getRegisteredBlocks', () => { it( 'gets an empty array when checkout area has no registered blocks', () => { expect( getRegisteredBlocks( innerBlockAreas.CONTACT_INFORMATION ) ).toEqual( [] ); } ); it( 'gets an empty array when the area is not defined', () => { expect( getRegisteredBlocks( 'not-defined' ) ).toEqual( [] ); } ); } ); } ); packages/woocommerce-blocks/packages/checkout/blocks-registry/types.ts 0000644 00000002716 15132754524 0022354 0 ustar 00 /** * External dependencies */ import type { LazyExoticComponent } from 'react'; import type { BlockConfiguration } from '@wordpress/blocks'; export enum innerBlockAreas { CHECKOUT = 'woocommerce/checkout', CHECKOUT_FIELDS = 'woocommerce/checkout-fields-block', CHECKOUT_TOTALS = 'woocommerce/checkout-totals-block', CONTACT_INFORMATION = 'woocommerce/checkout-contact-information-block', SHIPPING_ADDRESS = 'woocommerce/checkout-shipping-address-block', BILLING_ADDRESS = 'woocommerce/checkout-billing-address-block', SHIPPING_METHODS = 'woocommerce/checkout-shipping-methods-block', PAYMENT_METHODS = 'woocommerce/checkout-payment-methods-block', CART = 'woocommerce/cart-i2', EMPTY_CART = 'woocommerce/empty-cart-block', FILLED_CART = 'woocommerce/filled-cart-block', CART_ITEMS = 'woocommerce/cart-items-block', CART_TOTALS = 'woocommerce/cart-totals-block', } interface CheckoutBlockOptionsMetadata extends Partial< BlockConfiguration > { name: string; parent: string[]; } export type RegisteredBlock = { blockName: string; metadata: CheckoutBlockOptionsMetadata; component: | LazyExoticComponent< React.ComponentType< unknown > > | ( () => JSX.Element | null ) | null; force: boolean; }; export type RegisteredBlocks = Record< string, RegisteredBlock >; export type CheckoutBlockOptions = { metadata: CheckoutBlockOptionsMetadata; component: | LazyExoticComponent< React.ComponentType< unknown > > | ( () => JSX.Element | null ) | null; }; packages/woocommerce-blocks/packages/checkout/blocks-registry/registered-blocks.ts 0000644 00000000240 15132754524 0024606 0 ustar 00 /** * Internal dependencies */ import type { RegisteredBlocks } from './types'; const registeredBlocks: RegisteredBlocks = {}; export { registeredBlocks }; packages/woocommerce-blocks/packages/checkout/blocks-registry/get-registered-blocks.ts 0000644 00000001226 15132754524 0025370 0 ustar 00 /** * Internal dependencies */ import { innerBlockAreas, RegisteredBlock } from './types'; import { registeredBlocks } from './registered-blocks'; /** * Check area is valid. */ export const hasInnerBlocks = ( block: string ): block is innerBlockAreas => { return Object.values( innerBlockAreas ).includes( block as innerBlockAreas ); }; /** * Get a list of blocks available within a specific area. */ export const getRegisteredBlocks = ( block: string ): Array< RegisteredBlock > => { return hasInnerBlocks( block ) ? Object.values( registeredBlocks ).filter( ( { metadata } ) => ( metadata?.parent || [] ).includes( block ) ) : []; }; packages/woocommerce-blocks/packages/checkout/blocks-registry/index.ts 0000644 00000000155 15132754524 0022312 0 ustar 00 export * from './get-registered-blocks'; export * from './register-checkout-block'; export * from './types'; packages/woocommerce-blocks/packages/checkout/order-meta/index.js 0000644 00000001423 15132754524 0021213 0 ustar 00 /** * External dependencies */ import classnames from 'classnames'; /** * Internal dependencies */ import { createSlotFill, hasValidFills, useSlot } from '../slot'; import TotalsWrapper from '../wrapper'; const slotName = '__experimentalOrderMeta'; const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill( slotName ); const Slot = ( { className, extensions, cart } ) => { const { fills } = useSlot( slotName ); return ( hasValidFills( fills ) && ( <TotalsWrapper slotWrapper={ true }> <OrderMetaSlot className={ classnames( className, 'wc-block-components-order-meta' ) } fillProps={ { extensions, cart } } /> </TotalsWrapper> ) ); }; ExperimentalOrderMeta.Slot = Slot; export default ExperimentalOrderMeta; packages/woocommerce-blocks/packages/checkout/slot/index.js 0000644 00000007043 15132754524 0020141 0 ustar 00 /** * External dependencies */ import deprecated from '@wordpress/deprecated'; import { createSlotFill as baseCreateSlotFill, __experimentalUseSlot, useSlot as __useSlot, } from 'wordpress-components'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import { Children, cloneElement } from '@wordpress/element'; /** * Internal dependencies */ import BlockErrorBoundary from '../error-boundary'; /** * This function is used in case __experimentalUseSlot is removed and useSlot is not released, it tries to mock * the return value of that slot. * * @return {Object} The hook mocked return, currently: * fills, a null array of length 2. */ const mockedUseSlot = () => { /** * If we're here, it means useSlot was never graduated and __experimentalUseSlot is removed, so we should change our code. * */ deprecated( '__experimentalUseSlot', { plugin: 'woocommerce-gutenberg-products-block', } ); // We're going to mock its value return { fills: new Array( 2 ), }; }; /** * Checks if this slot has any valid fills. A valid fill is one that isn't falsy. * * @param {Array} fills The list of fills to check for a valid one in. * @return {boolean} True if this slot contains any valid fills. */ export const hasValidFills = ( fills ) => Array.isArray( fills ) && fills.filter( Boolean ).length > 0; /** * A hook that is used inside a slotFillProvider to return information on the a slot. * * @param {string} slotName The slot name to be hooked into. * @return {Object} slot data. */ let useSlot; if ( typeof __useSlot === 'function' ) { useSlot = __useSlot; } else if ( typeof __experimentalUseSlot === 'function' ) { useSlot = __experimentalUseSlot; } else { useSlot = mockedUseSlot; } export { useSlot }; /** * Abstracts @wordpress/components createSlotFill, wraps Fill in an error boundary and passes down fillProps. * * @param {string} slotName The generated slotName, based down to createSlotFill. * @param {null|function(Element):Element} [onError] Returns an element to display the error if the current use is an admin. * * @return {Object} Returns a newly wrapped Fill and Slot. */ export const createSlotFill = ( slotName, onError = null ) => { const { Fill: BaseFill, Slot: BaseSlot } = baseCreateSlotFill( slotName ); /** * A Fill that will get rendered inside associate slot. * If the code inside has a error, it would be caught ad removed. * The error is only visible to admins. * * @param {Object} props Items props. * @param {Array} props.children Children to be rendered. */ const Fill = ( { children } ) => ( <BaseFill> { ( fillProps ) => Children.map( children, ( fill ) => ( <BlockErrorBoundary /* Returning null would trigger the default error display. * Returning () => null would render nothing. */ renderError={ CURRENT_USER_IS_ADMIN ? onError : () => null } > { cloneElement( fill, fillProps ) } </BlockErrorBoundary> ) ) } </BaseFill> ); /** * A Slot that will get rendered inside our tree. * This forces Slot to use the Portal implementation that allows events to be bubbled to react tree instead of dom tree. * * @param {Object} [props] Slot props. * @param {string} props.className Class name to be used on slot. * @param {Object} props.fillProps Props to be passed to fills. * @param {Element|string} props.as Element used to render the slot, defaults to div. * */ const Slot = ( props ) => <BaseSlot { ...props } bubblesVirtually />; return { Fill, Slot, }; }; packages/woocommerce-blocks/packages/checkout/index.js 0000644 00000001143 15132754524 0017153 0 ustar 00 export * from './totals'; export * from './utils'; export * from './slot'; export * from './registry'; export { default as TotalsWrapper } from './wrapper'; export * from './blocks-registry'; export { default as ExperimentalOrderMeta } from './order-meta'; export { default as ExperimentalDiscountsMeta } from './discounts-meta'; export { default as ExperimentalOrderShippingPackages } from './order-shipping-packages'; export { default as Panel } from './panel'; export { SlotFillProvider } from 'wordpress-components'; export { default as Button } from './button'; export { default as Label } from './label'; packages/woocommerce-blocks/packages/checkout/label/index.ts 0000644 00000000160 15132754524 0020242 0 ustar 00 /** * External dependencies */ import Label from '@woocommerce/base-components/label'; export default Label; packages/woocommerce-blocks/packages/checkout/registry/index.ts 0000644 00000007667 15132754524 0021056 0 ustar 00 /** * External dependencies */ import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import { returnTrue } from '../utils'; type CheckoutFilterFunction = < T >( value: T, extensions: Record< string, unknown >, args?: CheckoutFilterArguments ) => T; type CheckoutFilterArguments = | ( Record< string, unknown > & { context?: string; } ) | null; let checkoutFilters: Record< string, Record< string, CheckoutFilterFunction > > = {}; /** * Register filters for a specific extension. */ export const __experimentalRegisterCheckoutFilters = ( namespace: string, filters: Record< string, CheckoutFilterFunction > ): void => { /** * Let developers know snackbarNotices is no longer available as a filter. * * See: https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4417 */ if ( Object.keys( filters ).includes( 'couponName' ) ) { deprecated( 'snackbarNotices', { alternative: 'snackbarNoticeVisibility', plugin: 'WooCommerce Blocks', link: 'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4417', } ); } /** * Let the user know couponName is no longer available as a filter. * * See https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4312 */ if ( Object.keys( filters ).includes( 'couponName' ) ) { deprecated( 'couponName', { alternative: 'coupons', plugin: 'WooCommerce Blocks', link: 'https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/bb921d21f42e21f38df2b1c87b48e07aa4cb0538/docs/extensibility/available-filters.md#coupons', } ); } checkoutFilters = { ...checkoutFilters, [ namespace ]: filters, }; }; /** * Get all filters with a specific name. * * @param {string} filterName Name of the filter to search for. * @return {Function[]} Array of functions that are registered for that filter * name. */ const getCheckoutFilters = ( filterName: string ): CheckoutFilterFunction[] => { const namespaces = Object.keys( checkoutFilters ); const filters = namespaces .map( ( namespace ) => checkoutFilters[ namespace ][ filterName ] ) .filter( Boolean ); return filters; }; /** * Apply a filter. */ export const __experimentalApplyCheckoutFilter = < T >( { filterName, defaultValue, extensions = null, arg = null, validation = returnTrue, }: { /** Name of the filter to apply. */ filterName: string; /** Default value to filter. */ defaultValue: T; /** Values extend to REST API response. */ extensions?: Record< string, unknown > | null; /** Object containing arguments for the filter function. */ arg?: CheckoutFilterArguments; /** Function that needs to return true when the filtered value is passed in order for the filter to be applied. */ validation?: ( value: T ) => true | Error; } ): T => { return useMemo( () => { const filters = getCheckoutFilters( filterName ); let value = defaultValue; filters.forEach( ( filter ) => { try { const newValue = filter( value, extensions || {}, arg ); if ( typeof newValue !== typeof value ) { throw new Error( sprintf( /* translators: %1$s is the type of the variable passed to the filter function, %2$s is the type of the value returned by the filter function. */ __( 'The type returned by checkout filters must be the same as the type they receive. The function received %1$s but returned %2$s.', 'woo-gutenberg-products-block' ), typeof value, typeof newValue ) ); } value = validation( newValue ) ? newValue : value; } catch ( e ) { if ( CURRENT_USER_IS_ADMIN ) { throw e; } else { // eslint-disable-next-line no-console console.error( e ); } } } ); return value; }, [ filterName, defaultValue, extensions, arg, validation ] ); }; packages/woocommerce-blocks/packages/checkout/registry/test/index.js 0000644 00000004240 15132754524 0022003 0 ustar 00 /** * External dependencies */ import { renderHook } from '@testing-library/react-hooks'; /** * Internal dependencies */ import { __experimentalRegisterCheckoutFilters, __experimentalApplyCheckoutFilter, } from '../'; describe( 'Checkout registry', () => { const filterName = 'loremIpsum'; test( 'should return default value if there are no filters', () => { const value = 'Hello World'; const { result: newValue } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, } ) ); expect( newValue.current ).toBe( value ); } ); test( 'should return filtered value when a filter is registered', () => { const value = 'Hello World'; __experimentalRegisterCheckoutFilters( filterName, { [ filterName ]: ( val, extensions, args ) => val.toUpperCase() + args.punctuationSign, } ); const { result: newValue } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, arg: { punctuationSign: '!', }, } ) ); expect( newValue.current ).toBe( 'HELLO WORLD!' ); } ); test( 'should not return filtered value if validation failed', () => { const value = 'Hello World'; __experimentalRegisterCheckoutFilters( filterName, { [ filterName ]: ( val ) => val.toUpperCase(), } ); const { result: newValue } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, validation: ( val ) => ! val.includes( 'HELLO' ), } ) ); expect( newValue.current ).toBe( value ); } ); test( 'should catch filter errors if user is not an admin', () => { const spy = {}; spy.console = jest .spyOn( console, 'error' ) .mockImplementation( () => {} ); const error = new Error( 'test error' ); const value = 'Hello World'; __experimentalRegisterCheckoutFilters( filterName, { [ filterName ]: () => { throw error; }, } ); const { result: newValue } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, } ) ); expect( spy.console ).toHaveBeenCalledWith( error ); expect( newValue.current ).toBe( value ); spy.console.mockRestore(); } ); } ); packages/woocommerce-blocks/packages/checkout/registry/test/admin.js 0000644 00000002767 15132754524 0022000 0 ustar 00 /** * External dependencies */ import { renderHook } from '@testing-library/react-hooks'; /** * Internal dependencies */ import { __experimentalRegisterCheckoutFilters, __experimentalApplyCheckoutFilter, } from '../'; jest.mock( '@woocommerce/settings', () => { const originalModule = jest.requireActual( '@woocommerce/settings' ); return { // @ts-ignore We know @woocommerce/settings is an object. ...originalModule, CURRENT_USER_IS_ADMIN: true, }; } ); describe( 'Checkout registry (as admin user)', () => { test( 'should throw if the filter throws and user is an admin', () => { const filterName = 'ErrorTestFilter'; const value = 'Hello World'; __experimentalRegisterCheckoutFilters( filterName, { [ filterName ]: () => { throw new Error( 'test error' ); }, } ); const { result } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, } ) ); expect( result.error ).toEqual( Error( 'test error' ) ); } ); test( 'should throw if validation throws and user is an admin', () => { const filterName = 'ValidationTestFilter'; const value = 'Hello World'; __experimentalRegisterCheckoutFilters( filterName, { [ filterName ]: ( val ) => val, } ); const { result } = renderHook( () => __experimentalApplyCheckoutFilter( { filterName, defaultValue: value, validation: () => { throw Error( 'validation error' ); }, } ) ); expect( result.error ).toEqual( Error( 'validation error' ) ); } ); } ); packages/woocommerce-blocks/packages/checkout/utils/extension-cart-update.ts 0000644 00000001451 15132754524 0023443 0 ustar 00 /** * External dependencies */ import { dispatch } from '@wordpress/data'; import { CartResponse } from '@woocommerce/type-defs/cart-response'; import { ExtensionCartUpdateArgs } from '@woocommerce/types'; /** * Internal dependencies */ import { STORE_KEY } from '../../../assets/js/data/cart/constants'; /** * When executed, this will call the cart/extensions endpoint. * The args contains a namespace, so if that extension has registered an update * callback, it will be executed server-side and the new cart will be returned. * The new cart is then received into the client-side store. */ export const extensionCartUpdate = ( args: ExtensionCartUpdateArgs ): Promise< CartResponse > => { const { applyExtensionCartUpdate } = dispatch( STORE_KEY ); return applyExtensionCartUpdate( args ); }; packages/woocommerce-blocks/packages/checkout/utils/index.js 0000644 00000000135 15132754524 0020313 0 ustar 00 export * from './validation'; export { extensionCartUpdate } from './extension-cart-update'; packages/woocommerce-blocks/packages/checkout/utils/validation/index.ts 0000644 00000001324 15132754524 0022460 0 ustar 00 /** * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; /** * Checks if value passed contain passed label. */ export const mustContain = ( value: string, label: string ): true | Error => { if ( ! value.includes( label ) ) { throw Error( sprintf( /* translators: %1$s value passed to filter, %2$s : value that must be included. */ __( 'Returned value must include %1$s, you passed "%2$s"', 'woo-gutenberg-products-block' ), value, label ) ); } return true; }; /** * A function that always return true. * We need to have a single instance of this function so it doesn't * invalidate our memo comparison. */ export const returnTrue = (): true => true; packages/woocommerce-blocks/packages/prices/utils/index.js 0000644 00000000031 15132754524 0017766 0 ustar 00 export * from './price'; packages/woocommerce-blocks/packages/prices/utils/price.ts 0000644 00000006747 15132754524 0020017 0 ustar 00 /** * External dependencies */ import { CURRENCY } from '@woocommerce/settings'; import type { Currency, CurrencyResponse, CartShippingPackageShippingRate, SymbolPosition, } from '@woocommerce/types'; /** * Get currency prefix. */ const getPrefix = ( // Currency symbol. symbol: string, // Position of currency symbol from settings. symbolPosition: SymbolPosition ): string => { const prefixes = { left: symbol, left_space: ' ' + symbol, right: '', right_space: '', }; return prefixes[ symbolPosition ] || ''; }; /** * Get currency suffix. */ const getSuffix = ( // Currency symbol. symbol: string, // Position of currency symbol from settings. symbolPosition: SymbolPosition ): string => { const suffixes = { left: '', left_space: '', right: symbol, right_space: ' ' + symbol, }; return suffixes[ symbolPosition ] || ''; }; /** * Currency information in normalized format from server settings. */ const siteCurrencySettings: Currency = { code: CURRENCY.code, symbol: CURRENCY.symbol, thousandSeparator: CURRENCY.thousandSeparator, decimalSeparator: CURRENCY.decimalSeparator, minorUnit: CURRENCY.precision, prefix: getPrefix( CURRENCY.symbol, CURRENCY.symbolPosition as SymbolPosition ), suffix: getSuffix( CURRENCY.symbol, CURRENCY.symbolPosition as SymbolPosition ), }; /** * Gets currency information in normalized format from an API response or the server. */ export const getCurrencyFromPriceResponse = ( // Currency data object, for example an API response containing currency formatting data. currencyData: | CurrencyResponse | Record< string, never > | CartShippingPackageShippingRate ): Currency => { if ( ! currencyData || typeof currencyData !== 'object' ) { return siteCurrencySettings; } const { currency_code: code, currency_symbol: symbol, currency_thousand_separator: thousandSeparator, currency_decimal_separator: decimalSeparator, currency_minor_unit: minorUnit, currency_prefix: prefix, currency_suffix: suffix, } = currencyData; return { code: code || 'USD', symbol: symbol || '$', thousandSeparator: typeof thousandSeparator === 'string' ? thousandSeparator : ',', decimalSeparator: typeof decimalSeparator === 'string' ? decimalSeparator : '.', minorUnit: Number.isFinite( minorUnit ) ? minorUnit : 2, prefix: typeof prefix === 'string' ? prefix : '$', suffix: typeof suffix === 'string' ? suffix : '', }; }; /** * Gets currency information in normalized format, allowing overrides. */ export const getCurrency = ( currencyData: Partial< Currency > = {} ): Currency => { return { ...siteCurrencySettings, ...currencyData, }; }; /** * Format a price, provided using the smallest unit of the currency, as a * decimal complete with currency symbols using current store settings. */ export const formatPrice = ( // Price in minor unit, e.g. cents. price: number | string, currencyData?: Currency ): string => { if ( price === '' || price === undefined ) { return ''; } const priceInt: number = typeof price === 'number' ? price : parseInt( price, 10 ); if ( ! Number.isFinite( priceInt ) ) { return ''; } const currency: Currency = getCurrency( currencyData ); const formattedPrice: number = priceInt / 10 ** currency.minorUnit; const formattedValue: string = currency.prefix + formattedPrice + currency.suffix; // This uses a textarea to magically decode HTML currency symbols. const txt = document.createElement( 'textarea' ); txt.innerHTML = formattedValue; return txt.value; }; packages/woocommerce-blocks/packages/prices/utils/test/price.js 0000644 00000002371 15132754524 0020751 0 ustar 00 /** * Internal dependencies */ import { formatPrice, getCurrency } from '../price'; describe( 'formatPrice', () => { test.each` value | prefix | suffix | expected ${ 1000 } | ${ '€' } | ${ '' } | ${ '€10' } ${ 1000 } | ${ '' } | ${ '€' } | ${ '10€' } ${ 1000 } | ${ '' } | ${ '$' } | ${ '10$' } ${ '1000' } | ${ '€' } | ${ '' } | ${ '€10' } ${ 0 } | ${ '€' } | ${ '' } | ${ '€0' } ${ '' } | ${ '€' } | ${ '' } | ${ '' } ${ null } | ${ '€' } | ${ '' } | ${ '' } ${ undefined } | ${ '€' } | ${ '' } | ${ '' } `( 'correctly formats price given "$value", "$prefix" prefix, and "$suffix" suffix', ( { value, prefix, suffix, expected } ) => { const formattedPrice = formatPrice( value, getCurrency( { prefix, suffix } ) ); expect( formattedPrice ).toEqual( expected ); } ); test.each` value | expected ${ 1000 } | ${ '$10' } ${ 0 } | ${ '$0' } ${ '' } | ${ '' } ${ null } | ${ '' } ${ undefined } | ${ '' } `( 'correctly formats price given "$value" only', ( { value, expected } ) => { const formattedPrice = formatPrice( value ); expect( formattedPrice ).toEqual( expected ); } ); } ); packages/woocommerce-blocks/packages/prices/index.js 0000644 00000000031 15132754524 0016626 0 ustar 00 export * from './utils'; packages/woocommerce-blocks/woocommerce-gutenberg-products-block.php 0000644 00000021520 15132754524 0022065 0 ustar 00 <?php /** * Plugin Name: WooCommerce Blocks * Plugin URI: https://github.com/woocommerce/woocommerce-gutenberg-products-block * Description: WooCommerce blocks for the Gutenberg editor. * Version: 6.1.0 * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woo-gutenberg-products-block * Requires at least: 5.8 * Requires PHP: 7.0 * WC requires at least: 5.7 * WC tested up to: 5.7 * * @package WooCommerce\Blocks * @internal This file is only used when running as a feature plugin. */ defined( 'ABSPATH' ) || exit; $minimum_wp_version = '5.8'; if ( ! defined( 'WC_BLOCKS_IS_FEATURE_PLUGIN' ) ) { define( 'WC_BLOCKS_IS_FEATURE_PLUGIN', true ); } /** * Whether notices must be displayed in the current page (plugins and WooCommerce pages). * * @since 2.5.0 */ function should_display_compatibility_notices() { $current_screen = get_current_screen(); if ( ! isset( $current_screen ) ) { return false; } $is_plugins_page = property_exists( $current_screen, 'id' ) && 'plugins' === $current_screen->id; $is_woocommerce_page = property_exists( $current_screen, 'parent_base' ) && 'woocommerce' === $current_screen->parent_base; return $is_plugins_page || $is_woocommerce_page; } if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) { /** * Outputs for an admin notice about running WooCommerce Blocks on outdated WordPress. * * @since 2.5.0 */ function woocommerce_blocks_admin_unsupported_wp_notice() { if ( should_display_compatibility_notices() ) { ?> <div class="notice notice-error"> <p><?php esc_html_e( 'The WooCommerce Blocks feature plugin requires a more recent version of WordPress and has been paused. Please update WordPress to continue enjoying WooCommerce Blocks.', 'woocommerce' ); ?></p> </div> <?php } } add_action( 'admin_notices', 'woocommerce_blocks_admin_unsupported_wp_notice' ); return; } /** * Returns whether the current version is a development version * Note this relies on composer.json version, not plugin version. * Development installs of the plugin don't have a version defined in * composer json. * * @return bool True means the current version is a development version. */ function woocommerce_blocks_is_development_version() { $composer_file = __DIR__ . '/composer.json'; if ( ! is_readable( $composer_file ) ) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- including local file $composer_config = json_decode( file_get_contents( $composer_file ), true ); return ! isset( $composer_config['version'] ); } /** * If development version is detected and the Jetpack constant is not defined, show a notice. */ if ( woocommerce_blocks_is_development_version() && ! defined( 'JETPACK_AUTOLOAD_DEV' ) ) { add_action( 'admin_notices', function() { echo '<div class="error"><p>'; printf( /* translators: %1$s is referring to a php constant name, %2$s is referring to the wp-config.php file. */ esc_html__( 'WooCommerce Blocks development mode requires the %1$s constant to be defined and true in your %2$s file. Otherwise you are loading the blocks package from WooCommerce core.', 'woocommerce' ), 'JETPACK_AUTOLOAD_DEV', 'wp-config.php' ); echo '</p></div>'; } ); } /** * Autoload packages. * * The package autoloader includes version information which prevents classes in this feature plugin * conflicting with WooCommerce core. * * We want to fail gracefully if `composer install` has not been executed yet, so we are checking for the autoloader. * If the autoloader is not present, let's log the failure and display a nice admin notice. */ $autoloader = __DIR__ . '/vendor/autoload_packages.php'; if ( is_readable( $autoloader ) ) { require $autoloader; } else { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore sprintf( /* translators: 1: composer command. 2: plugin directory */ esc_html__( 'Your installation of the WooCommerce Blocks feature plugin is incomplete. Please run %1$s within the %2$s directory.', 'woocommerce' ), '`composer install`', '`' . esc_html( str_replace( ABSPATH, '', __DIR__ ) ) . '`' ) ); } /** * Outputs an admin notice if composer install has not been ran. */ add_action( 'admin_notices', function() { ?> <div class="notice notice-error"> <p> <?php printf( /* translators: 1: composer command. 2: plugin directory */ esc_html__( 'Your installation of the WooCommerce Blocks feature plugin is incomplete. Please run %1$s within the %2$s directory.', 'woocommerce' ), '<code>composer install</code>', '<code>' . esc_html( str_replace( ABSPATH, '', __DIR__ ) ) . '</code>' ); ?> </p> </div> <?php } ); return; } add_action( 'plugins_loaded', array( '\Automattic\WooCommerce\Blocks\Package', 'init' ) ); /** * Pre-filters script translations for the given file, script handle and text domain. * * @param string|false|null $translations JSON-encoded translation data. Default null. * @param string|false $file Path to the translation file to load. False if there isn't one. * @param string $handle Name of the script to register a translation domain to. * @param string $domain The text domain. * @return string JSON translations. */ function woocommerce_blocks_get_i18n_data_json( $translations, $file, $handle, $domain ) { if ( 'woo-gutenberg-products-block' !== $domain ) { return $translations; } global $wp_scripts; if ( ! isset( $wp_scripts->registered[ $handle ], $wp_scripts->registered[ $handle ]->src ) ) { return $translations; } $handle_filename = basename( $wp_scripts->registered[ $handle ]->src ); $locale = determine_locale(); $lang_dir = WP_LANG_DIR . '/plugins'; // Translations are always based on the unminified filename. if ( substr( $handle_filename, -7 ) === '.min.js' ) { $handle_filename = substr( $handle_filename, 0, -7 ) . '.js'; } // WordPress 5.0 uses md5 hashes of file paths to associate translation // JSON files with the file they should be included for. This is an md5 // of 'packages/woocommerce-blocks/build/FILENAME.js'. $core_path_md5 = md5( 'packages/woocommerce-blocks/build/' . $handle_filename ); $core_json_file = $lang_dir . '/woocommerce-' . $locale . '-' . $core_path_md5 . '.json'; $json_translations = is_file( $core_json_file ) && is_readable( $core_json_file ) ? file_get_contents( $core_json_file ) : false; // phpcs:ignore if ( ! $json_translations ) { return $translations; } // Rather than short circuit pre_load_script_translations, we will output // core translations using an inline script. This will allow us to continue // to load feature-plugin translations which may exist as well. $output = <<<JS ( function( domain, translations ) { var localeData = translations.locale_data[ domain ] || translations.locale_data.messages; localeData[""].domain = domain; wp.i18n.setLocaleData( localeData, domain ); } )( "{$domain}", {$json_translations} ); JS; printf( "<script type='text/javascript'>\n%s\n</script>\n", $output ); // phpcs:ignore // Finally, short circuit the pre_load_script_translations hook by returning // the translation JSON from the feature plugin, if it exists so this hook // does not run again for the current handle. $path_md5 = md5( 'build/' . $handle_filename ); $json_file = $lang_dir . '/' . $domain . '-' . $locale . '-' . $path_md5 . '.json'; $translations = is_file( $json_file ) && is_readable( $json_file ) ? file_get_contents( $json_file ) : false; // phpcs:ignore if ( $translations ) { return $translations; } // Return valid empty Jed locale. return '{ "locale_data": { "messages": { "": {} } } }'; } add_filter( 'pre_load_script_translations', 'woocommerce_blocks_get_i18n_data_json', 10, 4 ); /** * Filter translations so we can retrieve translations from Core when the original and the translated * texts are the same (which happens when translations are missing). * * @param string $translation Translated text based on WC Blocks translations. * @param string $text Text to translate. * @param string $domain The text domain. * @return string WC Blocks translation. In case it's the same as $text, Core translation. */ function woocommerce_blocks_get_php_translation_from_core( $translation, $text, $domain ) { if ( 'woo-gutenberg-products-block' !== $domain ) { return $translation; } // When translation is the same, that could mean the string is not translated. // In that case, load it from core. if ( $translation === $text ) { return translate( $text, 'woocommerce' ); // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction, WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.TextDomainMismatch } return $translation; } add_filter( 'gettext', 'woocommerce_blocks_get_php_translation_from_core', 10, 3 ); packages/woocommerce-blocks/src/InboxNotifications.php 0000644 00000011665 15132754524 0017246 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; /** * A class used to display inbox messages to merchants in the WooCommerce Admin dashboard. * * @package Automattic\WooCommerce\Blocks * @since x.x.x */ class InboxNotifications { const SURFACE_CART_CHECKOUT_NOTE_NAME = 'surface_cart_checkout'; const SURFACE_CART_CHECKOUT_PROBABILITY_OPTION = 'wc_blocks_surface_cart_checkout_probability'; const PERCENT_USERS_TO_TARGET = 50; const INELIGIBLE_EXTENSIONS = [ 'automatewoo', 'mailchimp-for-woocommerce', 'mailpoet', 'klarna-payments-for-woocommerce', 'klarna-checkout-for-woocommerce', 'woocommerce-gutenberg-products-block', // Disallow the notification if the store is using the feature plugin already. 'woocommerce-all-products-for-subscriptions', 'woocommerce-bookings', 'woocommerce-box-office', 'woocommerce-cart-add-ons', 'woocommerce-checkout-add-ons', 'woocommerce-checkout-field-editor', 'woocommerce-conditional-shipping-and-payments', 'woocommerce-dynamic-pricing', 'woocommerce-eu-vat-number', 'woocommerce-follow-up-emails', 'woocommerce-gateway-amazon-payments-advanced', 'woocommerce-gateway-authorize-net-cim', 'woocommerce-google-analytics-pro', 'woocommerce-memberships', 'woocommerce-paypal-payments', 'woocommerce-pre-orders', 'woocommerce-product-bundles', 'woocommerce-shipping-fedex', 'woocommerce-smart-coupons', ]; const ELIGIBLE_COUNTRIES = [ 'GB', 'US', ]; /** * Deletes the note. */ public static function delete_surface_cart_checkout_blocks_notification() { Notes::delete_notes_with_name( self::SURFACE_CART_CHECKOUT_NOTE_NAME ); } /** * Creates a notification letting merchants know about the Cart and Checkout Blocks. */ public static function create_surface_cart_checkout_blocks_notification() { // If this is the feature plugin, then we don't need to do this. This should only show when Blocks is bundled // with WooCommerce Core. if ( Package::feature()->is_feature_plugin_build() ) { return; } if ( ! class_exists( 'Automattic\WooCommerce\Admin\Notes\WC_Admin_Notes' ) ) { return; } if ( ! class_exists( 'WC_Data_Store' ) ) { return; } $data_store = \WC_Data_Store::load( 'admin-note' ); $note_ids = $data_store->get_notes_with_name( self::SURFACE_CART_CHECKOUT_NOTE_NAME ); // Calculate store's eligibility to be shown the notice, starting with whether they have any plugins we know to // be incompatible with Blocks. This check is done before checking if the note exists already because we want to // delete the note if the merchant activates an ineligible plugin. foreach ( self::INELIGIBLE_EXTENSIONS as $extension ) { if ( is_plugin_active( $extension . '/' . $extension . '.php' ) ) { // Delete the notification here, we shouldn't show it if it's not going to work with the merchant's site. self::delete_surface_cart_checkout_blocks_notification(); return; } } foreach ( (array) $note_ids as $note_id ) { $note = Notes::get_note( $note_id ); // Return now because the note already exists. if ( $note->get_name() === self::SURFACE_CART_CHECKOUT_NOTE_NAME ) { return; } } // Next check the store is located in one of the eligible countries. $raw_country = get_option( 'woocommerce_default_country' ); $country = explode( ':', $raw_country )[0]; if ( ! in_array( $country, self::ELIGIBLE_COUNTRIES, true ) ) { return; } // Pick a random number between 1 and 100 and add this to the wp_options table. This can then be used to target // a percentage of users. We do this here so we target a truer percentage of eligible users than if we did it // before checking plugins/country. $existing_probability = get_option( self::SURFACE_CART_CHECKOUT_PROBABILITY_OPTION ); if ( false === $existing_probability ) { $existing_probability = wp_rand( 0, 100 ); add_option( self::SURFACE_CART_CHECKOUT_PROBABILITY_OPTION, $existing_probability ); } // Finally, check if the store's generated % chance is below the % of users we want to surface this to. if ( $existing_probability > self::PERCENT_USERS_TO_TARGET ) { return; } // At this point, the store meets all the criteria to be shown the notice! Woo! $note = new Note(); $note->set_title( __( 'Introducing the Cart and Checkout blocks!', 'woocommerce' ) ); $note->set_content( __( "Increase your store's revenue with the conversion optimized Cart & Checkout WooCommerce blocks available in the WooCommerce Blocks extension.", 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_source( 'woo-gutenberg-products-block' ); $note->set_name( self::SURFACE_CART_CHECKOUT_NOTE_NAME ); $note->add_action( 'learn_more', 'Learn More', 'https://woocommerce.com/checkout-blocks/' ); $note->save(); } } packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php 0000644 00000004071 15132754524 0021062 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use WC_Gateway_Paypal; use Automattic\WooCommerce\Blocks\Assets\Api; /** * PayPal Standard payment method integration * * @since 2.6.0 */ final class PayPal extends AbstractPaymentMethodType { /** * Payment method name defined by payment methods extending this class. * * @var string */ protected $name = 'paypal'; /** * An instance of the Asset Api * * @var Api */ private $asset_api; /** * Constructor * * @param Api $asset_api An instance of Api. */ public function __construct( Api $asset_api ) { $this->asset_api = $asset_api; } /** * Initializes the payment method type. */ public function initialize() { $this->settings = get_option( 'woocommerce_paypal_settings', [] ); } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return filter_var( $this->get_setting( 'enabled', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Returns an array of scripts/handles to be registered for this payment method. * * @return array */ public function get_payment_method_script_handles() { $this->asset_api->register_script( 'wc-payment-method-paypal', 'build/wc-payment-method-paypal.js' ); return [ 'wc-payment-method-paypal' ]; } /** * Returns an array of key=>value pairs of data made available to the payment methods script. * * @return array */ public function get_payment_method_data() { return [ 'title' => $this->get_setting( 'title' ), 'description' => $this->get_setting( 'description' ), 'supports' => $this->get_supported_features(), ]; } /** * Returns an array of supported features. * * @return string[] */ public function get_supported_features() { $gateway = new WC_Gateway_Paypal(); $features = array_filter( $gateway->supports, array( $gateway, 'supports' ) ); return apply_filters( '__experimental_woocommerce_blocks_payment_gateway_features_list', $features, $this->get_name() ); } } packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php 0000644 00000003261 15132754524 0022254 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use Automattic\WooCommerce\Blocks\Assets\Api; /** * Bank Transfer (BACS) payment method integration * * @since 3.0.0 */ final class BankTransfer extends AbstractPaymentMethodType { /** * Payment method name/id/slug (matches id in WC_Gateway_BACS in core). * * @var string */ protected $name = 'bacs'; /** * An instance of the Asset Api * * @var Api */ private $asset_api; /** * Constructor * * @param Api $asset_api An instance of Api. */ public function __construct( Api $asset_api ) { $this->asset_api = $asset_api; } /** * Initializes the payment method type. */ public function initialize() { $this->settings = get_option( 'woocommerce_bacs_settings', [] ); } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return filter_var( $this->get_setting( 'enabled', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Returns an array of scripts/handles to be registered for this payment method. * * @return array */ public function get_payment_method_script_handles() { $this->asset_api->register_script( 'wc-payment-method-bacs', 'build/wc-payment-method-bacs.js' ); return [ 'wc-payment-method-bacs' ]; } /** * Returns an array of key=>value pairs of data made available to the payment methods script. * * @return array */ public function get_payment_method_data() { return [ 'title' => $this->get_setting( 'title' ), 'description' => $this->get_setting( 'description' ), 'supports' => $this->get_supported_features(), ]; } } packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php 0000644 00000005425 15132754524 0025004 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface; /** * AbstractPaymentMethodType class. * * @since 2.6.0 */ abstract class AbstractPaymentMethodType implements PaymentMethodTypeInterface { /** * Payment method name defined by payment methods extending this class. * * @var string */ protected $name = ''; /** * Settings from the WP options table * * @var array */ protected $settings = []; /** * Get a setting from the settings array if set. * * @param string $name Setting name. * @param mixed $default Value that is returned if the setting does not exist. * @return mixed */ protected function get_setting( $name, $default = '' ) { return isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : $default; } /** * Returns the name of the payment method. */ public function get_name() { return $this->name; } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return true; } /** * Returns an array of script handles to enqueue for this payment method in * the frontend context * * @return string[] */ public function get_payment_method_script_handles() { return []; } /** * Returns an array of script handles to enqueue for this payment method in * the admin context * * @return string[] */ public function get_payment_method_script_handles_for_admin() { return $this->get_payment_method_script_handles(); } /** * Returns an array of supported features. * * @return string[] */ public function get_supported_features() { return [ 'products' ]; } /** * An array of key, value pairs of data made available to payment methods * client side. * * @return array */ public function get_payment_method_data() { return []; } /** * Returns an array of script handles to enqueue in the frontend context. * * Alias of get_payment_method_script_handles. Defined by IntegrationInterface. * * @return string[] */ public function get_script_handles() { return $this->get_payment_method_script_handles(); } /** * Returns an array of script handles to enqueue in the admin context. * * Alias of get_payment_method_script_handles_for_admin. Defined by IntegrationInterface. * * @return string[] */ public function get_editor_script_handles() { return $this->get_payment_method_script_handles_for_admin(); } /** * An array of key, value pairs of data made available to the block on the client side. * * Alias of get_payment_method_data. Defined by IntegrationInterface. * * @return array */ public function get_script_data() { return $this->get_payment_method_data(); } } packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php 0000644 00000004756 15132754524 0022565 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use Automattic\WooCommerce\Blocks\Assets\Api; /** * Cash on Delivery (COD) payment method integration * * @since 3.0.0 */ final class CashOnDelivery extends AbstractPaymentMethodType { /** * Payment method name/id/slug (matches id in WC_Gateway_COD in core). * * @var string */ protected $name = 'cod'; /** * An instance of the Asset Api * * @var Api */ private $asset_api; /** * Constructor * * @param Api $asset_api An instance of Api. */ public function __construct( Api $asset_api ) { $this->asset_api = $asset_api; } /** * Initializes the payment method type. */ public function initialize() { $this->settings = get_option( 'woocommerce_cod_settings', [] ); } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return filter_var( $this->get_setting( 'enabled', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Return enable_for_virtual option. * * @return boolean True if store allows COD payment for orders containing only virtual products. */ private function get_enable_for_virtual() { return filter_var( $this->get_setting( 'enable_for_virtual', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Return enable_for_methods option. * * @return array Array of shipping methods (string ids) that allow COD. (If empty, all support COD.) */ private function get_enable_for_methods() { $enable_for_methods = $this->get_setting( 'enable_for_methods', [] ); if ( '' === $enable_for_methods ) { return []; } return $enable_for_methods; } /** * Returns an array of scripts/handles to be registered for this payment method. * * @return array */ public function get_payment_method_script_handles() { $this->asset_api->register_script( 'wc-payment-method-cod', 'build/wc-payment-method-cod.js' ); return [ 'wc-payment-method-cod' ]; } /** * Returns an array of key=>value pairs of data made available to the payment methods script. * * @return array */ public function get_payment_method_data() { return [ 'title' => $this->get_setting( 'title' ), 'description' => $this->get_setting( 'description' ), 'enableForVirtual' => $this->get_enable_for_virtual(), 'enableForShippingMethods' => $this->get_enable_for_methods(), 'supports' => $this->get_supported_features(), ]; } } packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php 0000644 00000026470 15132754524 0021151 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use Exception; use WC_Stripe_Payment_Request; use WC_Stripe_Helper; use WC_Gateway_Stripe; use Automattic\WooCommerce\Blocks\Assets\Api; use Automattic\WooCommerce\Blocks\Payments\PaymentContext; use Automattic\WooCommerce\Blocks\Payments\PaymentResult; /** * Stripe payment method integration * * Temporary integration of the stripe payment method for the new cart and * checkout blocks. Once the api is demonstrated to be stable, this integration * will be moved to the Stripe extension * * @since 2.6.0 */ final class Stripe extends AbstractPaymentMethodType { /** * Payment method name defined by payment methods extending this class. * * @var string */ protected $name = 'stripe'; /** * An instance of the Asset Api * * @var Api */ private $asset_api; /** * Constructor * * @param Api $asset_api An instance of Api. */ public function __construct( Api $asset_api ) { $this->asset_api = $asset_api; add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_payment_request_order_meta' ], 8, 2 ); add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_stripe_intents' ], 9999, 2 ); } /** * Initializes the payment method type. */ public function initialize() { $this->settings = get_option( 'woocommerce_stripe_settings', [] ); } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled']; } /** * Returns an array of scripts/handles to be registered for this payment method. * * @return array */ public function get_payment_method_script_handles() { $this->asset_api->register_script( 'wc-payment-method-stripe', 'build/wc-payment-method-stripe.js', [] ); return [ 'wc-payment-method-stripe' ]; } /** * Returns an array of key=>value pairs of data made available to the payment methods script. * * @return array */ public function get_payment_method_data() { return [ 'stripeTotalLabel' => $this->get_total_label(), 'publicKey' => $this->get_publishable_key(), 'allowPrepaidCard' => $this->get_allow_prepaid_card(), 'title' => $this->get_title(), 'button' => [ 'type' => $this->get_button_type(), 'theme' => $this->get_button_theme(), 'height' => $this->get_button_height(), 'locale' => $this->get_button_locale(), ], 'inline_cc_form' => $this->get_inline_cc_form(), 'icons' => $this->get_icons(), 'showSavedCards' => $this->get_show_saved_cards(), 'allowPaymentRequest' => $this->get_allow_payment_request(), 'showSaveOption' => $this->get_show_save_option(), 'supports' => $this->get_supported_features(), ]; } /** * Determine if store allows cards to be saved during checkout. * * @return bool True if merchant allows shopper to save card (payment method) during checkout). */ private function get_show_saved_cards() { return isset( $this->settings['saved_cards'] ) ? 'yes' === $this->settings['saved_cards'] : false; } /** * Determine if the checkbox to enable the user to save their payment method should be shown. * * @return bool True if the save payment checkbox should be displayed to the user. */ private function get_show_save_option() { $saved_cards = $this->get_show_saved_cards(); // This assumes that Stripe supports `tokenization` - currently this is true, based on // https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95 . // See https://github.com/woocommerce/woocommerce-gateway-stripe/blob/ad19168b63df86176cbe35c3e95203a245687640/includes/class-wc-gateway-stripe.php#L271 and // https://github.com/woocommerce/woocommerce/wiki/Payment-Token-API . return apply_filters( 'wc_stripe_display_save_payment_method_checkbox', filter_var( $saved_cards, FILTER_VALIDATE_BOOLEAN ) ); } /** * Returns the label to use accompanying the total in the stripe statement. * * @return string Statement descriptor. */ private function get_total_label() { return ! empty( $this->settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->settings['statement_descriptor'] ) : ''; } /** * Returns the publishable api key for the Stripe service. * * @return string Public api key. */ private function get_publishable_key() { $test_mode = ( ! empty( $this->settings['testmode'] ) && 'yes' === $this->settings['testmode'] ); $setting_key = $test_mode ? 'test_publishable_key' : 'publishable_key'; return ! empty( $this->settings[ $setting_key ] ) ? $this->settings[ $setting_key ] : ''; } /** * Returns whether to allow prepaid cards for payments. * * @return bool True means to allow prepaid card (default). */ private function get_allow_prepaid_card() { return apply_filters( 'wc_stripe_allow_prepaid_card', true ); } /** * Returns the title string to use in the UI (customisable via admin settings screen). * * @return string Title / label string */ private function get_title() { return isset( $this->settings['title'] ) ? $this->settings['title'] : __( 'Credit / Debit Card', 'woocommerce' ); } /** * Determine if store allows Payment Request buttons - e.g. Apple Pay / Chrome Pay. * * @return bool True if merchant has opted into payment request. */ private function get_allow_payment_request() { $option = isset( $this->settings['payment_request'] ) ? $this->settings['payment_request'] : false; return filter_var( $option, FILTER_VALIDATE_BOOLEAN ); } /** * Return the button type for the payment button. * * @return string Defaults to 'default'. */ private function get_button_type() { return isset( $this->settings['payment_request_button_type'] ) ? $this->settings['payment_request_button_type'] : 'default'; } /** * Return the theme to use for the payment button. * * @return string Defaults to 'dark'. */ private function get_button_theme() { return isset( $this->settings['payment_request_button_theme'] ) ? $this->settings['payment_request_button_theme'] : 'dark'; } /** * Return the height for the payment button. * * @return string A pixel value for the height (defaults to '64'). */ private function get_button_height() { return isset( $this->settings['payment_request_button_height'] ) ? str_replace( 'px', '', $this->settings['payment_request_button_height'] ) : '64'; } /** * Return the inline cc option. * * @return boolean True if the inline CC form option is enabled. */ private function get_inline_cc_form() { return isset( $this->settings['inline_cc_form'] ) && 'yes' === $this->settings['inline_cc_form']; } /** * Return the locale for the payment button. * * @return string Defaults to en_US. */ private function get_button_locale() { return apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ); } /** * Return the icons urls. * * @return array Arrays of icons metadata. */ private function get_icons() { $icons_src = [ 'visa' => [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg', 'alt' => __( 'Visa', 'woocommerce' ), ], 'amex' => [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg', 'alt' => __( 'American Express', 'woocommerce' ), ], 'mastercard' => [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg', 'alt' => __( 'Mastercard', 'woocommerce' ), ], ]; if ( 'USD' === get_woocommerce_currency() ) { $icons_src['discover'] = [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg', 'alt' => __( 'Discover', 'woocommerce' ), ]; $icons_src['jcb'] = [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg', 'alt' => __( 'JCB', 'woocommerce' ), ]; $icons_src['diners'] = [ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg', 'alt' => __( 'Diners', 'woocommerce' ), ]; } return $icons_src; } /** * Add payment request data to the order meta as hooked on the * woocommerce_rest_checkout_process_payment_with_context action. * * @param PaymentContext $context Holds context for the payment. * @param PaymentResult $result Result object for the payment. */ public function add_payment_request_order_meta( PaymentContext $context, PaymentResult &$result ) { $data = $context->payment_data; if ( ! empty( $data['payment_request_type'] ) && 'stripe' === $context->payment_method ) { // phpcs:ignore WordPress.Security.NonceVerification $post_data = $_POST; $_POST = $context->payment_data; $this->add_order_meta( $context->order, $data['payment_request_type'] ); $_POST = $post_data; } // hook into stripe error processing so that we can capture the error to // payment details (which is added to notices and thus not helpful for // this context). if ( 'stripe' === $context->payment_method ) { add_action( 'wc_gateway_stripe_process_payment_error', function( $error ) use ( &$result ) { $payment_details = $result->payment_details; $payment_details['errorMessage'] = wp_strip_all_tags( $error->getLocalizedMessage() ); $result->set_payment_details( $payment_details ); } ); } } /** * Handles any potential stripe intents on the order that need handled. * * This is configured to execute after legacy payment processing has * happened on the woocommerce_rest_checkout_process_payment_with_context * action hook. * * @param PaymentContext $context Holds context for the payment. * @param PaymentResult $result Result object for the payment. */ public function add_stripe_intents( PaymentContext $context, PaymentResult &$result ) { if ( 'stripe' === $context->payment_method && ( ! empty( $result->payment_details['payment_intent_secret'] ) || ! empty( $result->payment_details['setup_intent_secret'] ) ) ) { $payment_details = $result->payment_details; $payment_details['verification_endpoint'] = add_query_arg( [ 'order' => $context->order->get_id(), 'nonce' => wp_create_nonce( 'wc_stripe_confirm_pi' ), 'redirect_to' => rawurlencode( $result->redirect_url ), ], home_url() . \WC_Ajax::get_endpoint( 'wc_stripe_verify_intent' ) ); $result->set_payment_details( $payment_details ); $result->set_status( 'success' ); } } /** * Handles adding information about the payment request type used to the order meta. * * @param \WC_Order $order The order being processed. * @param string $payment_request_type The payment request type used for payment. */ private function add_order_meta( \WC_Order $order, string $payment_request_type ) { if ( 'apple_pay' === $payment_request_type ) { $order->set_payment_method_title( 'Apple Pay (Stripe)' ); $order->save(); } if ( 'payment_request_api' === $payment_request_type ) { $order->set_payment_method_title( 'Chrome Payment Request (Stripe)' ); $order->save(); } } /** * Returns an array of supported features. * * @return string[] */ public function get_supported_features() { $gateway = new WC_Gateway_Stripe(); return array_filter( $gateway->supports, array( $gateway, 'supports' ) ); } } packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php 0000644 00000003266 15132754524 0021113 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments\Integrations; use Exception; use Automattic\WooCommerce\Blocks\Assets\Api; /** * Cheque payment method integration * * @since 2.6.0 */ final class Cheque extends AbstractPaymentMethodType { /** * Payment method name defined by payment methods extending this class. * * @var string */ protected $name = 'cheque'; /** * An instance of the Asset Api * * @var Api */ private $asset_api; /** * Constructor * * @param Api $asset_api An instance of Api. */ public function __construct( Api $asset_api ) { $this->asset_api = $asset_api; } /** * Initializes the payment method type. */ public function initialize() { $this->settings = get_option( 'woocommerce_cheque_settings', [] ); } /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return filter_var( $this->get_setting( 'enabled', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Returns an array of scripts/handles to be registered for this payment method. * * @return array */ public function get_payment_method_script_handles() { $this->asset_api->register_script( 'wc-payment-method-cheque', 'build/wc-payment-method-cheque.js' ); return [ 'wc-payment-method-cheque' ]; } /** * Returns an array of key=>value pairs of data made available to the payment methods script. * * @return array */ public function get_payment_method_data() { return [ 'title' => $this->get_setting( 'title' ), 'description' => $this->get_setting( 'description' ), 'supports' => $this->get_supported_features(), ]; } } packages/woocommerce-blocks/src/Payments/PaymentContext.php 0000644 00000003471 15132754524 0020213 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments; /** * PaymentContext class. */ class PaymentContext { /** * Payment method ID. * * @var string */ protected $payment_method = ''; /** * Order object for the order being paid. * * @var \WC_Order */ protected $order; /** * Holds data to send to the payment gateway to support payment. * * @var array Key value pairs. */ protected $payment_data = []; /** * Magic getter for protected properties. * * @param string $name Property name. */ public function __get( $name ) { if ( in_array( $name, [ 'payment_method', 'order', 'payment_data' ], true ) ) { return $this->$name; } return null; } /** * Set the chosen payment method ID context. * * @param string $payment_method Payment method ID. */ public function set_payment_method( $payment_method ) { $this->payment_method = (string) $payment_method; } /** * Retrieve the payment method instance for the current set payment method. * * @return {\WC_Payment_Gateway|null} An instance of the payment gateway if it exists. */ public function get_payment_method_instance() { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $this->payment_method ] ) ) { return; } return $available_gateways[ $this->payment_method ]; } /** * Set the order context. * * @param \WC_Order $order Order object. */ public function set_order( \WC_Order $order ) { $this->order = $order; } /** * Set payment data context. * * @param array $payment_data Array of key value pairs of data. */ public function set_payment_data( $payment_data = [] ) { $this->payment_data = []; foreach ( $payment_data as $key => $value ) { $this->payment_data[ (string) $key ] = (string) $value; } } } packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php 0000644 00000003520 15132754524 0021533 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; /** * Class used for interacting with payment method types. * * @since 2.6.0 */ final class PaymentMethodRegistry extends IntegrationRegistry { /** * Integration identifier is used to construct hook names and is given when the integration registry is initialized. * * @var string */ protected $registry_identifier = 'payment_method_type'; /** * Retrieves all registered payment methods that are also active. * * @return PaymentMethodTypeInterface[] */ public function get_all_active_registered() { return array_filter( $this->get_all_registered(), function( $payment_method ) { return $payment_method->is_active(); } ); } /** * Gets an array of all registered payment method script handles, but only for active payment methods. * * @return string[] */ public function get_all_active_payment_method_script_dependencies() { $script_handles = []; $payment_methods = $this->get_all_active_registered(); foreach ( $payment_methods as $payment_method ) { $script_handles = array_merge( $script_handles, is_admin() ? $payment_method->get_payment_method_script_handles_for_admin() : $payment_method->get_payment_method_script_handles() ); } return array_unique( array_filter( $script_handles ) ); } /** * Gets an array of all registered payment method script data, but only for active payment methods. * * @return array */ public function get_all_registered_script_data() { $script_data = []; $payment_methods = $this->get_all_active_registered(); foreach ( $payment_methods as $payment_method ) { $script_data[ $payment_method->get_name() . '_data' ] = $payment_method->get_payment_method_data(); } return array_filter( $script_data ); } } packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php 0000644 00000002012 15132754524 0022460 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments; use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface; interface PaymentMethodTypeInterface extends IntegrationInterface { /** * Returns if this payment method should be active. If false, the scripts will not be enqueued. * * @return boolean */ public function is_active(); /** * Returns an array of script handles to enqueue for this payment method in * the frontend context * * @return string[] */ public function get_payment_method_script_handles(); /** * Returns an array of script handles to enqueue for this payment method in * the admin context * * @return string[] */ public function get_payment_method_script_handles_for_admin(); /** * An array of key, value pairs of data made available to payment methods * client side. * * @return array */ public function get_payment_method_data(); /** * Get array of supported features. * * @return string[] */ public function get_supported_features(); } packages/woocommerce-blocks/src/Payments/Api.php 0000644 00000022062 15132754524 0015737 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\NoticeHandler; use Automattic\WooCommerce\Blocks\Payments\Integrations\Stripe; use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque; use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal; use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer; use Automattic\WooCommerce\Blocks\Payments\Integrations\CashOnDelivery; /** * The Api class provides an interface to payment method registration. * * @since 2.6.0 */ class Api { /** * Reference to the PaymentMethodRegistry instance. * * @var PaymentMethodRegistry */ private $payment_method_registry; /** * Reference to the AssetDataRegistry instance. * * @var AssetDataRegistry */ private $asset_registry; /** * Constructor * * @param PaymentMethodRegistry $payment_method_registry An instance of Payment Method Registry. * @param AssetDataRegistry $asset_registry Used for registering data to pass along to the request. */ public function __construct( PaymentMethodRegistry $payment_method_registry, AssetDataRegistry $asset_registry ) { $this->payment_method_registry = $payment_method_registry; $this->asset_registry = $asset_registry; $this->init(); } /** * Initialize class features. */ protected function init() { add_action( 'init', array( $this->payment_method_registry, 'initialize' ), 5 ); add_filter( 'woocommerce_blocks_register_script_dependencies', array( $this, 'add_payment_method_script_dependencies' ), 10, 2 ); add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_payment_method_script_data' ) ); add_action( 'woocommerce_blocks_cart_enqueue_data', array( $this, 'add_payment_method_script_data' ) ); add_action( 'woocommerce_blocks_payment_method_type_registration', array( $this, 'register_payment_method_integrations' ) ); add_action( 'woocommerce_rest_checkout_process_payment_with_context', array( $this, 'process_legacy_payment' ), 999, 2 ); add_action( 'wp_print_scripts', array( $this, 'verify_payment_methods_dependencies' ), 1 ); } /** * Add payment method script handles as script dependencies. * * @param array $dependencies Array of script dependencies. * @param string $handle Script handle. * @return array */ public function add_payment_method_script_dependencies( $dependencies, $handle ) { if ( ! in_array( $handle, [ 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-block', 'wc-cart-block-frontend', 'wc-cart-i2-block', 'wc-cart-i2-block-frontend' ], true ) ) { return $dependencies; } return array_merge( $dependencies, $this->payment_method_registry->get_all_active_payment_method_script_dependencies() ); } /** * Returns true if the payment gateway is enabled. * * @param object $gateway Payment gateway. * @return boolean */ private function is_payment_gateway_enabled( $gateway ) { return filter_var( $gateway->enabled, FILTER_VALIDATE_BOOLEAN ); } /** * Add payment method data to Asset Registry. */ public function add_payment_method_script_data() { // Enqueue the order of enabled gateways as `paymentGatewaySortOrder`. if ( ! $this->asset_registry->exists( 'paymentGatewaySortOrder' ) ) { $payment_gateways = WC()->payment_gateways->payment_gateways(); $enabled_gateways = array_filter( $payment_gateways, array( $this, 'is_payment_gateway_enabled' ) ); $this->asset_registry->add( 'paymentGatewaySortOrder', array_keys( $enabled_gateways ) ); } // Enqueue all registered gateway data (settings/config etc). $script_data = $this->payment_method_registry->get_all_registered_script_data(); foreach ( $script_data as $asset_data_key => $asset_data_value ) { if ( ! $this->asset_registry->exists( $asset_data_key ) ) { $this->asset_registry->add( $asset_data_key, $asset_data_value ); } } } /** * Register payment method integrations bundled with blocks. * * @param PaymentMethodRegistry $payment_method_registry Payment method registry instance. */ public function register_payment_method_integrations( PaymentMethodRegistry $payment_method_registry ) { // This is temporarily registering Stripe until it's moved to the extension. if ( class_exists( '\WC_Stripe', false ) && ! $payment_method_registry->is_registered( 'stripe' ) ) { $payment_method_registry->register( Package::container()->get( Stripe::class ) ); } $payment_method_registry->register( Package::container()->get( Cheque::class ) ); $payment_method_registry->register( Package::container()->get( PayPal::class ) ); $payment_method_registry->register( Package::container()->get( BankTransfer::class ) ); $payment_method_registry->register( Package::container()->get( CashOnDelivery::class ) ); } /** * Attempt to process a payment for the checkout API if no payment methods support the * woocommerce_rest_checkout_process_payment_with_context action. * * @param PaymentContext $context Holds context for the payment. * @param PaymentResult $result Result of the payment. */ public function process_legacy_payment( PaymentContext $context, PaymentResult &$result ) { if ( $result->status ) { return; } // phpcs:ignore WordPress.Security.NonceVerification $post_data = $_POST; // Set constants. wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); // Add the payment data from the API to the POST global. $_POST = $context->payment_data; // Call the process payment method of the chosen gateway. $payment_method_object = $context->get_payment_method_instance(); if ( ! $payment_method_object instanceof \WC_Payment_Gateway ) { return; } $payment_method_object->validate_fields(); // If errors were thrown, we need to abort. NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_payment_error' ); // Process Payment. $gateway_result = $payment_method_object->process_payment( $context->order->get_id() ); // Restore $_POST data. $_POST = $post_data; // If `process_payment` added notices, clear them. Notices are not displayed from the API -- payment should fail, // and a generic notice will be shown instead if payment failed. wc_clear_notices(); // Handle result. $result->set_status( isset( $gateway_result['result'] ) && 'success' === $gateway_result['result'] ? 'success' : 'failure' ); // set payment_details from result. $result->set_payment_details( array_merge( $result->payment_details, $gateway_result ) ); $result->set_redirect_url( $gateway_result['redirect'] ); } /** * Verify all dependencies of registered payment methods have been registered. * If not, remove that payment method script from the list of dependencies * of Cart and Checkout block scripts so it doesn't break the blocks and show * an error in the admin. */ public function verify_payment_methods_dependencies() { $wp_scripts = wp_scripts(); $payment_method_scripts = $this->payment_method_registry->get_all_active_payment_method_script_dependencies(); foreach ( $payment_method_scripts as $payment_method_script ) { if ( ! array_key_exists( $payment_method_script, $wp_scripts->registered ) || ! property_exists( $wp_scripts->registered[ $payment_method_script ], 'deps' ) ) { continue; } $deps = $wp_scripts->registered[ $payment_method_script ]->deps; foreach ( $deps as $dep ) { if ( ! wp_script_is( $dep, 'registered' ) ) { $error_handle = $dep . '-dependency-error'; $error_message = sprintf( 'Payment gateway with handle \'%1$s\' has been deactivated in Cart and Checkout blocks because its dependency \'%2$s\' is not registered. Read the docs about registering assets for payment methods: https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/extensibility/payment-method-integration.md#registering-assets', esc_html( $payment_method_script ), esc_html( $dep ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $error_message ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( $error_handle, '' ); wp_enqueue_script( $error_handle ); wp_add_inline_script( $error_handle, sprintf( 'console.error( "%s" );', $error_message ) ); $cart_checkout_scripts = [ 'wc-cart-block', 'wc-cart-block-frontend', 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-i2-block', 'wc-cart-i2-block-frontend' ]; foreach ( $cart_checkout_scripts as $script_handle ) { if ( ! array_key_exists( $script_handle, $wp_scripts->registered ) || ! property_exists( $wp_scripts->registered[ $script_handle ], 'deps' ) ) { continue; } // Remove payment method script from dependencies. $wp_scripts->registered[ $script_handle ]->deps = array_diff( $wp_scripts->registered[ $script_handle ]->deps, [ $payment_method_script ] ); } } } } } } packages/woocommerce-blocks/src/Payments/PaymentResult.php 0000644 00000003726 15132754524 0020050 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Payments; /** * PaymentResult class. */ class PaymentResult { /** * List of valid payment statuses. * * @var array */ protected $valid_statuses = [ 'success', 'failure', 'pending', 'error' ]; /** * Current payment status. * * @var string */ protected $status = ''; /** * Array of details about the payment. * * @var string */ protected $payment_details = []; /** * Redirect URL for checkout. * * @var string */ protected $redirect_url = ''; /** * Constructor. * * @param string $status Sets the payment status for the result. */ public function __construct( $status = '' ) { if ( $status ) { $this->set_status( $status ); } } /** * Magic getter for protected properties. * * @param string $name Property name. */ public function __get( $name ) { if ( in_array( $name, [ 'status', 'payment_details', 'redirect_url' ], true ) ) { return $this->$name; } return null; } /** * Set payment status. * * @throws \Exception When an invalid status is provided. * * @param string $payment_status Status to set. */ public function set_status( $payment_status ) { if ( ! in_array( $payment_status, $this->valid_statuses, true ) ) { throw new \Exception( sprintf( 'Invalid payment status %s. Use one of %s', $payment_status, implode( ', ', $this->valid_statuses ) ) ); } $this->status = $payment_status; } /** * Set payment details. * * @param array $payment_details Array of key value pairs of data. */ public function set_payment_details( $payment_details = [] ) { $this->payment_details = []; foreach ( $payment_details as $key => $value ) { $this->payment_details[ (string) $key ] = (string) $value; } } /** * Set redirect URL. * * @param array $redirect_url URL to redirect the customer to after checkout. */ public function set_redirect_url( $redirect_url = [] ) { $this->redirect_url = esc_url_raw( $redirect_url ); } } packages/woocommerce-blocks/src/RestApi.php 0000644 00000005725 15132754524 0015004 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\StoreApi\RoutesController; use Automattic\WooCommerce\Blocks\StoreApi\SchemaController; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * RestApi class. * Registers controllers in the blocks REST API namespace. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class RestApi { /** * Stores Rest Routes instance * * @var RoutesController */ private $routes; /** * Constructor * * @param RoutesController $routes Rest Routes instance. */ public function __construct( RoutesController $routes ) { $this->routes = $routes; $this->init(); } /** * Initialize class features. */ protected function init() { add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 ); add_filter( 'rest_authentication_errors', array( $this, 'store_api_authentication' ) ); add_action( 'set_logged_in_cookie', array( $this, 'store_api_logged_in_cookie' ) ); } /** * Register REST API routes. */ public function register_rest_routes() { $this->routes->register_routes(); } /** * Get routes for a namespace. * * @param string $namespace Namespace to retrieve. * @return array|null */ public function get_routes_from_namespace( $namespace ) { $rest_server = rest_get_server(); $namespace_index = $rest_server->get_namespace_index( [ 'namespace' => $namespace, 'context' => 'view', ] ); $response_data = $namespace_index->get_data(); return isset( $response_data['routes'] ) ? $response_data['routes'] : null; } /** * The Store API does not require authentication. * * @param \WP_Error|mixed $result Error from another authentication handler, null if we should handle it, or another value if not. * @return \WP_Error|null|bool */ public function store_api_authentication( $result ) { // Pass through errors from other authentication methods used before this one. if ( ! empty( $result ) || ! self::is_request_to_store_api() ) { return $result; } return true; } /** * When the login cookies are set, they are not available until the next page reload. For the Store API, specifically * for returning updated nonces, we need this to be available immediately. * * @param string $logged_in_cookie The value for the logged in cookie. */ public function store_api_logged_in_cookie( $logged_in_cookie ) { if ( ! defined( 'LOGGED_IN_COOKIE' ) || ! self::is_request_to_store_api() ) { return; } $_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie; } /** * Check if is request to the Store API. * * @return bool */ protected function is_request_to_store_api() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); return false !== strpos( $request_uri, $rest_prefix . 'wc/store' ); } } packages/woocommerce-blocks/src/Assets/Api.php 0000644 00000016050 15132754524 0015401 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Assets; use Automattic\WooCommerce\Blocks\Domain\Package; use Exception; /** * The Api class provides an interface to various asset registration helpers. * * Contains asset api methods * * @since 2.5.0 */ class Api { /** * Stores inline scripts already enqueued. * * @var array */ private $inline_scripts = []; /** * Reference to the Package instance * * @var Package */ private $package; /** * Constructor for class * * @param Package $package An instance of Package. */ public function __construct( Package $package ) { $this->package = $package; } /** * Get the file modified time as a cache buster if we're in dev mode. * * @param string $file Local path to the file (relative to the plugin * directory). * @return string The cache buster value to use for the given file. */ protected function get_file_version( $file ) { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $this->package->get_path() . $file ) ) { return filemtime( $this->package->get_path( trim( $file, '/' ) ) ); } return $this->package->get_version(); } /** * Retrieve the url to an asset for this plugin. * * @param string $relative_path An optional relative path appended to the * returned url. * * @return string */ protected function get_asset_url( $relative_path = '' ) { return $this->package->get_url( $relative_path ); } /** * Get src, version and dependencies given a script relative src. * * @param string $relative_src Relative src to the script. * @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array. * * @return array src, version and dependencies of the script. */ public function get_script_data( $relative_src, $dependencies = [] ) { $src = ''; $version = '1'; if ( $relative_src ) { $src = $this->get_asset_url( $relative_src ); $asset_path = $this->package->get_path( str_replace( '.js', '.asset.php', $relative_src ) ); if ( file_exists( $asset_path ) ) { $asset = require $asset_path; $dependencies = isset( $asset['dependencies'] ) ? array_merge( $asset['dependencies'], $dependencies ) : $dependencies; $version = ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src ); } else { $version = $this->get_file_version( $relative_src ); } } return array( 'src' => $src, 'version' => $version, 'dependencies' => $dependencies, ); } /** * Registers a script according to `wp_register_script`, adding the correct prefix, and additionally loading translations. * * When creating script assets, the following rules should be followed: * 1. All asset handles should have a `wc-` prefix. * 2. If the asset handle is for a Block (in editor context) use the `-block` suffix. * 3. If the asset handle is for a Block (in frontend context) use the `-block-frontend` suffix. * 4. If the asset is for any other script being consumed or enqueued by the blocks plugin, use the `wc-blocks-` prefix. * * @since 2.5.0 * @throws Exception If the registered script has a dependency on itself. * * @param string $handle Unique name of the script. * @param string $relative_src Relative url for the script to the path from plugin root. * @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array. * @param bool $has_i18n Optional. Whether to add a script translation call to this file. Default: true. */ public function register_script( $handle, $relative_src, $dependencies = [], $has_i18n = true ) { $script_data = $this->get_script_data( $relative_src, $dependencies ); if ( in_array( $handle, $script_data['dependencies'], true ) ) { if ( $this->package->feature()->is_development_environment() ) { $dependencies = array_diff( $script_data['dependencies'], [ $handle ] ); add_action( 'admin_notices', function() use ( $handle ) { echo '<div class="error"><p>'; /* translators: %s file handle name. */ printf( esc_html__( 'Script with handle %s had a dependency on itself which has been removed. This is an indicator that your JS code has a circular dependency that can cause bugs.', 'woocommerce' ), esc_html( $handle ) ); echo '</p></div>'; } ); } else { throw new Exception( sprintf( 'Script with handle %s had a dependency on itself. This is an indicator that your JS code has a circular dependency that can cause bugs.', $handle ) ); } } wp_register_script( $handle, $script_data['src'], apply_filters( 'woocommerce_blocks_register_script_dependencies', $script_data['dependencies'], $handle ), $script_data['version'], true ); if ( $has_i18n && function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( $handle, 'woocommerce', $this->package->get_path( 'languages' ) ); } } /** * Registers a style according to `wp_register_style`. * * @since 2.5.0 * @since 2.6.0 Change src to be relative source. * * @param string $handle Name of the stylesheet. Should be unique. * @param string $relative_src Relative source of the stylesheet to the plugin path. * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. * @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like * 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. */ public function register_style( $handle, $relative_src, $deps = [], $media = 'all' ) { $filename = str_replace( plugins_url( '/', __DIR__ ), '', $relative_src ); $src = $this->get_asset_url( $relative_src ); $ver = $this->get_file_version( $filename ); wp_register_style( $handle, $src, $deps, $ver, $media ); } /** * Returns the appropriate asset path for loading either legacy builds or * current builds. * * @param string $filename Filename for asset path (without extension). * @param string $type File type (.css or .js). * * @return string The generated path. */ public function get_block_asset_build_path( $filename, $type = 'js' ) { global $wp_version; $suffix = version_compare( $wp_version, '5.3', '>=' ) ? '' : '-legacy'; return "build/$filename$suffix.$type"; } /** * Adds an inline script, once. * * @param string $handle Script handle. * @param string $script Script contents. */ public function add_inline_script( $handle, $script ) { if ( ! empty( $this->inline_scripts[ $handle ] ) && in_array( $script, $this->inline_scripts[ $handle ], true ) ) { return; } wp_add_inline_script( $handle, $script ); if ( isset( $this->inline_scripts[ $handle ] ) ) { $this->inline_scripts[ $handle ][] = $script; } else { $this->inline_scripts[ $handle ] = array( $script ); } } } packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php 0000644 00000026120 15132754524 0020271 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Assets; use Automattic\WooCommerce\Blocks\Package; use Exception; use InvalidArgumentException; /** * Class instance for registering data used on the current view session by * assets. * * Holds data registered for output on the current view session when * `wc-settings` is enqueued( directly or via dependency ) * * @since 2.5.0 */ class AssetDataRegistry { /** * Contains registered data. * * @var array */ private $data = []; /** * Lazy data is an array of closures that will be invoked just before * asset data is generated for the enqueued script. * * @var array */ private $lazy_data = []; /** * Asset handle for registered data. * * @var string */ private $handle = 'wc-settings'; /** * Asset API interface for various asset registration. * * @var API */ private $api; /** * Constructor * * @param Api $asset_api Asset API interface for various asset registration. */ public function __construct( Api $asset_api ) { $this->api = $asset_api; $this->init(); } /** * Hook into WP asset registration for enqueueing asset data. */ protected function init() { add_action( 'init', array( $this, 'register_data_script' ) ); add_action( 'wp_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 ); add_action( 'admin_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 ); } /** * Exposes core data via the wcSettings global. This data is shared throughout the client. * * Settings that are used by various components or multiple blocks should be added here. Note, that settings here are * global so be sure not to add anything heavy if possible. * * @return array An array containing core data. */ protected function get_core_data() { return [ 'adminUrl' => admin_url(), 'countries' => WC()->countries->get_countries(), 'currency' => $this->get_currency_data(), 'currentUserIsAdmin' => current_user_can( 'manage_woocommerce' ), 'homeUrl' => esc_url( home_url( '/' ) ), 'locale' => $this->get_locale_data(), 'orderStatuses' => $this->get_order_statuses(), 'placeholderImgSrc' => wc_placeholder_img_src(), 'siteTitle' => get_bloginfo( 'name' ), 'storePages' => $this->get_store_pages(), 'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ), 'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '', 'wpLoginUrl' => wp_login_url(), 'wpVersion' => get_bloginfo( 'version' ), ]; } /** * Get currency data to include in settings. * * @return array */ protected function get_currency_data() { $currency = get_woocommerce_currency(); return [ 'code' => $currency, 'precision' => wc_get_price_decimals(), 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ) ), 'symbolPosition' => get_option( 'woocommerce_currency_pos' ), 'decimalSeparator' => wc_get_price_decimal_separator(), 'thousandSeparator' => wc_get_price_thousand_separator(), 'priceFormat' => html_entity_decode( get_woocommerce_price_format() ), ]; } /** * Get locale data to include in settings. * * @return array */ protected function get_locale_data() { global $wp_locale; return [ 'siteLocale' => get_locale(), 'userLocale' => get_user_locale(), 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), ]; } /** * Get store pages to include in settings. * * @return array */ protected function get_store_pages() { return array_map( [ $this, 'format_page_resource' ], [ 'myaccount' => wc_get_page_id( 'myaccount' ), 'shop' => wc_get_page_id( 'shop' ), 'cart' => wc_get_page_id( 'cart' ), 'checkout' => wc_get_page_id( 'checkout' ), 'privacy' => wc_privacy_policy_page_id(), 'terms' => wc_terms_and_conditions_page_id(), ] ); } /** * Format a page object into a standard array of data. * * @param WP_Post|int $page Page object or ID. * @return array */ protected function format_page_resource( $page ) { if ( is_numeric( $page ) && $page > 0 ) { $page = get_post( $page ); } if ( ! is_a( $page, '\WP_Post' ) || 'publish' !== $page->post_status ) { return [ 'id' => 0, 'title' => '', 'permalink' => false, ]; } return [ 'id' => $page->ID, 'title' => $page->post_title, 'permalink' => get_permalink( $page->ID ), ]; } /** * Returns block-related data for enqueued wc-settings script. * Format order statuses by removing a leading 'wc-' if present. * * @return array formatted statuses. */ protected function get_order_statuses() { $formatted_statuses = array(); foreach ( wc_get_order_statuses() as $key => $value ) { $formatted_key = preg_replace( '/^wc-/', '', $key ); $formatted_statuses[ $formatted_key ] = $value; } return $formatted_statuses; } /** * Used for on demand initialization of asset data and registering it with * the internal data registry. * * Note: core data will overwrite any externally registered data via the api. */ protected function initialize_core_data() { /** * Low level hook for registration of new data late in the cycle. This is deprecated. * Instead, use the data api: * Automattic\WooCommerce\Blocks\Package::container() * ->get( Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class ) * ->add( $key, $value ) */ $settings = apply_filters( 'woocommerce_shared_settings', $this->data ); // Surface a deprecation warning in the error console. if ( has_filter( 'woocommerce_shared_settings' ) ) { $error_handle = 'deprecated-shared-settings-error'; $error_message = '`woocommerce_shared_settings` filter in Blocks is deprecated. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/contributors/block-assets.md'; // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( $error_handle, '' ); wp_enqueue_script( $error_handle ); wp_add_inline_script( $error_handle, sprintf( 'console.warn( "%s" );', $error_message ) ); } // note this WILL wipe any data already registered to these keys because they are protected. $this->data = array_replace_recursive( $settings, $this->get_core_data() ); } /** * Loops through each registered lazy data callback and adds the returned * value to the data array. * * This method is executed right before preparing the data for printing to * the rendered screen. * * @return void */ protected function execute_lazy_data() { foreach ( $this->lazy_data as $key => $callback ) { $this->data[ $key ] = $callback(); } } /** * Exposes private registered data to child classes. * * @return array The registered data on the private data property */ protected function get() { return $this->data; } /** * Allows checking whether a key exists. * * @param string $key The key to check if exists. * @return bool Whether the key exists in the current data registry. */ public function exists( $key ) { return array_key_exists( $key, $this->data ); } /** * Interface for adding data to the registry. * * You can only register data that is not already in the registry identified by the given key. If there is a * duplicate found, unless $ignore_duplicates is true, an exception will be thrown. * * @param string $key The key used to reference the data being registered. * @param mixed $data If not a function, registered to the registry as is. If a function, then the * callback is invoked right before output to the screen. * @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists. * If false, duplicate data will cause an exception. * * @throws InvalidArgumentException Only throws when site is in debug mode. Always logs the error. */ public function add( $key, $data, $check_key_exists = false ) { if ( $check_key_exists && $this->exists( $key ) ) { return; } try { $this->add_data( $key, $data ); } catch ( Exception $e ) { if ( $this->debug() ) { // bubble up. throw $e; } wc_caught_exception( $e, __METHOD__, [ $key, $data ] ); } } /** * Hydrate from API. * * @param string $path REST API path to preload. */ public function hydrate_api_request( $path ) { if ( ! isset( $this->data['preloadedApiRequests'] ) ) { $this->data['preloadedApiRequests'] = []; } if ( ! isset( $this->data['preloadedApiRequests'][ $path ] ) ) { $this->data['preloadedApiRequests'] = rest_preload_api_request( $this->data['preloadedApiRequests'], $path ); } } /** * Adds a page permalink to the data registry. * * @param integer $page_id Page ID to add to the registry. */ public function register_page_id( $page_id ) { $permalink = $page_id ? get_permalink( $page_id ) : false; if ( $permalink ) { $this->data[ 'page-' . $page_id ] = $permalink; } } /** * Callback for registering the data script via WordPress API. * * @return void */ public function register_data_script() { $this->api->register_script( $this->handle, 'build/wc-settings.js', [], true ); } /** * Callback for enqueuing asset data via the WP api. * * Note: while this is hooked into print/admin_print_scripts, it still only * happens if the script attached to `wc-settings` handle is enqueued. This * is done to allow for any potentially expensive data generation to only * happen for routes that need it. */ public function enqueue_asset_data() { if ( wp_script_is( $this->handle, 'enqueued' ) ) { $this->initialize_core_data(); $this->execute_lazy_data(); $data = rawurlencode( wp_json_encode( $this->data ) ); wp_add_inline_script( $this->handle, "var wcSettings = wcSettings || JSON.parse( decodeURIComponent( '" . esc_js( $data ) . "' ) );", 'before' ); } } /** * See self::add() for docs. * * @param string $key Key for the data. * @param mixed $data Value for the data. * * @throws InvalidArgumentException If key is not a string or already * exists in internal data cache. */ protected function add_data( $key, $data ) { if ( ! is_string( $key ) ) { if ( $this->debug() ) { throw new InvalidArgumentException( 'Key for the data being registered must be a string' ); } } if ( isset( $this->data[ $key ] ) ) { if ( $this->debug() ) { throw new InvalidArgumentException( 'Overriding existing data with an already registered key is not allowed' ); } return; } if ( \is_callable( $data ) ) { $this->lazy_data[ $key ] = $data; return; } $this->data[ $key ] = $data; } /** * Exposes whether the current site is in debug mode or not. * * @return boolean True means the site is in debug mode. */ protected function debug() { return defined( 'WP_DEBUG' ) && WP_DEBUG; } } packages/woocommerce-blocks/src/Registry/SharedType.php 0000644 00000001344 15132754524 0017306 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Registry; /** * A definition for the SharedType dependency type. * * @since 2.5.0 */ class SharedType extends AbstractDependencyType { /** * Holds a cached instance of the value stored (or returned) internally. * * @var mixed */ private $shared_instance; /** * Returns the internal stored and shared value after initial generation. * * @param Container $container An instance of the dependency injection * container. * * @return mixed */ public function get( Container $container ) { if ( empty( $this->shared_instance ) ) { $this->shared_instance = $this->resolve_value( $container ); } return $this->shared_instance; } } packages/woocommerce-blocks/src/Registry/Container.php 0000644 00000006025 15132754524 0017161 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Registry; use Closure; use Exception; /** * A simple Dependency Injection Container * * This is used to manage dependencies used throughout the plugin. * * @since 2.5.0 */ class Container { /** * A map of Dependency Type objects used to resolve dependencies. * * @var AbstractDependencyType[] */ private $registry = []; /** * Public api for adding a factory to the container. * * Factory dependencies will have the instantiation callback invoked * every time the dependency is requested. * * Typical Usage: * * ``` * $container->register( MyClass::class, $container->factory( $mycallback ) ); * ``` * * @param Closure $instantiation_callback This will be invoked when the * dependency is required. It will * receive an instance of this * container so the callback can * retrieve dependencies from the * container. * * @return FactoryType An instance of the FactoryType dependency. */ public function factory( Closure $instantiation_callback ) { return new FactoryType( $instantiation_callback ); } /** * Interface for registering a new dependency with the container. * * By default, the $value will be added as a shared dependency. This means * that it will be a single instance shared among any other classes having * that dependency. * * If you want a new instance every time it's required, then wrap the value * in a call to the factory method (@see Container::factory for example) * * Note: Currently if the provided id already is registered in the container, * the provided value is ignored. * * @param string $id A unique string identifier for the provided value. * Typically it's the fully qualified name for the * dependency. * @param mixed $value The value for the dependency. Typically, this is a * closure that will create the class instance needed. */ public function register( $id, $value ) { if ( empty( $this->registry[ $id ] ) ) { if ( ! $value instanceof FactoryType ) { $value = new SharedType( $value ); } $this->registry[ $id ] = $value; } } /** * Interface for retrieving the dependency stored in the container for the * given identifier. * * @param string $id The identifier for the dependency being retrieved. * @throws Exception If there is no dependency for the given identifier in * the container. * * @return mixed Typically a class instance. */ public function get( $id ) { if ( ! isset( $this->registry[ $id ] ) ) { // this is a developer facing exception, hence it is not localized. throw new Exception( sprintf( 'Cannot construct an instance of %s because it has not been registered.', $id ) ); } return $this->registry[ $id ]->get( $this ); } } packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php 0000644 00000002362 15132754524 0021643 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Registry; /** * An abstract class for dependency types. * * Dependency types are instances of a dependency used by the * Dependency Injection Container for storing dependencies to invoke as they * are needed. * * @since 2.5.0 */ abstract class AbstractDependencyType { /** * Holds a callable or value provided for this type. * * @var mixed */ private $callable_or_value; /** * Constructor * * @param mixed $callable_or_value A callable or value for the dependency * type instance. */ public function __construct( $callable_or_value ) { $this->callable_or_value = $callable_or_value; } /** * Resolver for the internal dependency value. * * @param Container $container The Dependency Injection Container. * * @return mixed */ protected function resolve_value( Container $container ) { $callback = $this->callable_or_value; return \is_callable( $callback ) ? $callback( $container ) : $callback; } /** * Retrieves the value stored internally for this DependencyType * * @param Container $container The Dependency Injection Container. * * @return void */ abstract public function get( Container $container ); } packages/woocommerce-blocks/src/Registry/FactoryType.php 0000644 00000000771 15132754524 0017512 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Registry; /** * Definition for the FactoryType dependency type. * * @since 2.5.0 */ class FactoryType extends AbstractDependencyType { /** * Invokes and returns the value from the stored internal callback. * * @param Container $container An instance of the dependency injection * container. * * @return mixed */ public function get( Container $container ) { return $this->resolve_value( $container ); } } packages/woocommerce-blocks/src/AssetsController.php 0000644 00000013161 15132754524 0016734 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry as AssetDataRegistry; /** * AssetsController class. * * @since 5.0.0 * @internal */ final class AssetsController { /** * Asset API interface for various asset registration. * * @var AssetApi */ private $api; /** * Constructor. * * @param AssetApi $asset_api Asset API interface for various asset registration. */ public function __construct( AssetApi $asset_api ) { $this->api = $asset_api; $this->init(); } /** * Initialize class features. */ protected function init() { add_action( 'init', array( $this, 'register_assets' ) ); add_action( 'body_class', array( $this, 'add_theme_body_class' ), 1 ); add_action( 'admin_body_class', array( $this, 'add_theme_body_class' ), 1 ); add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 ); } /** * Register block scripts & styles. */ public function register_assets() { $this->register_style( 'wc-blocks-vendors-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-vendors-style', 'css' ), __DIR__ ) ); $this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), __DIR__ ), [ 'wp-edit-blocks' ], 'all', true ); $this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-style', 'css' ), __DIR__ ), [ 'wc-blocks-vendors-style' ], 'all', true ); $this->api->register_script( 'wc-blocks-middleware', 'build/wc-blocks-middleware.js', [], false ); $this->api->register_script( 'wc-blocks-data-store', 'build/wc-blocks-data.js', [ 'wc-blocks-middleware' ] ); $this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), [], false ); $this->api->register_script( 'wc-blocks-registry', 'build/wc-blocks-registry.js', [], false ); $this->api->register_script( 'wc-blocks', $this->api->get_block_asset_build_path( 'wc-blocks' ), [ 'wc-blocks-vendors' ], false ); $this->api->register_script( 'wc-blocks-shared-context', 'build/wc-blocks-shared-context.js', [] ); $this->api->register_script( 'wc-blocks-shared-hocs', 'build/wc-blocks-shared-hocs.js', [], false ); // The price package is shared externally so has no blocks prefix. $this->api->register_script( 'wc-price-format', 'build/price-format.js', [], false ); if ( Package::feature()->is_feature_plugin_build() ) { $this->api->register_script( 'wc-blocks-checkout', 'build/blocks-checkout.js', [] ); } wp_add_inline_script( 'wc-blocks-middleware', " var wcBlocksMiddlewareConfig = { storeApiNonce: '" . esc_js( wp_create_nonce( 'wc_store_api' ) ) . "', wcStoreApiNonceTimestamp: '" . esc_js( time() ) . "' }; ", 'before' ); } /** * Add body classes to the frontend and within admin. * * @param string|array $classes Array or string of CSS classnames. * @return string|array Modified classnames. */ public function add_theme_body_class( $classes ) { $class = 'theme-' . get_template(); if ( is_array( $classes ) ) { $classes[] = $class; } else { $classes .= ' ' . $class . ' '; } return $classes; } /** * Get the file modified time as a cache buster if we're in dev mode. * * @param string $file Local path to the file. * @return string The cache buster value to use for the given file. */ protected function get_file_version( $file ) { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( \Automattic\WooCommerce\Blocks\Package::get_path() . $file ) ) { return filemtime( \Automattic\WooCommerce\Blocks\Package::get_path() . $file ); } return \Automattic\WooCommerce\Blocks\Package::get_version(); } /** * Registers a style according to `wp_register_style`. * * @param string $handle Name of the stylesheet. Should be unique. * @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. * @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like * 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $rtl Optional. Whether or not to register RTL styles. */ protected function register_style( $handle, $src, $deps = [], $media = 'all', $rtl = false ) { $filename = str_replace( plugins_url( '/', __DIR__ ), '', $src ); $ver = self::get_file_version( $filename ); wp_register_style( $handle, $src, $deps, $ver, $media ); if ( $rtl ) { wp_style_add_data( $handle, 'rtl', 'replace' ); } } /** * Update block style dependencies after they have been registered. */ public function update_block_style_dependencies() { $wp_styles = wp_styles(); $style = $wp_styles->query( 'wc-blocks-style', 'registered' ); if ( ! $style ) { return; } // In WC < 5.5, `woocommerce-general` is not registered in block editor // screens, so we don't add it as a dependency if it's not registered. // In WC >= 5.5, `woocommerce-general` is registered on `admin_enqueue_scripts`, // so we need to check if it's registered here instead of on `init`. if ( wp_style_is( 'woocommerce-general', 'registered' ) && ! in_array( 'woocommerce-general', $style->deps, true ) ) { $style->deps[] = 'woocommerce-general'; } } } packages/woocommerce-blocks/src/Installer.php 0000644 00000006347 15132754524 0015373 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; /** * Installer class. * Handles installation of Blocks plugin dependencies. * * @internal */ class Installer { /** * Constructor */ public function __construct() { $this->init(); } /** * Installation tasks ran on admin_init callback. */ public function install() { $this->maybe_create_tables(); } /** * Initialize class features. */ protected function init() { add_action( 'admin_init', array( $this, 'install' ) ); } /** * Set up the database tables which the plugin needs to function. */ public function maybe_create_tables() { global $wpdb; $schema_version = 260; $db_schema_version = (int) get_option( 'wc_blocks_db_schema_version', 0 ); if ( $db_schema_version >= $schema_version && 0 !== $db_schema_version ) { return; } $show_errors = $wpdb->hide_errors(); $table_name = $wpdb->prefix . 'wc_reserved_stock'; $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : ''; $exists = $this->maybe_create_table( $wpdb->prefix . 'wc_reserved_stock', " CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `stock_quantity` double NOT NULL DEFAULT 0, `timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`order_id`, `product_id`) ) $collate; " ); if ( $show_errors ) { $wpdb->show_errors(); } if ( ! $exists ) { return $this->add_create_table_notice( $table_name ); } // Update succeeded. This is only updated when successful and validated. // $schema_version should be incremented when changes to schema are made within this method. update_option( 'wc_blocks_db_schema_version', $schema_version ); } /** * Create database table, if it doesn't already exist. * * Based on admin/install-helper.php maybe_create_table function. * * @param string $table_name Database table name. * @param string $create_sql Create database table SQL. * @return bool False on error, true if already exists or success. */ protected function maybe_create_table( $table_name, $create_sql ) { global $wpdb; if ( in_array( $table_name, $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ), 0 ), true ) ) { return true; } $wpdb->query( $create_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return in_array( $table_name, $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ), 0 ), true ); } /** * Add a notice if table creation fails. * * @param string $table_name Name of the missing table. */ protected function add_create_table_notice( $table_name ) { add_action( 'admin_notices', function() use ( $table_name ) { echo '<div class="error"><p>'; printf( /* translators: %1$s table name, %2$s database user, %3$s database name. */ esc_html__( 'WooCommerce %1$s table creation failed. Does the %2$s user have CREATE privileges on the %3$s database?', 'woocommerce' ), '<code>' . esc_html( $table_name ) . '</code>', '<code>' . esc_html( DB_USER ) . '</code>', '<code>' . esc_html( DB_NAME ) . '</code>' ); echo '</p></div>'; } ); } } packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php 0000644 00000001563 15132754524 0022203 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Integrations; /** * Integration.Interface * * Integrations must use this interface when registering themselves with blocks, */ interface IntegrationInterface { /** * The name of the integration. * * @return string */ public function get_name(); /** * When called invokes any initialization/setup for the integration. */ public function initialize(); /** * Returns an array of script handles to enqueue in the frontend context. * * @return string[] */ public function get_script_handles(); /** * Returns an array of script handles to enqueue in the editor context. * * @return string[] */ public function get_editor_script_handles(); /** * An array of key, value pairs of data made available to the block on the client side. * * @return array */ public function get_script_data(); } packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php 0000644 00000012411 15132754524 0022105 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Integrations; /** * Class used for tracking registered integrations with various Block types. */ class IntegrationRegistry { /** * Integration identifier is used to construct hook names and is given when the integration registry is initialized. * * @var string */ protected $registry_identifier = ''; /** * Registered integrations, as `$name => $instance` pairs. * * @var IntegrationInterface[] */ protected $registered_integrations = []; /** * Initializes all registered integrations. * * Integration identifier is used to construct hook names and is given when the integration registry is initialized. * * @param string $registry_identifier Identifier for this registry. */ public function initialize( $registry_identifier = '' ) { if ( $registry_identifier ) { $this->registry_identifier = $registry_identifier; } if ( empty( $this->registry_identifier ) ) { _doing_it_wrong( __METHOD__, esc_html( __( 'Integration registry requires an identifier.', 'woocommerce' ) ) ); return false; } /** * Hook: integration_registration. * * Runs before integrations are initialized allowing new integration to be registered for use. This should be * used as the primary hook for integrations to include their scripts, styles, and other code extending the * blocks. * * @param IntegrationRegistry $this Instance of the IntegrationRegistry class which exposes the IntegrationRegistry::register() method. */ do_action( 'woocommerce_blocks_' . $this->registry_identifier . '_registration', $this ); foreach ( $this->get_all_registered() as $registered_integration ) { $registered_integration->initialize(); } } /** * Registers an integration. * * @param IntegrationInterface $integration An instance of IntegrationInterface. * * @return boolean True means registered successfully. */ public function register( IntegrationInterface $integration ) { $name = $integration->get_name(); if ( $this->is_registered( $name ) ) { /* translators: %s: Integration name. */ _doing_it_wrong( __METHOD__, esc_html( sprintf( __( '"%s" is already registered.', 'woocommerce' ), $name ) ) ); return false; } $this->registered_integrations[ $name ] = $integration; return true; } /** * Checks if an integration is already registered. * * @param string $name Integration name. * @return bool True if the integration is registered, false otherwise. */ public function is_registered( $name ) { return isset( $this->registered_integrations[ $name ] ); } /** * Un-register an integration. * * @param string|IntegrationInterface $name Integration name, or alternatively a IntegrationInterface instance. * @return boolean|IntegrationInterface Returns the unregistered integration instance if unregistered successfully. */ public function unregister( $name ) { if ( $name instanceof IntegrationInterface ) { $name = $name->get_name(); } if ( ! $this->is_registered( $name ) ) { /* translators: %s: Integration name. */ _doing_it_wrong( __METHOD__, esc_html( sprintf( __( 'Integration "%s" is not registered.', 'woocommerce' ), $name ) ) ); return false; } $unregistered = $this->registered_integrations[ $name ]; unset( $this->registered_integrations[ $name ] ); return $unregistered; } /** * Retrieves a registered Integration by name. * * @param string $name Integration name. * @return IntegrationInterface|null The registered integration, or null if it is not registered. */ public function get_registered( $name ) { return $this->is_registered( $name ) ? $this->registered_integrations[ $name ] : null; } /** * Retrieves all registered integrations. * * @return IntegrationInterface[] */ public function get_all_registered() { return $this->registered_integrations; } /** * Gets an array of all registered integration's script handles for the editor. * * @return string[] */ public function get_all_registered_editor_script_handles() { $script_handles = []; $registered_integrations = $this->get_all_registered(); foreach ( $registered_integrations as $registered_integration ) { $script_handles = array_merge( $script_handles, $registered_integration->get_editor_script_handles() ); } return array_unique( array_filter( $script_handles ) ); } /** * Gets an array of all registered integration's script handles. * * @return string[] */ public function get_all_registered_script_handles() { $script_handles = []; $registered_integrations = $this->get_all_registered(); foreach ( $registered_integrations as $registered_integration ) { $script_handles = array_merge( $script_handles, $registered_integration->get_script_handles() ); } return array_unique( array_filter( $script_handles ) ); } /** * Gets an array of all registered integration's script data. * * @return array */ public function get_all_registered_script_data() { $script_data = []; $registered_integrations = $this->get_all_registered(); foreach ( $registered_integrations as $registered_integration ) { $script_data[ $registered_integration->get_name() . '_data' ] = $registered_integration->get_script_data(); } return array_filter( $script_data ); } } packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php 0000644 00000000335 15132754524 0017714 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * PriceFilter class. */ class PriceFilter extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'price-filter'; } packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php 0000644 00000012411 15132754524 0020737 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * FeaturedCategory class. */ class FeaturedCategory extends AbstractDynamicBlock { /** * Block name. * * @var string */ protected $block_name = 'featured-category'; /** * Default attribute values, should match what's set in JS `registerBlockType`. * * @var array */ protected $defaults = array( 'align' => 'none', 'contentAlign' => 'center', 'dimRatio' => 50, 'focalPoint' => false, 'height' => false, 'mediaId' => 0, 'mediaSrc' => '', 'showDesc' => true, ); /** * Render the Featured Category block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { $id = absint( isset( $attributes['categoryId'] ) ? $attributes['categoryId'] : 0 ); $category = get_term( $id, 'product_cat' ); if ( ! $category || is_wp_error( $category ) ) { return ''; } $attributes = wp_parse_args( $attributes, $this->defaults ); $attributes['height'] = $attributes['height'] ? $attributes['height'] : wc_get_theme_support( 'featured_block::default_height', 500 ); $title = sprintf( '<h2 class="wc-block-featured-category__title">%s</h2>', wp_kses_post( $category->name ) ); $desc_str = sprintf( '<div class="wc-block-featured-category__description">%s</div>', wc_format_content( wp_kses_post( $category->description ) ) ); $output = sprintf( '<div class="%1$s" style="%2$s">', esc_attr( $this->get_classes( $attributes ) ), esc_attr( $this->get_styles( $attributes, $category ) ) ); $output .= '<div class="wc-block-featured-category__wrapper">'; $output .= $title; if ( $attributes['showDesc'] ) { $output .= $desc_str; } $output .= '<div class="wc-block-featured-category__link">' . $content . '</div>'; $output .= '</div>'; $output .= '</div>'; return $output; } /** * Get the styles for the wrapper element (background image, color). * * @param array $attributes Block attributes. Default empty array. * @param \WP_Term $category Term object. * @return string */ public function get_styles( $attributes, $category ) { $style = ''; $image_size = 'large'; if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) { $image_size = 'full'; } if ( $attributes['mediaId'] ) { $image = wp_get_attachment_image_url( $attributes['mediaId'], $image_size ); } else { $image = $this->get_image( $category, $image_size ); } if ( ! empty( $image ) ) { $style .= sprintf( 'background-image:url(%s);', esc_url( $image ) ); } if ( isset( $attributes['customOverlayColor'] ) ) { $style .= sprintf( 'background-color:%s;', esc_attr( $attributes['customOverlayColor'] ) ); } if ( isset( $attributes['height'] ) ) { $style .= sprintf( 'min-height:%dpx;', intval( $attributes['height'] ) ); } if ( is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] ) ) { $style .= sprintf( 'background-position: %s%% %s%%', $attributes['focalPoint']['x'] * 100, $attributes['focalPoint']['y'] * 100 ); } return $style; } /** * Get class names for the block container. * * @param array $attributes Block attributes. Default empty array. * @return string */ public function get_classes( $attributes ) { $classes = array( 'wc-block-' . $this->block_name ); if ( isset( $attributes['align'] ) ) { $classes[] = "align{$attributes['align']}"; } if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) { $classes[] = 'has-background-dim'; if ( 50 !== $attributes['dimRatio'] ) { $classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 ); } } if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) { $classes[] = "has-{$attributes['contentAlign']}-content"; } if ( isset( $attributes['overlayColor'] ) ) { $classes[] = "has-{$attributes['overlayColor']}-background-color"; } if ( isset( $attributes['className'] ) ) { $classes[] = $attributes['className']; } return implode( ' ', $classes ); } /** * Returns the main product image URL. * * @param \WP_Term $category Term object. * @param string $size Image size, defaults to 'full'. * @return string */ public function get_image( $category, $size = 'full' ) { $image = ''; $image_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); if ( $image_id ) { $image = wp_get_attachment_image_url( $image_id, $size ); } return $image; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true ); $this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true ); } } packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php 0000644 00000026124 15132754524 0021136 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductCategories class. */ class ProductCategories extends AbstractDynamicBlock { /** * Block name. * * @var string */ protected $block_name = 'product-categories'; /** * Default attribute values, should match what's set in JS `registerBlockType`. * * @var array */ protected $defaults = array( 'hasCount' => true, 'hasImage' => false, 'hasEmpty' => false, 'isDropdown' => false, 'isHierarchical' => true, ); /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array_merge( parent::get_block_type_attributes(), array( 'align' => $this->get_schema_align(), 'className' => $this->get_schema_string(), 'hasCount' => $this->get_schema_boolean( true ), 'hasImage' => $this->get_schema_boolean( false ), 'hasEmpty' => $this->get_schema_boolean( false ), 'isDropdown' => $this->get_schema_boolean( false ), 'isHierarchical' => $this->get_schema_boolean( true ), ) ); } /** * Render the Product Categories List block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { $uid = uniqid( 'product-categories-' ); $categories = $this->get_categories( $attributes ); if ( empty( $categories ) ) { return ''; } if ( ! empty( $content ) ) { // Deal with legacy attributes (before this was an SSR block) that differ from defaults. if ( strstr( $content, 'data-has-count="false"' ) ) { $attributes['hasCount'] = false; } if ( strstr( $content, 'data-is-dropdown="true"' ) ) { $attributes['isDropdown'] = true; } if ( strstr( $content, 'data-is-hierarchical="false"' ) ) { $attributes['isHierarchical'] = false; } if ( strstr( $content, 'data-has-empty="true"' ) ) { $attributes['hasEmpty'] = true; } } $classes = $this->get_container_classes( $attributes ); $output = '<div class="' . esc_attr( $classes ) . '">'; $output .= ! empty( $attributes['isDropdown'] ) ? $this->renderDropdown( $categories, $attributes, $uid ) : $this->renderList( $categories, $attributes, $uid ); $output .= '</div>'; return $output; } /** * Get the list of classes to apply to this block. * * @param array $attributes Block attributes. Default empty array. * @return string space-separated list of classes. */ protected function get_container_classes( $attributes = array() ) { $classes = array( 'wc-block-product-categories' ); if ( isset( $attributes['align'] ) ) { $classes[] = "align{$attributes['align']}"; } if ( ! empty( $attributes['className'] ) ) { $classes[] = $attributes['className']; } if ( $attributes['isDropdown'] ) { $classes[] = 'is-dropdown'; } else { $classes[] = 'is-list'; } return implode( ' ', $classes ); } /** * Get categories (terms) from the db. * * @param array $attributes Block attributes. Default empty array. * @return array */ protected function get_categories( $attributes ) { $hierarchical = wc_string_to_bool( $attributes['isHierarchical'] ); $categories = get_terms( 'product_cat', [ 'hide_empty' => ! $attributes['hasEmpty'], 'pad_counts' => true, 'hierarchical' => true, ] ); if ( ! is_array( $categories ) || empty( $categories ) ) { return []; } // This ensures that no categories with a product count of 0 is rendered. if ( ! $attributes['hasEmpty'] ) { $categories = array_filter( $categories, function( $category ) { return 0 !== $category->count; } ); } return $hierarchical ? $this->build_category_tree( $categories ) : $categories; } /** * Build hierarchical tree of categories. * * @param array $categories List of terms. * @return array */ protected function build_category_tree( $categories ) { $categories_by_parent = []; foreach ( $categories as $category ) { if ( ! isset( $categories_by_parent[ 'cat-' . $category->parent ] ) ) { $categories_by_parent[ 'cat-' . $category->parent ] = []; } $categories_by_parent[ 'cat-' . $category->parent ][] = $category; } $tree = $categories_by_parent['cat-0']; unset( $categories_by_parent['cat-0'] ); foreach ( $tree as $category ) { if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) { $category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent ); } } return $tree; } /** * Build hierarchical tree of categories by appending children in the tree. * * @param array $categories List of terms. * @param array $categories_by_parent List of terms grouped by parent. * @return array */ protected function fill_category_children( $categories, $categories_by_parent ) { foreach ( $categories as $category ) { if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) { $category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent ); } } return $categories; } /** * Render the category list as a dropdown. * * @param array $categories List of terms. * @param array $attributes Block attributes. Default empty array. * @param int $uid Unique ID for the rendered block, used for HTML IDs. * @return string Rendered output. */ protected function renderDropdown( $categories, $attributes, $uid ) { $aria_label = empty( $attributes['hasCount'] ) ? __( 'List of categories', 'woocommerce' ) : __( 'List of categories with their product counts', 'woocommerce' ); $output = ' <div class="wc-block-product-categories__dropdown"> <label class="screen-reader-text" for="' . esc_attr( $uid ) . '-select" > ' . esc_html__( 'Select a category', 'woocommerce' ) . ' </label> <select aria-label="' . esc_attr( $aria_label ) . '" id="' . esc_attr( $uid ) . '-select"> <option value="false" hidden> ' . esc_html__( 'Select a category', 'woocommerce' ) . ' </option> ' . $this->renderDropdownOptions( $categories, $attributes, $uid ) . ' </select> </div> <button type="button" class="wc-block-product-categories__button" aria-label="' . esc_html__( 'Go to category', 'woocommerce' ) . '" onclick="const url = document.getElementById( \'' . esc_attr( $uid ) . '-select\' ).value; if ( \'false\' !== url ) document.location.href = url;" > <svg aria-hidden="true" role="img" focusable="false" class="dashicon dashicons-arrow-right-alt2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" > <path d="M6 15l5-5-5-5 1-2 7 7-7 7z" /> </svg> </button> '; return $output; } /** * Render dropdown options list. * * @param array $categories List of terms. * @param array $attributes Block attributes. Default empty array. * @param int $uid Unique ID for the rendered block, used for HTML IDs. * @param int $depth Current depth. * @return string Rendered output. */ protected function renderDropdownOptions( $categories, $attributes, $uid, $depth = 0 ) { $output = ''; foreach ( $categories as $category ) { $output .= ' <option value="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '"> ' . str_repeat( '−', $depth ) . ' ' . esc_html( $category->name ) . ' ' . $this->getCount( $category, $attributes ) . ' </option> ' . ( ! empty( $category->children ) ? $this->renderDropdownOptions( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . ' '; } return $output; } /** * Render the category list as a list. * * @param array $categories List of terms. * @param array $attributes Block attributes. Default empty array. * @param int $uid Unique ID for the rendered block, used for HTML IDs. * @param int $depth Current depth. * @return string Rendered output. */ protected function renderList( $categories, $attributes, $uid, $depth = 0 ) { $classes = [ 'wc-block-product-categories-list', 'wc-block-product-categories-list--depth-' . absint( $depth ), ]; if ( ! empty( $attributes['hasImage'] ) ) { $classes[] = 'wc-block-product-categories-list--has-images'; } $output = '<ul class="' . esc_attr( implode( ' ', $classes ) ) . '">' . $this->renderListItems( $categories, $attributes, $uid, $depth ) . '</ul>'; return $output; } /** * Render a list of terms. * * @param array $categories List of terms. * @param array $attributes Block attributes. Default empty array. * @param int $uid Unique ID for the rendered block, used for HTML IDs. * @param int $depth Current depth. * @return string Rendered output. */ protected function renderListItems( $categories, $attributes, $uid, $depth = 0 ) { $output = ''; foreach ( $categories as $category ) { $output .= ' <li class="wc-block-product-categories-list-item"> <a href="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '">' . $this->get_image_html( $category, $attributes ) . esc_html( $category->name ) . '</a> ' . $this->getCount( $category, $attributes ) . ' ' . ( ! empty( $category->children ) ? $this->renderList( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . ' </li> '; } return preg_replace( '/\r|\n/', '', $output ); } /** * Returns the category image html * * @param \WP_Term $category Term object. * @param array $attributes Block attributes. Default empty array. * @param string $size Image size, defaults to 'woocommerce_thumbnail'. * @return string */ public function get_image_html( $category, $attributes, $size = 'woocommerce_thumbnail' ) { if ( empty( $attributes['hasImage'] ) ) { return ''; } $image_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); if ( ! $image_id ) { return '<span class="wc-block-product-categories-list-item__image wc-block-product-categories-list-item__image--placeholder">' . wc_placeholder_img( 'woocommerce_thumbnail' ) . '</span>'; } return '<span class="wc-block-product-categories-list-item__image">' . wp_get_attachment_image( $image_id, 'woocommerce_thumbnail' ) . '</span>'; } /** * Get the count, if displaying. * * @param object $category Term object. * @param array $attributes Block attributes. Default empty array. * @return string */ protected function getCount( $category, $attributes ) { if ( empty( $attributes['hasCount'] ) ) { return ''; } if ( $attributes['isDropdown'] ) { return '(' . absint( $category->count ) . ')'; } $screen_reader_text = sprintf( /* translators: %s number of products in cart. */ _n( '%d product', '%d products', absint( $category->count ), 'woocommerce' ), absint( $category->count ) ); return '<span class="wc-block-product-categories-list-item-count">' . '<span aria-hidden="true">' . absint( $category->count ) . '</span>' . '<span class="screen-reader-text">' . esc_html( $screen_reader_text ) . '</span>' . '</span>'; } } packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php 0000644 00000001335 15132754524 0020623 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductCategory class. */ class ProductCategory extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-category'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) {} /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array_merge( parent::get_block_type_attributes(), array( 'className' => $this->get_schema_string(), 'orderby' => $this->get_schema_orderby(), 'editMode' => $this->get_schema_boolean( true ), ) ); } } packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php 0000644 00000006665 15132754524 0020266 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductSearch class. */ class ProductSearch extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'product-search'; /** * Get the frontend script handle for this block type. * * @param string $key Data to get, or default to everything. * @return null */ protected function get_block_type_script( $key = null ) { return null; } /** * Render the block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { static $instance_id = 0; $attributes = wp_parse_args( $attributes, array( 'hasLabel' => true, 'align' => '', 'className' => '', 'label' => __( 'Search', 'woocommerce' ), 'placeholder' => __( 'Search products…', 'woocommerce' ), ) ); /** * Product Search event. * * Listens for product search form submission, and on submission fires a WP Hook named * `experimental__woocommerce_blocks-product-search`. This can be used by tracking extensions such as Google * Analytics to track searches. */ $this->asset_api->add_inline_script( 'wp-hooks', " window.addEventListener( 'DOMContentLoaded', () => { const forms = document.querySelectorAll( '.wc-block-product-search form' ); for ( const form of forms ) { form.addEventListener( 'submit', ( event ) => { const field = form.querySelector( '.wc-block-product-search__field' ); if ( field && field.value ) { wp.hooks.doAction( 'experimental__woocommerce_blocks-product-search', { event: event, searchTerm: field.value } ); } } ); } } ); ", 'after' ); $input_id = 'wc-block-search__input-' . ( ++$instance_id ); $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', array_filter( [ 'wc-block-product-search', $attributes['align'] ? 'align' . $attributes['align'] : '', ] ) ), ) ); $label_markup = $attributes['hasLabel'] ? sprintf( '<label for="%s" class="wc-block-product-search__label">%s</label>', esc_attr( $input_id ), esc_html( $attributes['label'] ) ) : sprintf( '<label for="%s" class="wc-block-product-search__label screen-reader-text">%s</label>', esc_attr( $input_id ), esc_html( $attributes['label'] ) ); $input_markup = sprintf( '<input type="search" id="%s" class="wc-block-product-search__field" placeholder="%s" name="s" />', esc_attr( $input_id ), esc_attr( $attributes['placeholder'] ) ); $button_markup = sprintf( '<button type="submit" class="wc-block-product-search__button" aria-label="%s"> <svg aria-hidden="true" role="img" focusable="false" class="dashicon dashicons-arrow-right-alt2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"> <path d="M6 15l5-5-5-5 1-2 7 7-7 7z" /> </svg> </button>', esc_attr__( 'Search', 'woocommerce' ) ); $field_markup = ' <div class="wc-block-product-search__fields"> ' . $input_markup . $button_markup . ' <input type="hidden" name="post_type" value="product" /> </div> '; return sprintf( '<div %s><form role="search" method="get" action="%s">%s</form></div>', $wrapper_attributes, esc_url( home_url( '/' ) ), $label_markup . $field_markup ); } } packages/woocommerce-blocks/src/BlockTypes/CartI2.php 0000644 00000013167 15132754524 0016577 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; /** * Cart class. * * @internal */ class CartI2 extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'cart-i2'; /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Enqueue frontend assets for this block, just in time for rendering. * * @param array $attributes Any attributes that currently are available from the block. */ protected function enqueue_assets( array $attributes ) { do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' ); parent::enqueue_assets( $attributes ); do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' ); } /** * Append frontend scripts when rendering the Cart block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { // Deregister core cart scripts and styles. wp_dequeue_script( 'wc-cart' ); wp_dequeue_script( 'wc-password-strength-meter' ); wp_dequeue_script( 'selectWoo' ); wp_dequeue_style( 'select2' ); return $this->inject_html_data_attributes( $content, $attributes ); } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'shippingCountries', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); }, true ); $this->asset_data_registry->add( 'shippingStates', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); }, true ); $this->asset_data_registry->add( 'countryLocale', function() { // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. $country_locale = wc()->countries->get_country_locale(); $states = wc()->countries->get_states(); foreach ( $states as $country => $states ) { if ( empty( $states ) ) { $country_locale[ $country ]['state']['required'] = false; $country_locale[ $country ]['state']['hidden'] = true; } } return $country_locale; }, true ); $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); $this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); // Hydrate the following data depending on admin or frontend context. if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->hydrate_from_api(); } do_action( 'woocommerce_blocks_cart_enqueue_data' ); } /** * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. * * @param array $array Array of values to sort. * @return array Sorted array. */ protected function deep_sort_with_accents( $array ) { if ( ! is_array( $array ) || empty( $array ) ) { return $array; } if ( is_array( reset( $array ) ) ) { return array_map( [ $this, 'deep_sort_with_accents' ], $array ); } $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); asort( $array_without_accents ); return array_replace( $array_without_accents, $array ); } /** * Hydrate the cart block with data from the API. */ protected function hydrate_from_api() { $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); } } packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php 0000644 00000003544 15132754524 0021534 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AbstractDynamicBlock class. */ abstract class AbstractDynamicBlock extends AbstractBlock { /** * Get the frontend script handle for this block type. * * @param string $key Data to get, or default to everything. * @return null */ protected function get_block_type_script( $key = null ) { return null; } /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array(); } /** * Get the schema for the alignment property. * * @return array Property definition for align. */ protected function get_schema_align() { return array( 'type' => 'string', 'enum' => array( 'left', 'center', 'right', 'wide', 'full' ), ); } /** * Get the schema for a list of IDs. * * @return array Property definition for a list of numeric ids. */ protected function get_schema_list_ids() { return array( 'type' => 'array', 'items' => array( 'type' => 'number', ), 'default' => array(), ); } /** * Get the schema for a boolean value. * * @param string $default The default value. * @return array Property definition. */ protected function get_schema_boolean( $default = true ) { return array( 'type' => 'boolean', 'default' => $default, ); } /** * Get the schema for a numeric value. * * @param string $default The default value. * @return array Property definition. */ protected function get_schema_number( $default ) { return array( 'type' => 'number', 'default' => $default, ); } /** * Get the schema for a string value. * * @param string $default The default value. * @return array Property definition. */ protected function get_schema_string( $default = '' ) { return array( 'type' => 'string', 'default' => $default, ); } } packages/woocommerce-blocks/src/BlockTypes/MiniCart.php 0000644 00000023227 15132754524 0017217 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; /** * Mini Cart class. * * @internal */ class MiniCart extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'mini-cart'; /** * Array of scripts that will be lazy loaded when interacting with the block. * * @var string[] */ protected $scripts_to_lazy_load = array(); /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { if ( is_cart() || is_checkout() ) { return; } $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { if ( is_cart() || is_checkout() ) { return; } parent::enqueue_data( $attributes ); // Hydrate the following data depending on admin or frontend context. if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->hydrate_from_api(); } $script_data = $this->asset_api->get_script_data( 'build/mini-cart-component-frontend.js' ); $num_dependencies = count( $script_data['dependencies'] ); $wp_scripts = wp_scripts(); for ( $i = 0; $i < $num_dependencies; $i++ ) { $dependency = $script_data['dependencies'][ $i ]; foreach ( $wp_scripts->registered as $script ) { if ( $script->handle === $dependency ) { $this->append_script_and_deps_src( $script ); break; } } } $this->scripts_to_lazy_load['wc-block-mini-cart-component-frontend'] = array( 'src' => $script_data['src'], 'version' => $script_data['version'], ); $this->asset_data_registry->add( 'mini_cart_block_frontend_dependencies', $this->scripts_to_lazy_load, true ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); do_action( 'woocommerce_blocks_cart_enqueue_data' ); } /** * Hydrate the cart block with data from the API. */ protected function hydrate_from_api() { $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); } /** * Returns the script data given its handle. * * @param string $handle Handle of the script. * * @return array Array containing the script data. */ protected function get_script_from_handle( $handle ) { $wp_scripts = wp_scripts(); foreach ( $wp_scripts->registered as $script ) { if ( $script->handle === $handle ) { return $script; } } return ''; } /** * Recursively appends a scripts and its dependencies into the * scripts_to_lazy_load array. * * @param string $script Array containing script data. */ protected function append_script_and_deps_src( $script ) { $wp_scripts = wp_scripts(); // This script and its dependencies have already been appended. if ( array_key_exists( $script->handle, $this->scripts_to_lazy_load ) ) { return; } if ( count( $script->deps ) > 0 ) { foreach ( $script->deps as $dep ) { if ( ! array_key_exists( $dep, $this->scripts_to_lazy_load ) ) { $dep_script = $this->get_script_from_handle( $dep ); $this->append_script_and_deps_src( $dep_script ); } } } $this->scripts_to_lazy_load[ $script->handle ] = array( 'src' => $script->src, 'version' => $script->ver, 'before' => $wp_scripts->print_inline_script( $script->handle, 'before', false ), 'after' => $wp_scripts->print_inline_script( $script->handle, 'after', false ), 'translations' => $wp_scripts->print_translations( $script->handle, false ), ); } /** * Append frontend scripts when rendering the Mini Cart block. * * @param array $attributes Block attributes. * @param string $content Block content. * * @return string Rendered block type output. */ protected function render( $attributes, $content ) { return $this->inject_html_data_attributes( $content . $this->get_markup(), $attributes ); } /** * Render the markup for the Mini Cart block. * * @return string The HTML markup. */ protected function get_markup() { if ( is_admin() || WC()->is_rest_api_request() ) { // In the editor we will display the placeholder, so no need to load // real cart data and to print the markup. return ''; } $cart_controller = new CartController(); $cart = $cart_controller->get_cart_instance(); $cart_contents_count = $cart->get_cart_contents_count(); $cart_contents = $cart->get_cart(); $cart_contents_total = $cart->get_subtotal(); if ( $cart->display_prices_including_tax() ) { $cart_contents_total += $cart->get_subtotal_tax(); } $button_text = sprintf( /* translators: %d is the number of products in the cart. */ _n( '%d item', '%d items', $cart_contents_count, 'woocommerce' ), $cart_contents_count ); $aria_label = sprintf( /* translators: %1$d is the number of products in the cart. %2$s is the cart total */ _n( '%1$d item in cart, total price of %2$s', '%1$d items in cart, total price of %2$s', $cart_contents_count, 'woocommerce' ), $cart_contents_count, wp_strip_all_tags( wc_price( $cart_contents_total ) ) ); $title = sprintf( /* translators: %d is the count of items in the cart. */ _n( 'Your cart (%d item)', 'Your cart (%d items)', $cart_contents_count, 'woocommerce' ), $cart_contents_count ); if ( is_cart() || is_checkout() ) { return '<div class="wc-block-mini-cart"> <button class="wc-block-mini-cart__button" aria-label="' . $aria_label . '" disabled>' . $button_text . '</button> </div>'; } return '<div class="wc-block-mini-cart"> <button class="wc-block-mini-cart__button" aria-label="' . $aria_label . '">' . $button_text . '</button> <div class="wc-block-mini-cart__drawer is-loading is-mobile wc-block-components-drawer__screen-overlay wc-block-components-drawer__screen-overlay--is-hidden" aria-hidden="true"> <div class="components-modal__frame wc-block-components-drawer"> <div class="components-modal__content"> <div class="components-modal__header"> <div class="components-modal__header-heading-container"> <h1 id="components-modal-header-1" class="components-modal__header-heading">' . $title . '</h1> </div> </div>' . $this->get_cart_contents_markup( $cart_contents ) . '</div> </div> </div> </div>'; } /** * Render the markup of the Cart contents. * * @param array $cart_contents Array of contents in the cart. * * @return string The HTML markup. */ protected function get_cart_contents_markup( $cart_contents ) { // Force mobile styles. return '<table class="wc-block-cart-items"> <thead> <tr class="wc-block-cart-items__header"> <th class="wc-block-cart-items__header-image"><span /></th> <th class="wc-block-cart-items__header-product"><span /></th> <th class="wc-block-cart-items__header-total"><span /></th> </tr> </thead> <tbody>' . implode( array_map( array( $this, 'get_cart_item_markup' ), $cart_contents ) ) . '</tbody> </table>'; } /** * Render the skeleton of a Cart item. * * @return string The skeleton HTML markup. */ protected function get_cart_item_markup() { return '<tr class="wc-block-cart-items__row"> <td class="wc-block-cart-item__image"> <a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a> </td> <td class="wc-block-cart-item__product"> <div class="wc-block-components-product-name"></div> <div class="wc-block-components-product-price"></div> <div class="wc-block-components-product-metadata"></div> <div class="wc-block-cart-item__quantity"> <div class="wc-block-components-quantity-selector"> <input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" /> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button> </div> <button class="wc-block-cart-item__remove-link"></button> </div> </td> <td class="wc-block-cart-item__total"> <div class="wc-block-cart-item__total-price-and-sale-badge-wrapper"> <div class="wc-block-components-product-price"></div> </div> </td> </tr>'; } } packages/woocommerce-blocks/src/BlockTypes/ProductNew.php 0000644 00000000700 15132754524 0017572 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductNew class. */ class ProductNew extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-new'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { $query_args['orderby'] = 'date'; $query_args['order'] = 'DESC'; } } packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php 0000644 00000000650 15132754524 0020567 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductTopRated class. */ class ProductTopRated extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-top-rated'; /** * Force orderby to rating. * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { $query_args['orderby'] = 'rating'; } } packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php 0000644 00000001773 15132754524 0020275 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * SingleProduct class. */ class SingleProduct extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'single-product'; /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Render the block on the frontend. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { return $this->inject_html_data_attributes( $content, $attributes ); } } packages/woocommerce-blocks/src/BlockTypes/Cart.php 0000644 00000023634 15132754524 0016404 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; /** * Cart class. * * @internal */ class Cart extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'cart'; /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Enqueue frontend assets for this block, just in time for rendering. * * @param array $attributes Any attributes that currently are available from the block. */ protected function enqueue_assets( array $attributes ) { do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' ); parent::enqueue_assets( $attributes ); do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' ); } /** * Append frontend scripts when rendering the Cart block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { // Deregister core cart scripts and styles. wp_dequeue_script( 'wc-cart' ); wp_dequeue_script( 'wc-password-strength-meter' ); wp_dequeue_script( 'selectWoo' ); wp_dequeue_style( 'select2' ); return $this->inject_html_data_attributes( $content . $this->get_skeleton(), $attributes ); } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'shippingCountries', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); }, true ); $this->asset_data_registry->add( 'shippingStates', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); }, true ); $this->asset_data_registry->add( 'countryLocale', function() { // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. $country_locale = wc()->countries->get_country_locale(); $states = wc()->countries->get_states(); foreach ( $states as $country => $states ) { if ( empty( $states ) ) { $country_locale[ $country ]['state']['required'] = false; $country_locale[ $country ]['state']['hidden'] = true; } } return $country_locale; }, true ); $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); $this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); // Hydrate the following data depending on admin or frontend context. if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->hydrate_from_api(); } do_action( 'woocommerce_blocks_cart_enqueue_data' ); } /** * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. * * @param array $array Array of values to sort. * @return array Sorted array. */ protected function deep_sort_with_accents( $array ) { if ( ! is_array( $array ) || empty( $array ) ) { return $array; } if ( is_array( reset( $array ) ) ) { return array_map( [ $this, 'deep_sort_with_accents' ], $array ); } $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); asort( $array_without_accents ); return array_replace( $array_without_accents, $array ); } /** * Hydrate the cart block with data from the API. */ protected function hydrate_from_api() { $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); } /** * Render skeleton markup for the cart block. */ protected function get_skeleton() { return ' <div class="wc-block-skeleton wc-block-components-sidebar-layout wc-block-cart wc-block-cart--is-loading wc-block-cart--skeleton hidden" aria-hidden="true"> <div class="wc-block-components-main wc-block-cart__main"> <h2 class="wc-block-components-title"><span></span></h2> <table class="wc-block-cart-items"> <thead> <tr class="wc-block-cart-items__header"> <th class="wc-block-cart-items__header-image"><span /></th> <th class="wc-block-cart-items__header-product"><span /></th> <th class="wc-block-cart-items__header-total"><span /></th> </tr> </thead> <tbody> <tr class="wc-block-cart-items__row"> <td class="wc-block-cart-item__image"> <a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a> </td> <td class="wc-block-cart-item__product"> <div class="wc-block-components-product-name"></div> <div class="wc-block-components-product-price"></div> <div class="wc-block-components-product-metadata"></div> <div class="wc-block-components-quantity-selector"> <input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" /> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button> </div> </td> <td class="wc-block-cart-item__total"> <div class="wc-block-components-product-price"></div> </td> </tr> <tr class="wc-block-cart-items__row"> <td class="wc-block-cart-item__image"> <a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a> </td> <td class="wc-block-cart-item__product"> <div class="wc-block-components-product-name"></div> <div class="wc-block-components-product-price"></div> <div class="wc-block-components-product-metadata"></div> <div class="wc-block-components-quantity-selector"> <input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" /> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button> </div> </td> <td class="wc-block-cart-item__total"> <div class="wc-block-components-product-price"></div> </td> </tr> <tr class="wc-block-cart-items__row"> <td class="wc-block-cart-item__image"> <a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a> </td> <td class="wc-block-cart-item__product"> <div class="wc-block-components-product-name"></div> <div class="wc-block-components-product-price"></div> <div class="wc-block-components-product-metadata"></div> <div class="wc-block-components-quantity-selector"> <input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" /> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button> <button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button> </div> </td> <td class="wc-block-cart-item__total"> <div class="wc-block-components-product-price"></div> </td> </tr> </tbody> </table> </div> <div class="wc-block-components-sidebar wc-block-cart__sidebar"> <div class="components-card"></div> </div> </div> ' . $this->get_skeleton_inline_script(); } } packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php 0000644 00000001404 15132754524 0020613 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AttributeFilter class. */ class AttributeFilter extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'attribute-filter'; /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'attributes', array_values( wc_get_attribute_taxonomies() ), true ); } } packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php 0000644 00000036645 15132754524 0021433 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; use Automattic\WooCommerce\Blocks\Utils\BlocksWpQuery; use Automattic\WooCommerce\Blocks\StoreApi\SchemaController; use Automattic\WooCommerce\Blocks\Package; /** * AbstractProductGrid class. */ abstract class AbstractProductGrid extends AbstractDynamicBlock { /** * Attributes. * * @var array */ protected $attributes = array(); /** * InnerBlocks content. * * @var string */ protected $content = ''; /** * Query args. * * @var array */ protected $query_args = array(); /** * Get a set of attributes shared across most of the grid blocks. * * @return array List of block attributes with type and defaults. */ protected function get_block_type_attributes() { return array( 'className' => $this->get_schema_string(), 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), 'categories' => $this->get_schema_list_ids(), 'catOperator' => array( 'type' => 'string', 'default' => 'any', ), 'contentVisibility' => $this->get_schema_content_visibility(), 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'isPreview' => $this->get_schema_boolean( false ), ); } /** * Include and render the dynamic block. * * @param array $attributes Block attributes. Default empty array. * @param string $content Block content. Default empty string. * @return string Rendered block type output. */ protected function render( $attributes = array(), $content = '' ) { $this->attributes = $this->parse_attributes( $attributes ); $this->content = $content; $this->query_args = $this->parse_query_args(); $products = array_filter( array_map( 'wc_get_product', $this->get_products() ) ); if ( ! $products ) { return ''; } /** * Product List Render event. * * Fires a WP Hook named `experimental__woocommerce_blocks-product-list-render` on render so that the client * can add event handling when certain products are displayed. This can be used by tracking extensions such * as Google Analytics to track impressions. * * Provides the list of product data (shaped like the Store API responses) and the block name. */ $this->asset_api->add_inline_script( 'wp-hooks', ' window.addEventListener( "DOMContentLoaded", () => { wp.hooks.doAction( "experimental__woocommerce_blocks-product-list-render", { products: JSON.parse( decodeURIComponent( "' . esc_js( rawurlencode( wp_json_encode( array_map( [ Package::container()->get( SchemaController::class )->get( 'product' ), 'get_item_response' ], $products ) ) ) ) . '" ) ), listName: "' . esc_js( $this->block_name ) . '" } ); } ); ', 'after' ); return sprintf( '<div class="%s"><ul class="wc-block-grid__products">%s</ul></div>', esc_attr( $this->get_container_classes() ), implode( '', array_map( array( $this, 'render_product' ), $products ) ) ); } /** * Get the schema for the contentVisibility attribute * * @return array List of block attributes with type and defaults. */ protected function get_schema_content_visibility() { return array( 'type' => 'object', 'properties' => array( 'title' => $this->get_schema_boolean( true ), 'price' => $this->get_schema_boolean( true ), 'rating' => $this->get_schema_boolean( true ), 'button' => $this->get_schema_boolean( true ), ), ); } /** * Get the schema for the orderby attribute. * * @return array Property definition of `orderby` attribute. */ protected function get_schema_orderby() { return array( 'type' => 'string', 'enum' => array( 'date', 'popularity', 'price_asc', 'price_desc', 'rating', 'title', 'menu_order' ), 'default' => 'date', ); } /** * Get the block's attributes. * * @param array $attributes Block attributes. Default empty array. * @return array Block attributes merged with defaults. */ protected function parse_attributes( $attributes ) { // These should match what's set in JS `registerBlockType`. $defaults = array( 'columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), 'rows' => wc_get_theme_support( 'product_blocks::default_rows', 3 ), 'alignButtons' => false, 'categories' => array(), 'catOperator' => 'any', 'contentVisibility' => array( 'title' => true, 'price' => true, 'rating' => true, 'button' => true, ), ); return wp_parse_args( $attributes, $defaults ); } /** * Parse query args. * * @return array */ protected function parse_query_args() { $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'fields' => 'ids', 'ignore_sticky_posts' => true, 'no_found_rows' => false, 'orderby' => '', 'order' => '', 'meta_query' => WC()->query->get_meta_query(), // phpcs:ignore WordPress.DB.SlowDBQuery 'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery 'posts_per_page' => $this->get_products_limit(), ); $this->set_block_query_args( $query_args ); $this->set_ordering_query_args( $query_args ); $this->set_categories_query_args( $query_args ); $this->set_visibility_query_args( $query_args ); return $query_args; } /** * Parse query args. * * @param array $query_args Query args. */ protected function set_ordering_query_args( &$query_args ) { if ( isset( $this->attributes['orderby'] ) ) { if ( 'price_desc' === $this->attributes['orderby'] ) { $query_args['orderby'] = 'price'; $query_args['order'] = 'DESC'; } elseif ( 'price_asc' === $this->attributes['orderby'] ) { $query_args['orderby'] = 'price'; $query_args['order'] = 'ASC'; } elseif ( 'date' === $this->attributes['orderby'] ) { $query_args['orderby'] = 'date'; $query_args['order'] = 'DESC'; } else { $query_args['orderby'] = $this->attributes['orderby']; } } $query_args = array_merge( $query_args, WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ) ); } /** * Set args specific to this block * * @param array $query_args Query args. */ abstract protected function set_block_query_args( &$query_args ); /** * Set categories query args. * * @param array $query_args Query args. */ protected function set_categories_query_args( &$query_args ) { if ( ! empty( $this->attributes['categories'] ) ) { $categories = array_map( 'absint', $this->attributes['categories'] ); $query_args['tax_query'][] = array( 'taxonomy' => 'product_cat', 'terms' => $categories, 'field' => 'term_id', 'operator' => 'all' === $this->attributes['catOperator'] ? 'AND' : 'IN', /* * When cat_operator is AND, the children categories should be excluded, * as only products belonging to all the children categories would be selected. */ 'include_children' => 'all' === $this->attributes['catOperator'] ? false : true, ); } } /** * Set visibility query args. * * @param array $query_args Query args. */ protected function set_visibility_query_args( &$query_args ) { $product_visibility_terms = wc_get_product_visibility_term_ids(); $product_visibility_not_in = array( $product_visibility_terms['exclude-from-catalog'] ); if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $product_visibility_not_in[] = $product_visibility_terms['outofstock']; } $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_not_in, 'operator' => 'NOT IN', ); } /** * Works out the item limit based on rows and columns, or returns default. * * @return int */ protected function get_products_limit() { if ( isset( $this->attributes['rows'], $this->attributes['columns'] ) && ! empty( $this->attributes['rows'] ) ) { $this->attributes['limit'] = intval( $this->attributes['columns'] ) * intval( $this->attributes['rows'] ); } return intval( $this->attributes['limit'] ); } /** * Run the query and return an array of product IDs * * @return array List of product IDs */ protected function get_products() { $is_cacheable = (bool) apply_filters( 'woocommerce_blocks_product_grid_is_cacheable', true, $this->query_args ); $transient_version = \WC_Cache_Helper::get_transient_version( 'product_query' ); $query = new BlocksWpQuery( $this->query_args ); $results = wp_parse_id_list( $is_cacheable ? $query->get_cached_posts( $transient_version ) : $query->get_posts() ); // Remove ordering query arguments which may have been added by get_catalog_ordering_args. WC()->query->remove_ordering_args(); // Prime caches to reduce future queries. if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $results ); } return $results; } /** * Get the list of classes to apply to this block. * * @return string space-separated list of classes. */ protected function get_container_classes() { $classes = array( 'wc-block-grid', "wp-block-{$this->block_name}", "wc-block-{$this->block_name}", "has-{$this->attributes['columns']}-columns", ); if ( $this->attributes['rows'] > 1 ) { $classes[] = 'has-multiple-rows'; } if ( isset( $this->attributes['align'] ) ) { $classes[] = "align{$this->attributes['align']}"; } if ( ! empty( $this->attributes['alignButtons'] ) ) { $classes[] = 'has-aligned-buttons'; } if ( ! empty( $this->attributes['className'] ) ) { $classes[] = $this->attributes['className']; } return implode( ' ', $classes ); } /** * Render a single products. * * @param \WC_Product $product Product object. * @return string Rendered product output. */ protected function render_product( $product ) { $data = (object) array( 'permalink' => esc_url( $product->get_permalink() ), 'image' => $this->get_image_html( $product ), 'title' => $this->get_title_html( $product ), 'rating' => $this->get_rating_html( $product ), 'price' => $this->get_price_html( $product ), 'badge' => $this->get_sale_badge_html( $product ), 'button' => $this->get_button_html( $product ), ); return apply_filters( 'woocommerce_blocks_product_grid_item_html', "<li class=\"wc-block-grid__product\"> <a href=\"{$data->permalink}\" class=\"wc-block-grid__product-link\"> {$data->image} {$data->title} </a> {$data->badge} {$data->price} {$data->rating} {$data->button} </li>", $data, $product ); } /** * Get the product image. * * @param \WC_Product $product Product. * @return string */ protected function get_image_html( $product ) { return '<div class="wc-block-grid__product-image">' . $product->get_image( 'woocommerce_thumbnail' ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get the product title. * * @param \WC_Product $product Product. * @return string */ protected function get_title_html( $product ) { if ( empty( $this->attributes['contentVisibility']['title'] ) ) { return ''; } return '<div class="wc-block-grid__product-title">' . wp_kses_post( $product->get_title() ) . '</div>'; } /** * Render the rating icons. * * @param WC_Product $product Product. * @return string Rendered product output. */ protected function get_rating_html( $product ) { if ( empty( $this->attributes['contentVisibility']['rating'] ) ) { return ''; } $rating_count = $product->get_rating_count(); $review_count = $product->get_review_count(); $average = $product->get_average_rating(); if ( $rating_count > 0 ) { return sprintf( '<div class="wc-block-grid__product-rating">%s</div>', wc_get_rating_html( $average, $rating_count ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } return ''; } /** * Get the price. * * @param \WC_Product $product Product. * @return string Rendered product output. */ protected function get_price_html( $product ) { if ( empty( $this->attributes['contentVisibility']['price'] ) ) { return ''; } return sprintf( '<div class="wc-block-grid__product-price price">%s</div>', wp_kses_post( $product->get_price_html() ) ); } /** * Get the sale badge. * * @param \WC_Product $product Product. * @return string Rendered product output. */ protected function get_sale_badge_html( $product ) { if ( empty( $this->attributes['contentVisibility']['price'] ) ) { return ''; } if ( ! $product->is_on_sale() ) { return; } return '<div class="wc-block-grid__product-onsale"> <span aria-hidden="true">' . esc_html__( 'Sale', 'woocommerce' ) . '</span> <span class="screen-reader-text">' . esc_html__( 'Product on sale', 'woocommerce' ) . '</span> </div>'; } /** * Get the button. * * @param \WC_Product $product Product. * @return string Rendered product output. */ protected function get_button_html( $product ) { if ( empty( $this->attributes['contentVisibility']['button'] ) ) { return ''; } return '<div class="wp-block-button wc-block-grid__product-add-to-cart">' . $this->get_add_to_cart( $product ) . '</div>'; } /** * Get the "add to cart" button. * * @param \WC_Product $product Product. * @return string Rendered product output. */ protected function get_add_to_cart( $product ) { $attributes = array( 'aria-label' => $product->add_to_cart_description(), 'data-quantity' => '1', 'data-product_id' => $product->get_id(), 'data-product_sku' => $product->get_sku(), 'rel' => 'nofollow', 'class' => 'wp-block-button__link add_to_cart_button', ); if ( $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && ( $product->is_in_stock() || $product->backorders_allowed() ) ) { $attributes['class'] .= ' ajax_add_to_cart'; } return sprintf( '<a href="%s" %s>%s</a>', esc_url( $product->add_to_cart_url() ), wc_implode_html_attributes( $attributes ), esc_html( $product->add_to_cart_text() ) ); } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true ); $this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true ); $this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true ); $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); } } packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php 0000644 00000003155 15132754524 0017676 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AtomicBlock class. * * @internal */ class AtomicBlock extends AbstractBlock { /** * Inject attributes and block name. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { return $this->inject_html_data_attributes( $content, $attributes ); } /** * Get the editor script data for this block type. * * @param string $key Data to get, or default to everything. * @return null */ protected function get_block_type_editor_script( $key = null ) { return null; } /** * Get the editor style handle for this block type. * * @return null */ protected function get_block_type_editor_style() { return null; } /** * Get the frontend script handle for this block type. * * @param string $key Data to get, or default to everything. * @return null */ protected function get_block_type_script( $key = null ) { return null; } /** * Get the frontend style handle for this block type. * * @return null */ protected function get_block_type_style() { return null; } /** * Converts block attributes to HTML data attributes. * * @param array $attributes Key value pairs of attributes. * @return string Rendered HTML attributes. */ protected function get_html_data_attributes( array $attributes ) { $data = parent::get_html_data_attributes( $attributes ); return trim( $data . ' data-block-name="' . esc_attr( $this->namespace . '/' . $this->block_name ) . '"' ); } } packages/woocommerce-blocks/src/BlockTypes/Checkout.php 0000644 00000027405 15132754524 0017260 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * Checkout class. * * @internal */ class Checkout extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'checkout'; /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Enqueue frontend assets for this block, just in time for rendering. * * @param array $attributes Any attributes that currently are available from the block. */ protected function enqueue_assets( array $attributes ) { do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' ); parent::enqueue_assets( $attributes ); do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' ); } /** * Append frontend scripts when rendering the block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { if ( $this->is_checkout_endpoint() ) { // Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the // legacy shortcode instead and do not render block. return '[woocommerce_checkout]'; } // Deregister core checkout scripts and styles. wp_dequeue_script( 'wc-checkout' ); wp_dequeue_script( 'wc-password-strength-meter' ); wp_dequeue_script( 'selectWoo' ); wp_dequeue_style( 'select2' ); // If the content is empty, we may have transformed from an older checkout block. Insert the default list of blocks. $regex_for_empty_block = '/<div class="[a-zA-Z0-9_\- ]*wp-block-woocommerce-checkout[a-zA-Z0-9_\- ]*"><\/div>/mi'; $is_empty = preg_match( $regex_for_empty_block, $content ); if ( $is_empty ) { $inner_blocks_html = '<div data-block-name="woocommerce/checkout-fields-block" class="wp-block-woocommerce-checkout-fields-block"></div><div data-block-name="woocommerce/checkout-totals-block" class="wp-block-woocommerce-checkout-totals-block"></div>'; $content = str_replace( '</div>', $inner_blocks_html . '</div>', $content ); } return $this->inject_html_data_attributes( $content, $attributes ); } /** * Check if we're viewing a checkout page endpoint, rather than the main checkout page itself. * * @return boolean */ protected function is_checkout_endpoint() { return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' ); } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'allowedCountries', function() { return $this->deep_sort_with_accents( WC()->countries->get_allowed_countries() ); }, true ); $this->asset_data_registry->add( 'allowedStates', function() { return $this->deep_sort_with_accents( WC()->countries->get_allowed_country_states() ); }, true ); $this->asset_data_registry->add( 'shippingCountries', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); }, true ); $this->asset_data_registry->add( 'shippingStates', function() { return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); }, true ); $this->asset_data_registry->add( 'countryLocale', function() { // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. $country_locale = wc()->countries->get_country_locale(); $states = wc()->countries->get_states(); foreach ( $states as $country => $states ) { if ( empty( $states ) ) { $country_locale[ $country ]['state']['required'] = false; $country_locale[ $country ]['state']['hidden'] = true; } } return $country_locale; }, true ); $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); $this->asset_data_registry->add( 'checkoutAllowsGuest', false === filter_var( WC()->checkout()->is_registration_required(), FILTER_VALIDATE_BOOLEAN ), true ); $this->asset_data_registry->add( 'checkoutAllowsSignup', filter_var( WC()->checkout()->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ), true ); $this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ), true ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); $this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 ); $is_block_editor = $this->is_block_editor(); // Hydrate the following data depending on admin or frontend context. if ( $is_block_editor && ! $this->asset_data_registry->exists( 'shippingMethodsExist' ) ) { $methods_exist = wc_get_shipping_method_count( false, true ) > 0; $this->asset_data_registry->add( 'shippingMethodsExist', $methods_exist ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) { $shipping_methods = WC()->shipping()->get_shipping_methods(); $formatted_shipping_methods = array_reduce( $shipping_methods, function( $acc, $method ) { if ( $method->supports( 'settings' ) ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ]; } return $acc; }, [] ); $this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) { $shipping_zones = \WC_Shipping_Zones::get_zones(); $formatted_shipping_zones = array_reduce( $shipping_zones, function( $acc, $zone ) { $acc[] = [ 'id' => $zone['id'], 'title' => $zone['zone_name'], 'description' => $zone['formatted_zone_location'], ]; return $acc; }, [] ); $formatted_shipping_zones[] = [ 'id' => 0, 'title' => __( 'International', 'woocommerce' ), 'description' => __( 'Locations outside all other zones', 'woocommerce' ), ]; $this->asset_data_registry->add( 'activeShippingZones', $formatted_shipping_zones ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) { $payment_methods = WC()->payment_gateways->payment_gateways(); $formatted_payment_methods = array_reduce( $payment_methods, function( $acc, $method ) { if ( 'yes' === $method->enabled ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ]; } return $acc; }, [] ); $this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods ); } if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->hydrate_from_api(); $this->hydrate_customer_payment_methods(); } do_action( 'woocommerce_blocks_checkout_enqueue_data' ); } /** * Are we currently on the admin block editor screen? */ protected function is_block_editor() { if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); return $screen && $screen->is_block_editor(); } /** * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. * * @param array $array Array of values to sort. * @return array Sorted array. */ protected function deep_sort_with_accents( $array ) { if ( ! is_array( $array ) || empty( $array ) ) { return $array; } if ( is_array( reset( $array ) ) ) { return array_map( [ $this, 'deep_sort_with_accents' ], $array ); } $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); asort( $array_without_accents ); return array_replace( $array_without_accents, $array ); } /** * Get customer payment methods for use in checkout. */ protected function hydrate_customer_payment_methods() { if ( ! is_user_logged_in() || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) { return; } add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); $this->asset_data_registry->add( 'customerPaymentMethods', wc_get_customer_saved_methods_list( get_current_user_id() ) ); remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); } /** * Hydrate the checkout block with data from the API. */ protected function hydrate_from_api() { // Print existing notices now, otherwise they are caught by the Cart // Controller and converted to exceptions. wc_print_notices(); add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); $this->asset_data_registry->hydrate_api_request( '/wc/store/checkout' ); remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); } /** * Callback for woocommerce_payment_methods_list_item filter to add token id * to the generated list. * * @param array $list_item The current list item for the saved payment method. * @param \WC_Token $token The token for the current list item. * * @return array The list item with the token id added. */ public static function include_token_id_with_payment_methods( $list_item, $token ) { $list_item['tokenId'] = $token->get_id(); $brand = ! empty( $list_item['method']['brand'] ) ? strtolower( $list_item['method']['brand'] ) : ''; // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core. if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) { $list_item['method']['brand'] = wc_get_credit_card_type_label( $brand ); } return $list_item; } } packages/woocommerce-blocks/src/BlockTypes/AllReviews.php 0000644 00000002461 15132754524 0017563 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AllReviews class. */ class AllReviews extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'all-reviews'; /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-reviews-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); } } packages/woocommerce-blocks/src/BlockTypes/AllProducts.php 0000644 00000004121 15132754524 0017735 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AllProducts class. */ class AllProducts extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'all-products'; /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true ); $this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true ); $this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true ); $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); } /** * Register script and style assets for the block type before it is registered. * * This registers the scripts; it does not enqueue them. */ protected function register_block_type_assets() { parent::register_block_type_assets(); $this->register_chunk_translations( [ 'atomic-block-components/price', 'atomic-block-components/image', 'atomic-block-components/title', 'atomic-block-components/rating', 'atomic-block-components/button', 'atomic-block-components/summary', 'atomic-block-components/sale-badge', 'atomic-block-components/sku', 'atomic-block-components/category-list', 'atomic-block-components/tag-list', 'atomic-block-components/stock-indicator', 'atomic-block-components/add-to-cart', ] ); } } packages/woocommerce-blocks/src/BlockTypes/ProductTag.php 0000644 00000004244 15132754524 0017563 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductTag class. */ class ProductTag extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-tag'; /** * Set args specific to this block. * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { if ( ! empty( $this->attributes['tags'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => 'product_tag', 'terms' => array_map( 'absint', $this->attributes['tags'] ), 'field' => 'id', 'operator' => isset( $this->attributes['tagOperator'] ) && 'any' === $this->attributes['tagOperator'] ? 'IN' : 'AND', ); } } /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array( 'className' => $this->get_schema_string(), 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), 'contentVisibility' => $this->get_schema_content_visibility(), 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'orderby' => $this->get_schema_orderby(), 'tags' => $this->get_schema_list_ids(), 'tagOperator' => array( 'type' => 'string', 'default' => 'any', ), 'isPreview' => $this->get_schema_boolean( false ), ); } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $tag_count = wp_count_terms( 'product_tag' ); $this->asset_data_registry->add( 'hasTags', $tag_count > 0, true ); $this->asset_data_registry->add( 'limitTags', $tag_count > 100, true ); } } packages/woocommerce-blocks/src/BlockTypes/StockFilter.php 0000644 00000001616 15132754524 0017740 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * AttributeFilter class. */ class StockFilter extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'stock-filter'; /** * Extra data passed through from server to client for block. * * @param array $stock_statuses Any stock statuses that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $stock_statuses = [] ) { parent::enqueue_data( $stock_statuses ); $this->asset_data_registry->add( 'stockStatusOptions', wc_get_product_stock_status_options(), true ); $this->asset_data_registry->add( 'hideOutOfStockItems', 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ), true ); } } packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php 0000644 00000003471 15132754524 0021266 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * HandpickedProducts class. */ class HandpickedProducts extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'handpicked-products'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { $ids = array_map( 'absint', $this->attributes['products'] ); $query_args['post__in'] = $ids; $query_args['posts_per_page'] = count( $ids ); } /** * Set visibility query args. Handpicked products will show hidden products if chosen. * * @param array $query_args Query args. */ protected function set_visibility_query_args( &$query_args ) { if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $product_visibility_terms = wc_get_product_visibility_term_ids(); $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( $product_visibility_terms['outofstock'] ), 'operator' => 'NOT IN', ); } } /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array( 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'className' => $this->get_schema_string(), 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), 'editMode' => $this->get_schema_boolean( true ), 'orderby' => $this->get_schema_orderby(), 'products' => $this->get_schema_list_ids(), 'contentVisibility' => $this->get_schema_content_visibility(), 'isPreview' => $this->get_schema_boolean( false ), ); } } packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php 0000644 00000002504 15132754524 0020764 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ReviewsByProduct class. */ class ReviewsByProduct extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'reviews-by-product'; /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-reviews-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); } } packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php 0000644 00000031620 15132754524 0020223 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; use WP_Block; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; use Automattic\WooCommerce\Blocks\RestApi; /** * AbstractBlock class. */ abstract class AbstractBlock { /** * Block namespace. * * @var string */ protected $namespace = 'woocommerce'; /** * Block name within this namespace. * * @var string */ protected $block_name = ''; /** * Tracks if assets have been enqueued. * * @var boolean */ protected $enqueued_assets = false; /** * Instance of the asset API. * * @var AssetApi */ protected $asset_api; /** * Instance of the asset data registry. * * @var AssetDataRegistry */ protected $asset_data_registry; /** * Instance of the integration registry. * * @var IntegrationRegistry */ protected $integration_registry; /** * Constructor. * * @param AssetApi $asset_api Instance of the asset API. * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. * @param IntegrationRegistry $integration_registry Instance of the integration registry. * @param string $block_name Optionally set block name during construct. */ public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry, IntegrationRegistry $integration_registry, $block_name = '' ) { $this->asset_api = $asset_api; $this->asset_data_registry = $asset_data_registry; $this->integration_registry = $integration_registry; $this->block_name = $block_name ? $block_name : $this->block_name; $this->initialize(); } /** * The default render_callback for all blocks. This will ensure assets are enqueued just in time, then render * the block (if applicable). * * @param array|WP_Block $attributes Block attributes, or an instance of a WP_Block. Defaults to an empty array. * @param string $content Block content. Default empty string. * @return string Rendered block type output. */ public function render_callback( $attributes = [], $content = '' ) { $render_callback_attributes = $this->parse_render_callback_attributes( $attributes ); if ( ! is_admin() ) { $this->enqueue_assets( $render_callback_attributes ); } return $this->render( $render_callback_attributes, $content ); } /** * Enqueue assets used for rendering the block in editor context. * * This is needed if a block is not yet within the post content--`render` and `enqueue_assets` may not have ran. */ public function enqueue_editor_assets() { if ( $this->enqueued_assets ) { return; } $this->enqueue_data(); } /** * Initialize this block type. * * - Hook into WP lifecycle. * - Register the block with WordPress. */ protected function initialize() { if ( empty( $this->block_name ) ) { _doing_it_wrong( __METHOD__, esc_html( __( 'Block name is required.', 'woocommerce' ) ), '4.5.0' ); return false; } $this->integration_registry->initialize( $this->block_name . '_block' ); $this->register_block_type_assets(); $this->register_block_type(); add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); } /** * Register script and style assets for the block type before it is registered. * * This registers the scripts; it does not enqueue them. */ protected function register_block_type_assets() { if ( null !== $this->get_block_type_editor_script() ) { $data = $this->asset_api->get_script_data( $this->get_block_type_editor_script( 'path' ) ); $has_i18n = in_array( 'wp-i18n', $data['dependencies'], true ); $this->asset_api->register_script( $this->get_block_type_editor_script( 'handle' ), $this->get_block_type_editor_script( 'path' ), array_merge( $this->get_block_type_editor_script( 'dependencies' ), $this->integration_registry->get_all_registered_editor_script_handles() ), $has_i18n ); } if ( null !== $this->get_block_type_script() ) { $data = $this->asset_api->get_script_data( $this->get_block_type_script( 'path' ) ); $has_i18n = in_array( 'wp-i18n', $data['dependencies'], true ); $this->asset_api->register_script( $this->get_block_type_script( 'handle' ), $this->get_block_type_script( 'path' ), array_merge( $this->get_block_type_script( 'dependencies' ), $this->integration_registry->get_all_registered_script_handles() ), $has_i18n ); } } /** * Injects Chunk Translations into the page so translations work for lazy loaded components. * * The chunk names are defined when creating lazy loaded components using webpackChunkName. * * @param string[] $chunks Array of chunk names. */ protected function register_chunk_translations( $chunks ) { foreach ( $chunks as $chunk ) { $handle = 'wc-blocks-' . $chunk . '-chunk'; $this->asset_api->register_script( $handle, $this->asset_api->get_block_asset_build_path( $chunk ), [], true ); wp_add_inline_script( $this->get_block_type_script( 'handle' ), wp_scripts()->print_translations( $handle, false ), 'before' ); wp_deregister_script( $handle ); } } /** * Registers the block type with WordPress. */ protected function register_block_type() { register_block_type( $this->get_block_type(), array( 'render_callback' => $this->get_block_type_render_callback(), 'editor_script' => $this->get_block_type_editor_script( 'handle' ), 'editor_style' => $this->get_block_type_editor_style(), 'style' => $this->get_block_type_style(), 'attributes' => $this->get_block_type_attributes(), 'supports' => $this->get_block_type_supports(), ) ); } /** * Get the block type. * * @return string */ protected function get_block_type() { return $this->namespace . '/' . $this->block_name; } /** * Get the render callback for this block type. * * Dynamic blocks should return a callback, for example, `return [ $this, 'render' ];` * * @see $this->register_block_type() * @return callable|null; */ protected function get_block_type_render_callback() { return [ $this, 'render_callback' ]; } /** * Get the editor script data for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the editor style handle for this block type. * * @see $this->register_block_type() * @return string|null */ protected function get_block_type_editor_style() { return 'wc-blocks-editor-style'; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend style handle for this block type. * * @see $this->register_block_type() * @return string|null */ protected function get_block_type_style() { return 'wc-blocks-style'; } /** * Get the supports array for this block type. * * @see $this->register_block_type() * @return string; */ protected function get_block_type_supports() { return []; } /** * Get block attributes. * * @return array; */ protected function get_block_type_attributes() { return []; } /** * Parses block attributes from the render_callback. * * @param array|WP_Block $attributes Block attributes, or an instance of a WP_Block. Defaults to an empty array. * @return array */ protected function parse_render_callback_attributes( $attributes ) { return is_a( $attributes, 'WP_Block' ) ? $attributes->attributes : $attributes; } /** * Render the block. Extended by children. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { return $content; } /** * Enqueue frontend assets for this block, just in time for rendering. * * @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that * we intentionally do not pass 'script' to register_block_type. * * @param array $attributes Any attributes that currently are available from the block. */ protected function enqueue_assets( array $attributes ) { if ( $this->enqueued_assets ) { return; } $this->enqueue_data( $attributes ); $this->enqueue_scripts( $attributes ); $this->enqueued_assets = true; } /** * Injects block attributes into the block. * * @param string $content HTML content to inject into. * @param array $attributes Key value pairs of attributes. * @return string Rendered block with data attributes. */ protected function inject_html_data_attributes( $content, array $attributes ) { return preg_replace( '/<div /', '<div ' . $this->get_html_data_attributes( $attributes ) . ' ', $content, 1 ); } /** * Converts block attributes to HTML data attributes. * * @param array $attributes Key value pairs of attributes. * @return string Rendered HTML attributes. */ protected function get_html_data_attributes( array $attributes ) { $data = []; foreach ( $attributes as $key => $value ) { if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } if ( ! is_scalar( $value ) ) { $value = wp_json_encode( $value ); } $data[] = 'data-' . esc_attr( strtolower( preg_replace( '/(?<!\ )[A-Z]/', '-$0', $key ) ) ) . '="' . esc_attr( $value ) . '"'; } return implode( ' ', $data ); } /** * Data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { $registered_script_data = $this->integration_registry->get_all_registered_script_data(); foreach ( $registered_script_data as $asset_data_key => $asset_data_value ) { if ( ! $this->asset_data_registry->exists( $asset_data_key ) ) { $this->asset_data_registry->add( $asset_data_key, $asset_data_value ); } } if ( ! $this->asset_data_registry->exists( 'wcBlocksConfig' ) ) { $this->asset_data_registry->add( 'wcBlocksConfig', [ 'buildPhase' => Package::feature()->get_flag(), 'pluginUrl' => plugins_url( '/', dirname( __DIR__ ) ), 'productCount' => array_sum( (array) wp_count_posts( 'product' ) ), 'restApiRoutes' => [ '/wc/store' => array_keys( Package::container()->get( RestApi::class )->get_routes_from_namespace( 'wc/store' ) ), ], 'defaultAvatar' => get_avatar_url( 0, [ 'force_default' => true ] ), /* * translators: If your word count is based on single characters (e.g. East Asian characters), * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'. * Do not translate into your own language. */ 'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ), ] ); } } /** * Register/enqueue scripts used for this block on the frontend, during render. * * @param array $attributes Any attributes that currently are available from the block. */ protected function enqueue_scripts( array $attributes = [] ) { if ( null !== $this->get_block_type_script() ) { wp_enqueue_script( $this->get_block_type_script( 'handle' ) ); } } /** * Script to append the correct sizing class to a block skeleton. * * @return string */ protected function get_skeleton_inline_script() { return "<script> var containers = document.querySelectorAll( 'div.wc-block-skeleton' ); if ( containers.length ) { Array.prototype.forEach.call( containers, function( el, i ) { var w = el.offsetWidth; var classname = ''; if ( w > 700 ) classname = 'is-large'; else if ( w > 520 ) classname = 'is-medium'; else if ( w > 400 ) classname = 'is-small'; else classname = 'is-mobile'; if ( ! el.classList.contains( classname ) ) { el.classList.add( classname ); } el.classList.remove( 'hidden' ); } ); } </script>"; } } packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php 0000644 00000000674 15132754524 0021302 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductBestSellers class. */ class ProductBestSellers extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-best-sellers'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { $query_args['orderby'] = 'popularity'; } } packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php 0000644 00000001371 15132754524 0020227 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductOnSale class. */ class ProductOnSale extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'product-on-sale'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { $query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() ); } /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array_merge( parent::get_block_type_attributes(), array( 'className' => $this->get_schema_string(), 'orderby' => $this->get_schema_orderby(), ) ); } } packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php 0000644 00000000342 15132754524 0020246 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ActiveFilters class. */ class ActiveFilters extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'active-filters'; } packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php 0000644 00000013677 15132754524 0020621 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * FeaturedProduct class. */ class FeaturedProduct extends AbstractDynamicBlock { /** * Block name. * * @var string */ protected $block_name = 'featured-product'; /** * Default attribute values, should match what's set in JS `registerBlockType`. * * @var array */ protected $defaults = array( 'align' => 'none', 'contentAlign' => 'center', 'dimRatio' => 50, 'focalPoint' => false, 'height' => false, 'mediaId' => 0, 'mediaSrc' => '', 'showDesc' => true, 'showPrice' => true, ); /** * Render the Featured Product block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { $id = absint( isset( $attributes['productId'] ) ? $attributes['productId'] : 0 ); $product = wc_get_product( $id ); if ( ! $product ) { return ''; } $attributes = wp_parse_args( $attributes, $this->defaults ); $attributes['height'] = $attributes['height'] ? $attributes['height'] : wc_get_theme_support( 'featured_block::default_height', 500 ); $title = sprintf( '<h2 class="wc-block-featured-product__title">%s</h2>', wp_kses_post( $product->get_title() ) ); if ( $product->is_type( 'variation' ) ) { $title .= sprintf( '<h3 class="wc-block-featured-product__variation">%s</h3>', wp_kses_post( wc_get_formatted_variation( $product, true, true, false ) ) ); } $desc_str = sprintf( '<div class="wc-block-featured-product__description">%s</div>', wc_format_content( wp_kses_post( $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) ) ); $price_str = sprintf( '<div class="wc-block-featured-product__price">%s</div>', wp_kses_post( $product->get_price_html() ) ); $output = sprintf( '<div class="%1$s" style="%2$s">', esc_attr( $this->get_classes( $attributes ) ), esc_attr( $this->get_styles( $attributes, $product ) ) ); $output .= '<div class="wc-block-featured-product__wrapper">'; $output .= $title; if ( $attributes['showDesc'] ) { $output .= $desc_str; } if ( $attributes['showPrice'] ) { $output .= $price_str; } $output .= '<div class="wc-block-featured-product__link">' . $content . '</div>'; $output .= '</div>'; $output .= '</div>'; return $output; } /** * Get the styles for the wrapper element (background image, color). * * @param array $attributes Block attributes. Default empty array. * @param \WC_Product $product Product object. * @return string */ public function get_styles( $attributes, $product ) { $style = ''; $image_size = 'large'; if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) { $image_size = 'full'; } if ( $attributes['mediaId'] ) { $image = wp_get_attachment_image_url( $attributes['mediaId'], $image_size ); } else { $image = $this->get_image( $product, $image_size ); } if ( ! empty( $image ) ) { $style .= sprintf( 'background-image:url(%s);', esc_url( $image ) ); } if ( isset( $attributes['customOverlayColor'] ) ) { $style .= sprintf( 'background-color:%s;', esc_attr( $attributes['customOverlayColor'] ) ); } if ( isset( $attributes['height'] ) ) { $style .= sprintf( 'min-height:%dpx;', intval( $attributes['height'] ) ); } if ( is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] ) ) { $style .= sprintf( 'background-position: %s%% %s%%', $attributes['focalPoint']['x'] * 100, $attributes['focalPoint']['y'] * 100 ); } return $style; } /** * Get class names for the block container. * * @param array $attributes Block attributes. Default empty array. * @return string */ public function get_classes( $attributes ) { $classes = array( 'wc-block-' . $this->block_name ); if ( isset( $attributes['align'] ) ) { $classes[] = "align{$attributes['align']}"; } if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) { $classes[] = 'has-background-dim'; if ( 50 !== $attributes['dimRatio'] ) { $classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 ); } } if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) { $classes[] = "has-{$attributes['contentAlign']}-content"; } if ( isset( $attributes['overlayColor'] ) ) { $classes[] = "has-{$attributes['overlayColor']}-background-color"; } if ( isset( $attributes['className'] ) ) { $classes[] = $attributes['className']; } return implode( ' ', $classes ); } /** * Returns the main product image URL. * * @param \WC_Product $product Product object. * @param string $size Image size, defaults to 'full'. * @return string */ public function get_image( $product, $size = 'full' ) { $image = ''; if ( $product->get_image_id() ) { $image = wp_get_attachment_image_url( $product->get_image_id(), $size ); } elseif ( $product->get_parent_id() ) { $parent_product = wc_get_product( $product->get_parent_id() ); if ( $parent_product ) { $image = wp_get_attachment_image_url( $parent_product->get_image_id(), $size ); } } return $image; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true ); $this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true ); } } packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php 0000644 00000002507 15132754524 0021124 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ReviewsByCategory class. */ class ReviewsByCategory extends AbstractBlock { /** * Block name. * * @var string */ protected $block_name = 'reviews-by-category'; /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $script = [ 'handle' => 'wc-reviews-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), 'dependencies' => [], ]; return $key ? $script[ $key ] : $script; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); } } packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php 0000644 00000003756 15132754524 0021500 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductsByAttribute class. */ class ProductsByAttribute extends AbstractProductGrid { /** * Block name. * * @var string */ protected $block_name = 'products-by-attribute'; /** * Set args specific to this block * * @param array $query_args Query args. */ protected function set_block_query_args( &$query_args ) { if ( ! empty( $this->attributes['attributes'] ) ) { $taxonomy = sanitize_title( $this->attributes['attributes'][0]['attr_slug'] ); $terms = wp_list_pluck( $this->attributes['attributes'], 'id' ); $query_args['tax_query'][] = array( 'taxonomy' => $taxonomy, 'terms' => array_map( 'absint', $terms ), 'field' => 'term_id', 'operator' => 'all' === $this->attributes['attrOperator'] ? 'AND' : 'IN', ); } } /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array( 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'attributes' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'number', ), 'attr_slug' => array( 'type' => 'string', ), ), ), 'default' => array(), ), 'attrOperator' => array( 'type' => 'string', 'default' => 'any', ), 'className' => $this->get_schema_string(), 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), 'contentVisibility' => $this->get_schema_content_visibility(), 'editMode' => $this->get_schema_boolean( true ), 'orderby' => $this->get_schema_orderby(), 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), 'isPreview' => $this->get_schema_boolean( false ), ); } } packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php 0000644 00000004124 15132754524 0017277 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Utils; use WP_Query; /** * BlocksWpQuery query. * * Wrapper for WP Query with additonal helper methods. * Allows query args to be set and parsed without doing running it, so that a cache can be used. * * @deprecated 2.5.0 */ class BlocksWpQuery extends WP_Query { /** * Constructor. * * Sets up the WordPress query, if parameter is not empty. * * Unlike the constructor in WP_Query, this does not RUN the query. * * @param string|array $query URL query string or array of vars. */ public function __construct( $query = '' ) { if ( ! empty( $query ) ) { $this->init(); $this->query = wp_parse_args( $query ); $this->query_vars = $this->query; $this->parse_query_vars(); } } /** * Get cached posts, if a cache exists. * * A hash is generated using the array of query_vars. If doing custom queries via filters such as posts_where * (where the SQL query is manipulated directly) you can still ensure there is a unique hash by injecting custom * query vars via the parse_query filter. For example: * * add_filter( 'parse_query', function( $wp_query ) { * $wp_query->query_vars['my_custom_query_var'] = true; * } ); * * Doing so won't have any negative effect on the query itself, and it will cause the hash to change. * * @param string $transient_version Transient version to allow for invalidation. * @return WP_Post[]|int[] Array of post objects or post IDs. */ public function get_cached_posts( $transient_version = '' ) { $hash = md5( wp_json_encode( $this->query_vars ) ); $transient_name = 'wc_blocks_query_' . $hash; $transient_value = get_transient( $transient_name ); if ( isset( $transient_value, $transient_value['version'], $transient_value['value'] ) && $transient_value['version'] === $transient_version ) { return $transient_value['value']; } $results = $this->get_posts(); set_transient( $transient_name, array( 'version' => $transient_version, 'value' => $results, ), DAY_IN_SECONDS * 30 ); return $results; } } packages/woocommerce-blocks/src/Utils/ArrayUtils.php 0000644 00000002024 15132754524 0016621 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Utils; /** * ArrayUtils class used for custom functions to operate on arrays */ class ArrayUtils { /** * Join a string with a natural language conjunction at the end. * * @param array $array The array to join together with the natural language conjunction. * @param bool $enclose_items_with_quotes Whether each item in the array should be enclosed within quotation marks. * * @return string a string containing a list of items and a natural language conjuction. */ public static function natural_language_join( $array, $enclose_items_with_quotes = false ) { if ( true === $enclose_items_with_quotes ) { $array = array_map( function( $item ) { return '"' . $item . '"'; }, $array ); } $last = array_pop( $array ); if ( $array ) { return sprintf( /* translators: 1: The first n-1 items of a list 2: the last item in the list. */ __( '%1$s and %2$s', 'woocommerce' ), implode( ', ', $array ), $last ); } return $last; } } packages/woocommerce-blocks/src/Package.php 0000644 00000006054 15132754524 0014764 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\Domain\Package as NewPackage; use Automattic\WooCommerce\Blocks\Domain\Bootstrap; use Automattic\WooCommerce\Blocks\Registry\Container; use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; /** * Main package class. * * Returns information about the package and handles init. * * In the context of this plugin, it handles init and is called from the main * plugin file (woocommerce-gutenberg-products-block.php). * * In the context of WooCommere core, it handles init and is called from * WooCommerce's package loader. The main plugin file is _not_ loaded. * * @since 2.5.0 */ class Package { /** * For back compat this is provided. Ideally, you should register your * class with Automattic\Woocommerce\Blocks\Container and make Package a * dependency. * * @since 2.5.0 * @return Package The Package instance class */ protected static function get_package() { return self::container()->get( NewPackage::class ); } /** * Init the package - load the blocks library and define constants. * * @since 2.5.0 Handled by new NewPackage. */ public static function init() { self::container()->get( Bootstrap::class ); } /** * Return the version of the package. * * @return string */ public static function get_version() { return self::get_package()->get_version(); } /** * Return the path to the package. * * @return string */ public static function get_path() { return self::get_package()->get_path(); } /** * Returns an instance of the the FeatureGating class. * * @return FeatureGating */ public static function feature() { return self::get_package()->feature(); } /** * Checks if we're executing the code in an experimental build mode. * * @return boolean */ public static function is_experimental_build() { return self::get_package()->is_experimental_build(); } /** * Checks if we're executing the code in an feature plugin or experimental build mode. * * @return boolean */ public static function is_feature_plugin_build() { return self::get_package()->is_feature_plugin_build(); } /** * Loads the dependency injection container for woocommerce blocks. * * @param boolean $reset Used to reset the container to a fresh instance. * Note: this means all dependencies will be * reconstructed. */ public static function container( $reset = false ) { static $container; if ( ! $container instanceof Container || $reset ) { $container = new Container(); // register Package. $container->register( NewPackage::class, function ( $container ) { // leave for automated version bumping. $version = '6.1.0'; return new NewPackage( $version, dirname( __DIR__ ), new FeatureGating() ); } ); // register Bootstrap. $container->register( Bootstrap::class, function ( $container ) { return new Bootstrap( $container ); } ); } return $container; } } packages/woocommerce-blocks/src/Domain/Bootstrap.php 0000644 00000024215 15132754524 0016614 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain; use Automattic\WooCommerce\Blocks\AssetsController as AssetsController; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\BlockTypesController; use Automattic\WooCommerce\Blocks\InboxNotifications; use Automattic\WooCommerce\Blocks\Installer; use Automattic\WooCommerce\Blocks\Registry\Container; use Automattic\WooCommerce\Blocks\RestApi; use Automattic\WooCommerce\Blocks\Payments\Api as PaymentsApi; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use Automattic\WooCommerce\Blocks\Payments\Integrations\Stripe; use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque; use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal; use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer; use Automattic\WooCommerce\Blocks\Payments\Integrations\CashOnDelivery; use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders; use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; use Automattic\WooCommerce\Blocks\Domain\Services\Email\CustomerNewAccount; use Automattic\WooCommerce\Blocks\StoreApi\Formatters; use Automattic\WooCommerce\Blocks\StoreApi\Formatters\MoneyFormatter; use Automattic\WooCommerce\Blocks\StoreApi\Formatters\HtmlFormatter; use Automattic\WooCommerce\Blocks\StoreApi\Formatters\CurrencyFormatter; use Automattic\WooCommerce\Blocks\StoreApi\RoutesController; use Automattic\WooCommerce\Blocks\StoreApi\SchemaController; use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics; /** * Takes care of bootstrapping the plugin. * * @since 2.5.0 */ class Bootstrap { /** * Holds the Dependency Injection Container * * @var Container */ private $container; /** * Holds the Package instance * * @var Package */ private $package; /** * Constructor * * @param Container $container The Dependency Injection Container. */ public function __construct( Container $container ) { $this->container = $container; $this->package = $container->get( Package::class ); if ( $this->has_core_dependencies() ) { $this->init(); /** * Usable as a safe event hook for when the plugin has been loaded. */ do_action( 'woocommerce_blocks_loaded' ); } } /** * Init the package - load the blocks library and define constants. */ protected function init() { $this->register_dependencies(); $this->register_payment_methods(); add_action( 'admin_init', function() { InboxNotifications::create_surface_cart_checkout_blocks_notification(); }, 10, 0 ); $is_rest = wc()->is_rest_api_request(); // Load assets in admin and on the frontend. if ( ! $is_rest ) { $this->add_build_notice(); $this->container->get( AssetDataRegistry::class ); $this->container->get( Installer::class ); $this->container->get( AssetsController::class ); } $this->container->get( DraftOrders::class )->init(); $this->container->get( CreateAccount::class )->init(); $this->container->get( ExtendRestApi::class ); $this->container->get( RestApi::class ); $this->container->get( GoogleAnalytics::class ); $this->container->get( BlockTypesController::class ); if ( $this->package->feature()->is_feature_plugin_build() ) { $this->container->get( PaymentsApi::class ); } } /** * Check core dependencies exist. * * @return boolean */ protected function has_core_dependencies() { $has_needed_dependencies = class_exists( 'WooCommerce', false ); if ( $has_needed_dependencies ) { $plugin_data = \get_file_data( $this->package->get_path( 'woocommerce-gutenberg-products-block.php' ), [ 'RequiredWCVersion' => 'WC requires at least', ] ); if ( isset( $plugin_data['RequiredWCVersion'] ) && version_compare( \WC()->version, $plugin_data['RequiredWCVersion'], '<' ) ) { $has_needed_dependencies = false; add_action( 'admin_notices', function() { if ( should_display_compatibility_notices() ) { ?> <div class="notice notice-error"> <p><?php esc_html_e( 'The WooCommerce Blocks feature plugin requires a more recent version of WooCommerce and has been paused. Please update WooCommerce to the latest version to continue enjoying WooCommerce Blocks.', 'woocommerce' ); ?></p> </div> <?php } } ); } } return $has_needed_dependencies; } /** * See if files have been built or not. * * @return bool */ protected function is_built() { return file_exists( $this->package->get_path( 'build/featured-product.js' ) ); } /** * Add a notice stating that the build has not been done yet. */ protected function add_build_notice() { if ( $this->is_built() ) { return; } add_action( 'admin_notices', function() { echo '<div class="error"><p>'; printf( /* translators: %1$s is the install command, %2$s is the build command, %3$s is the watch command. */ esc_html__( 'WooCommerce Blocks development mode requires files to be built. From the plugin directory, run %1$s to install dependencies, %2$s to build the files or %3$s to build the files and watch for changes.', 'woocommerce' ), '<code>npm install</code>', '<code>npm run build</code>', '<code>npm start</code>' ); echo '</p></div>'; } ); } /** * Register core dependencies with the container. */ protected function register_dependencies() { $this->container->register( FeatureGating::class, function ( Container $container ) { return new FeatureGating(); } ); $this->container->register( AssetApi::class, function ( Container $container ) { return new AssetApi( $container->get( Package::class ) ); } ); $this->container->register( AssetDataRegistry::class, function( Container $container ) { return new AssetDataRegistry( $container->get( AssetApi::class ) ); } ); $this->container->register( AssetsController::class, function( Container $container ) { return new AssetsController( $container->get( AssetApi::class ) ); } ); $this->container->register( PaymentMethodRegistry::class, function( Container $container ) { return new PaymentMethodRegistry(); } ); $this->container->register( RestApi::class, function ( Container $container ) { return new RestApi( $container->get( RoutesController::class ) ); } ); $this->container->register( Installer::class, function ( Container $container ) { return new Installer(); } ); $this->container->register( BlockTypesController::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); $asset_data_registry = $container->get( AssetDataRegistry::class ); return new BlockTypesController( $asset_api, $asset_data_registry ); } ); $this->container->register( DraftOrders::class, function( Container $container ) { return new DraftOrders( $container->get( Package::class ) ); } ); $this->container->register( CreateAccount::class, function( Container $container ) { return new CreateAccount( $container->get( Package::class ) ); } ); $this->container->register( Formatters::class, function( Container $container ) { $formatters = new Formatters(); $formatters->register( 'money', MoneyFormatter::class ); $formatters->register( 'html', HtmlFormatter::class ); $formatters->register( 'currency', CurrencyFormatter::class ); return $formatters; } ); $this->container->register( SchemaController::class, function( Container $container ) { return new SchemaController( $container->get( ExtendRestApi::class ) ); } ); $this->container->register( RoutesController::class, function( Container $container ) { return new RoutesController( $container->get( SchemaController::class ) ); } ); $this->container->register( ExtendRestApi::class, function( Container $container ) { return new ExtendRestApi( $container->get( Package::class ), $container->get( Formatters::class ) ); } ); $this->container->register( GoogleAnalytics::class, function( Container $container ) { // Require Google Analytics Integration to be activated. if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) { return; } $asset_api = $container->get( AssetApi::class ); return new GoogleAnalytics( $asset_api ); } ); if ( $this->package->feature()->is_feature_plugin_build() ) { $this->container->register( PaymentsApi::class, function ( Container $container ) { $payment_method_registry = $container->get( PaymentMethodRegistry::class ); $asset_data_registry = $container->get( AssetDataRegistry::class ); return new PaymentsApi( $payment_method_registry, $asset_data_registry ); } ); } } /** * Register payment method integrations with the container. * * @internal Stripe is a temporary method that is used for setting up payment method integrations with Cart and * Checkout blocks. This logic should get moved to the payment gateway extensions. */ protected function register_payment_methods() { $this->container->register( Stripe::class, function( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new Stripe( $asset_api ); } ); $this->container->register( Cheque::class, function( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new Cheque( $asset_api ); } ); $this->container->register( PayPal::class, function( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new PayPal( $asset_api ); } ); $this->container->register( BankTransfer::class, function( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new BankTransfer( $asset_api ); } ); $this->container->register( CashOnDelivery::class, function( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new CashOnDelivery( $asset_api ); } ); } } packages/woocommerce-blocks/src/Domain/Package.php 0000644 00000004746 15132754524 0016201 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain; use Automattic\WooCommerce\Blocks\Package as NewPackage; use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; /** * Main package class. * * Returns information about the package and handles init. * * @since 2.5.0 */ class Package { /** * Holds the current version of the blocks plugin. * * @var string */ private $version; /** * Holds the main path to the blocks plugin directory. * * @var string */ private $path; /** * Holds the feature gating class instance. * * @var FeatureGating */ private $feature_gating; /** * Constructor * * @param string $version Version of the plugin. * @param string $plugin_path Path to the main plugin file. * @param FeatureGating $feature_gating Feature gating class instance. */ public function __construct( $version, $plugin_path, FeatureGating $feature_gating ) { $this->version = $version; $this->path = $plugin_path; $this->feature_gating = $feature_gating; } /** * Returns the version of the plugin. * * @return string */ public function get_version() { return $this->version; } /** * Returns the path to the plugin directory. * * @param string $relative_path If provided, the relative path will be * appended to the plugin path. * * @return string */ public function get_path( $relative_path = '' ) { return trailingslashit( $this->path ) . $relative_path; } /** * Returns the url to the blocks plugin directory. * * @param string $relative_url If provided, the relative url will be * appended to the plugin url. * * @return string */ public function get_url( $relative_url = '' ) { // Append index.php so WP does not return the parent directory. return plugin_dir_url( $this->path . '/index.php' ) . $relative_url; } /** * Returns an instance of the the FeatureGating class. * * @return FeatureGating */ public function feature() { return $this->feature_gating; } /** * Checks if we're executing the code in an experimental build mode. * * @return boolean */ public function is_experimental_build() { return $this->feature()->is_experimental_build(); } /** * Checks if we're executing the code in an feature plugin or experimental build mode. * * @return boolean */ public function is_feature_plugin_build() { return $this->feature()->is_feature_plugin_build(); } } packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php 0000644 00000020776 15132754524 0021152 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use \WP_REST_Request; use \WC_Order; use Automattic\WooCommerce\Blocks\Domain\Package; use Automattic\WooCommerce\Blocks\Domain\Services\Email\CustomerNewAccount; /** * Service class implementing new create account behaviour for order processing. */ class CreateAccount { /** * Reference to the Package instance * * @var Package */ private $package; /** * Constructor. * * @param Package $package An instance of (Woo Blocks) Package. */ public function __construct( Package $package ) { $this->package = $package; } /** * Single method for feature gating logic. Used to gate all non-private methods. * * @return True if Checkout sign-up feature should be made available. */ private function is_feature_enabled() { // Checkout signup is feature gated to WooCommerce 4.7 and newer; // uses updated my-account/lost-password screen from 4.7+ for // setting initial password. // This service is feature gated to plugin only, to match the // availability of the Checkout block (feature plugin only). return $this->package->feature()->is_feature_plugin_build() && defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.7', '>=' ); } /** * Init - register handlers for WooCommerce core email hooks. */ public function init() { if ( ! self::is_feature_enabled() ) { return; } // Override core email handlers to add our new improved "new account" email. add_action( 'woocommerce_email', function ( $wc_emails_instance ) { // Remove core "new account" handler; we are going to replace it. remove_action( 'woocommerce_created_customer_notification', array( $wc_emails_instance, 'customer_new_account' ), 10, 3 ); // Add custom "new account" handler. add_action( 'woocommerce_created_customer_notification', function( $customer_id, $new_customer_data = array(), $password_generated = false ) use ( $wc_emails_instance ) { // If this is a block-based signup, send a new email // with password reset link (no password in email). if ( isset( $new_customer_data['is_checkout_block_customer_signup'] ) ) { $this->customer_new_account( $customer_id, $new_customer_data ); return; } // Otherwise, trigger the existing legacy email (with new password inline). $wc_emails_instance->customer_new_account( $customer_id, $new_customer_data, $password_generated ); }, 10, 3 ); } ); } /** * Trigger new account email. * This is intended as a replacement to WC_Emails::customer_new_account(), * with a set password link instead of emailing the new password in email * content. * * @param int $customer_id The ID of the new customer account. * @param array $new_customer_data Assoc array of data for the new account. */ public function customer_new_account( $customer_id = 0, array $new_customer_data = array() ) { if ( ! self::is_feature_enabled() ) { return; } if ( ! $customer_id ) { return; } $new_account_email = new CustomerNewAccount( $this->package ); $new_account_email->trigger( $customer_id, $new_customer_data ); } /** * Create a user account for specified request (if necessary). * If a new account is created: * - The user is logged in. * * @param \WP_REST_Request $request The current request object being handled. * * @throws Exception On error. * @return int The new user id, or 0 if no user was created. */ public function from_order_request( \WP_REST_Request $request ) { if ( ! self::is_feature_enabled() || ! $this->should_create_customer_account( $request ) ) { return 0; } $customer_id = $this->create_customer_account( $request['billing_address']['email'], $request['billing_address']['first_name'], $request['billing_address']['last_name'] ); // Log the customer in and associate with the order. wc_set_customer_auth_cookie( $customer_id ); return $customer_id; } /** * Check request options and store (shop) config to determine if a user account * should be created as part of order processing. * * @param \WP_REST_Request $request The current request object being handled. * * @return boolean True if a new user account should be created. */ protected function should_create_customer_account( \WP_REST_Request $request ) { if ( is_user_logged_in() ) { // User is already logged in - no need to create an account. return false; } // From here we know that the shopper is not logged in. // check for whether account creation is enabled at the global level. $checkout = WC()->checkout(); if ( ! $checkout instanceof \WC_Checkout ) { // If checkout class is not available, we have major problems, don't create account. return false; } if ( false === filter_var( $checkout->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) ) { // Registration is not enabled for the store, so return false. return false; } if ( true === filter_var( $checkout->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) ) { // Store requires an account for all checkouts (purchases). // Create an account independent of shopper option in $request. // Note - checkbox is not displayed to shopper in this case. return true; } // From here we know that the store allows guest checkout; // shopper can choose whether they sign up (`should_create_account`). if ( true === filter_var( $request['should_create_account'], FILTER_VALIDATE_BOOLEAN ) ) { // User has requested an account as part of checkout processing. return true; } return false; } /** * Convert an account creation error to an exception. * * @param \WP_Error $error An error object. * * @return Exception. */ private function map_create_account_error( \WP_Error $error ) { switch ( $error->get_error_code() ) { // WordPress core error codes. case 'empty_username': case 'invalid_username': case 'empty_email': case 'invalid_email': case 'email_exists': case 'registerfail': return new \Exception( 'woocommerce_rest_checkout_create_account_failure' ); } return new \Exception( 'woocommerce_rest_checkout_create_account_failure' ); } /** * Create a new account for a customer (using a new blocks-specific PHP API). * * The account is created with a generated username. The customer is sent * an email notifying them about the account and containing a link to set * their (initial) password. * * Intended as a replacement for wc_create_new_customer in WC core. * * @throws \Exception If an error is encountered when creating the user account. * * @param string $user_email The email address to use for the new account. * @param string $first_name The first name to use for the new account. * @param string $last_name The last name to use for the new account. * * @return int User id if successful */ private function create_customer_account( $user_email, $first_name, $last_name ) { if ( empty( $user_email ) || ! is_email( $user_email ) ) { throw new \Exception( 'registration-error-invalid-email' ); } if ( email_exists( $user_email ) ) { throw new \Exception( 'registration-error-email-exists' ); } $username = wc_create_new_customer_username( $user_email ); // Handle password creation. $password = wp_generate_password(); $password_generated = true; // Use WP_Error to handle registration errors. $errors = new \WP_Error(); do_action( 'woocommerce_register_post', $username, $user_email, $errors ); $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $user_email ); if ( $errors->get_error_code() ) { throw new \Exception( $errors->get_error_code() ); } $new_customer_data = apply_filters( 'woocommerce_new_customer_data', array( 'is_checkout_block_customer_signup' => true, 'user_login' => $username, 'user_pass' => $password, 'user_email' => $user_email, 'first_name' => $first_name, 'last_name' => $last_name, 'role' => 'customer', ) ); $customer_id = wp_insert_user( $new_customer_data ); if ( is_wp_error( $customer_id ) ) { throw $this->map_create_account_error( $customer_id ); } // Set account flag to remind customer to update generated password. update_user_option( $customer_id, 'default_password_nag', true, true ); do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); return $customer_id; } } packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php 0000644 00000017724 15132754524 0020650 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; use Exception; use WC_Order; /** * Service class for adding DraftOrder functionality to WooCommerce core. * * Sets up all logic related to the Checkout Draft Orders service * * @internal */ class DraftOrders { const DB_STATUS = 'wc-checkout-draft'; const STATUS = 'checkout-draft'; /** * Holds the Package instance * * @var Package */ private $package; /** * Constructor * * @param Package $package An instance of the package class. */ public function __construct( Package $package ) { $this->package = $package; } /** * Set all hooks related to adding Checkout Draft order functionality to Woo Core. */ public function init() { if ( $this->package->feature()->is_feature_plugin_build() ) { add_filter( 'wc_order_statuses', [ $this, 'register_draft_order_status' ] ); add_filter( 'woocommerce_register_shop_order_post_statuses', [ $this, 'register_draft_order_post_status' ] ); add_filter( 'woocommerce_analytics_excluded_order_statuses', [ $this, 'append_draft_order_post_status' ] ); add_filter( 'woocommerce_valid_order_statuses_for_payment', [ $this, 'append_draft_order_post_status' ] ); add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', [ $this, 'append_draft_order_post_status' ] ); // Hook into the query to retrieve My Account orders so draft status is excluded. add_action( 'woocommerce_my_account_my_orders_query', [ $this, 'delete_draft_order_post_status_from_args' ] ); add_action( 'woocommerce_cleanup_draft_orders', [ $this, 'delete_expired_draft_orders' ] ); add_action( 'admin_init', [ $this, 'install' ] ); } else { // Maybe remove existing cronjob if present because it shouldn't be needed in the environment. add_action( 'admin_init', [ $this, 'uninstall' ] ); } } /** * Installation related logic for Draft order functionality. * * @internal */ public function install() { $this->maybe_create_cronjobs(); } /** * Remove cronjobs if they exist (but only from admin). * * @internal */ public function uninstall() { $this->maybe_remove_cronjobs(); } /** * Maybe create cron events. */ protected function maybe_create_cronjobs() { if ( function_exists( 'as_next_scheduled_action' ) && false === as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) { as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'woocommerce_cleanup_draft_orders' ); } } /** * Unschedule cron jobs that are present. */ protected function maybe_remove_cronjobs() { if ( function_exists( 'as_next_scheduled_action' ) && as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) { as_unschedule_all_actions( 'woocommerce_cleanup_draft_orders' ); } } /** * Register custom order status for orders created via the API during checkout. * * Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order. * * @param array $statuses Array of statuses. * @internal * @return array */ public function register_draft_order_status( array $statuses ) { $statuses[ self::DB_STATUS ] = _x( 'Draft', 'Order status', 'woocommerce' ); return $statuses; } /** * Register custom order post status for orders created via the API during checkout. * * @param array $statuses Array of statuses. * @internal * @return array */ public function register_draft_order_post_status( array $statuses ) { $statuses[ self::DB_STATUS ] = $this->get_post_status_properties(); return $statuses; } /** * Returns the properties of this post status for registration. * * @return array */ private function get_post_status_properties() { return [ 'label' => _x( 'Draft', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => false, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Drafts <span class="count">(%s)</span>', 'Drafts <span class="count">(%s)</span>', 'woocommerce' ), ]; } /** * Remove draft status from the 'status' argument of an $args array. * * @param array $args Array of arguments containing statuses in the status key. * @internal * @return array */ public function delete_draft_order_post_status_from_args( $args ) { if ( ! array_key_exists( 'status', $args ) ) { $statuses = []; foreach ( wc_get_order_statuses() as $key => $label ) { if ( self::DB_STATUS !== $key ) { $statuses[] = str_replace( 'wc-', '', $key ); } } $args['status'] = $statuses; } elseif ( self::DB_STATUS === $args['status'] ) { $args['status'] = ''; } elseif ( is_array( $args['status'] ) ) { $args['status'] = array_diff_key( $args['status'], array( self::STATUS => null ) ); } return $args; } /** * Append draft status to a list of statuses. * * @param array $statuses Array of statuses. * @internal * @return array */ public function append_draft_order_post_status( $statuses ) { $statuses[] = self::STATUS; return $statuses; } /** * Delete draft orders older than a day in batches of 20. * * Ran on a daily cron schedule. * * @internal */ public function delete_expired_draft_orders() { $count = 0; $batch_size = 20; $this->ensure_draft_status_registered(); $orders = wc_get_orders( [ 'date_modified' => '<=' . strtotime( '-1 DAY' ), 'limit' => $batch_size, 'status' => self::DB_STATUS, 'type' => 'shop_order', ] ); // do we bail because the query results are unexpected? try { $this->assert_order_results( $orders, $batch_size ); if ( $orders ) { foreach ( $orders as $order ) { $order->delete( true ); $count ++; } } if ( $batch_size === $count && function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( 'woocommerce_cleanup_draft_orders' ); } } catch ( Exception $error ) { wc_caught_exception( $error, __METHOD__ ); } } /** * Since it's possible for third party code to clobber the `$wp_post_statuses` global, * we need to do a final check here to make sure the draft post status is * registered with the global so that it is not removed by WP_Query status * validation checks. */ private function ensure_draft_status_registered() { $is_registered = get_post_stati( [ 'name' => self::DB_STATUS ] ); if ( empty( $is_registered ) ) { register_post_status( self::DB_STATUS, $this->get_post_status_properties() ); } } /** * Asserts whether incoming order results are expected given the query * this service class executes. * * @param WC_Order[] $order_results The order results being asserted. * @param int $expected_batch_size The expected batch size for the results. * @throws Exception If any assertions fail, an exception is thrown. */ private function assert_order_results( $order_results, $expected_batch_size ) { // if not an array, then just return because it won't get handled // anyways. if ( ! is_array( $order_results ) ) { return; } $suffix = ' This is an indicator that something is filtering WooCommerce or WordPress queries and modifying the query parameters.'; // if count is greater than our expected batch size, then that's a problem. if ( count( $order_results ) > 20 ) { throw new Exception( 'There are an unexpected number of results returned from the query.' . $suffix ); } // if any of the returned orders are not draft (or not a WC_Order), then that's a problem. foreach ( $order_results as $order ) { if ( ! ( $order instanceof WC_Order ) ) { throw new Exception( 'The returned results contain a value that is not a WC_Order.' . $suffix ); } if ( ! $order->has_status( self::STATUS ) ) { throw new Exception( 'The results contain an order that is not a `wc-checkout-draft` status in the results.' . $suffix ); } } } } packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php 0000644 00000005563 15132754524 0021513 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; /** * Service class to integrate Blocks with the Google Analytics extension, */ class GoogleAnalytics { /** * Instance of the asset API. * * @var AssetApi */ protected $asset_api; /** * Constructor. * * @param AssetApi $asset_api Instance of the asset API. */ public function __construct( AssetApi $asset_api ) { $this->asset_api = $asset_api; $this->init(); } /** * Hook into WP. */ protected function init() { add_action( 'init', array( $this, 'register_assets' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 ); } /** * Register scripts. */ public function register_assets() { $this->asset_api->register_script( 'wc-blocks-google-analytics', 'build/wc-blocks-google-analytics.js', [ 'google-tag-manager' ] ); } /** * Enqueue the Google Tag Manager script if prerequisites are met. */ public function enqueue_scripts() { $settings = $this->get_google_analytics_settings(); // Require tracking to be enabled with a valid GA ID. if ( ! stristr( $settings['ga_id'], 'G-' ) || apply_filters( 'woocommerce_ga_disable_tracking', ! wc_string_to_bool( $settings['ga_event_tracking_enabled'] ) ) ) { return; } if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) { // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false ); wp_add_inline_script( 'google-tag-manager', " window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '" . esc_js( $settings['ga_id'] ) . "', { 'send_page_view': false });" ); } wp_enqueue_script( 'wc-blocks-google-analytics' ); } /** * Get settings from the GA integration extension. * * @return array */ private function get_google_analytics_settings() { return wp_parse_args( get_option( 'woocommerce_google_analytics_settings' ), [ 'ga_id' => '', 'ga_event_tracking_enabled' => 'no', ] ); } /** * Add async to script tags with defined handles. * * @param string $tag HTML for the script tag. * @param string $handle Handle of script. * @param string $src Src of script. * @return string */ public function async_script_loader_tags( $tag, $handle, $src ) { if ( ! in_array( $handle, array( 'google-tag-manager' ), true ) ) { return $tag; } // If script was output manually in wp_head, abort. if ( did_action( 'woocommerce_gtag_snippet' ) ) { return ''; } return str_replace( '<script src', '<script async src', $tag ); } } packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php 0000644 00000011133 15132754524 0023234 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services\Email; use \WP_User; use \WC_Email; use Automattic\WooCommerce\Blocks\Domain\Package; /** * Customer New Account. * * An email sent to the customer when they create an account. * This is intended as a replacement to WC_Email_Customer_New_Account(), * with a set password link instead of emailing the new password in email * content. * * @extends WC_Email */ class CustomerNewAccount extends \WC_Email { /** * User login name. * * @var string */ public $user_login; /** * User email. * * @var string */ public $user_email; /** * Magic link to set initial password. * * @var string */ public $set_password_url; /** * Override (force) default template path * * @var string */ public $default_template_path; /** * Constructor. * * @param Package $package An instance of (Woo Blocks) Package. */ public function __construct( Package $package ) { // Note - we're using the same ID as the real email. // This ensures that any merchant tweaks (Settings > Emails) // apply to this email (consistent with the core email). $this->id = 'customer_new_account'; $this->customer_email = true; $this->title = __( 'New account', 'woocommerce' ); $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account blocks.', 'woocommerce' ); $this->template_html = 'emails/customer-new-account-blocks.php'; $this->template_plain = 'emails/plain/customer-new-account-blocks.php'; $this->default_template_path = $package->get_path( '/templates/' ); // Call parent constructor. parent::__construct(); } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject() { return __( 'Your {site_title} account has been created!', 'woocommerce' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading() { return __( 'Welcome to {site_title}', 'woocommerce' ); } /** * Trigger. * * @param int $user_id User ID. * @param string $user_pass User password. * @param bool $password_generated Whether the password was generated automatically or not. */ public function trigger( $user_id, $user_pass = '', $password_generated = false ) { $this->setup_locale(); if ( $user_id ) { $this->object = new \WP_User( $user_id ); // Generate a magic link so user can set initial password. $key = get_password_reset_key( $this->object ); if ( ! is_wp_error( $key ) ) { $action = 'newaccount'; $this->set_password_url = wc_get_account_endpoint_url( 'lost-password' ) . "?action=$action&key=$key&login=" . rawurlencode( $this->object->user_login ); } $this->user_login = stripslashes( $this->object->user_login ); $this->user_email = stripslashes( $this->object->user_email ); $this->recipient = $this->user_email; } if ( $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments(), $this->set_password_url ); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->template_html, array( 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'user_login' => $this->user_login, 'blogname' => $this->get_blogname(), 'set_password_url' => $this->set_password_url, 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ), '', $this->default_template_path ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->template_plain, array( 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'user_login' => $this->user_login, 'blogname' => $this->get_blogname(), 'set_password_url' => $this->set_password_url, 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ), '', $this->default_template_path ); } /** * Default content to show below main email content. * * @since 3.7.0 * @return string */ public function get_default_additional_content() { return __( 'We look forward to seeing you soon.', 'woocommerce' ); } } packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php 0000644 00000006777 15132754524 0021164 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; /** * Service class that handles the feature flags. * * @internal */ class FeatureGating { /** * Current flag value. * * @var int */ private $flag; const EXPERIMENTAL_FLAG = 3; const FEATURE_PLUGIN_FLAG = 2; const CORE_FLAG = 1; /** * Current environment * * @var string */ private $environment; const PRODUCTION_ENVIRONMENT = 'production'; const DEVELOPMENT_ENVIRONMENT = 'development'; const TEST_ENVIRONMENT = 'test'; /** * Constructor * * @param int $flag Hardcoded flag value. Useful for tests. * @param string $environment Hardcoded environment value. Useful for tests. */ public function __construct( $flag = 0, $environment = 'unset' ) { $this->flag = $flag; $this->environment = $environment; $this->load_flag(); $this->load_environment(); } /** * Set correct flag. */ public function load_flag() { if ( 0 === $this->flag ) { $default_flag = defined( 'WC_BLOCKS_IS_FEATURE_PLUGIN' ) ? self::FEATURE_PLUGIN_FLAG : self::CORE_FLAG; if ( file_exists( __DIR__ . '/../../../blocks.ini' ) ) { $allowed_flags = [ self::EXPERIMENTAL_FLAG, self::FEATURE_PLUGIN_FLAG, self::CORE_FLAG ]; $woo_options = parse_ini_file( __DIR__ . '/../../../blocks.ini' ); $this->flag = is_array( $woo_options ) && in_array( intval( $woo_options['woocommerce_blocks_phase'] ), $allowed_flags, true ) ? $woo_options['woocommerce_blocks_phase'] : $default_flag; } else { $this->flag = $default_flag; } } } /** * Set correct environment. */ public function load_environment() { if ( 'unset' === $this->environment ) { if ( file_exists( __DIR__ . '/../../../blocks.ini' ) ) { $allowed_environments = [ self::PRODUCTION_ENVIRONMENT, self::DEVELOPMENT_ENVIRONMENT, self::TEST_ENVIRONMENT ]; $woo_options = parse_ini_file( __DIR__ . '/../../../blocks.ini' ); $this->environment = is_array( $woo_options ) && in_array( $woo_options['woocommerce_blocks_env'], $allowed_environments, true ) ? $woo_options['woocommerce_blocks_env'] : self::PRODUCTION_ENVIRONMENT; } else { $this->environment = self::PRODUCTION_ENVIRONMENT; } } } /** * Returns the current flag value. * * @return int */ public function get_flag() { return $this->flag; } /** * Checks if we're executing the code in an experimental build mode. * * @return boolean */ public function is_experimental_build() { return $this->flag >= self::EXPERIMENTAL_FLAG; } /** * Checks if we're executing the code in an feature plugin or experimental build mode. * * @return boolean */ public function is_feature_plugin_build() { return $this->flag >= self::FEATURE_PLUGIN_FLAG; } /** * Returns the current environment value. * * @return string */ public function get_environment() { return $this->environment; } /** * Checks if we're executing the code in an development environment. * * @return boolean */ public function is_development_environment() { return self::DEVELOPMENT_ENVIRONMENT === $this->environment; } /** * Checks if we're executing the code in a production environment. * * @return boolean */ public function is_production_environment() { return self::PRODUCTION_ENVIRONMENT === $this->environment; } /** * Checks if we're executing the code in a test environment. * * @return boolean */ public function is_test_environment() { return self::TEST_ENVIRONMENT === $this->environment; } } packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php 0000644 00000026270 15132754524 0021144 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; use Automattic\WooCommerce\Blocks\StoreApi\Formatters; use Throwable; use Exception; /** * Service class to provide utility functions to extend REST API. */ final class ExtendRestApi { /** * List of Store API schema that is allowed to be extended by extensions. * * @var array */ private $endpoints = [ \Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartItemSchema::IDENTIFIER, \Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema::IDENTIFIER, \Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema::IDENTIFIER, ]; /** * Holds the Package instance * * @var Package */ private $package; /** * Holds the formatters class instance. * * @var Formatters */ private $formatters; /** * Constructor * * @param Package $package An instance of the package class. * @param Formatters $formatters An instance of the formatters class. */ public function __construct( Package $package, Formatters $formatters ) { $this->package = $package; $this->formatters = $formatters; } /** * Returns a formatter instance. * * @param string $name Formatter name. * @return FormatterInterface */ public function get_formatter( $name ) { return $this->formatters->$name; } /** * Data to be extended * * @var array */ private $extend_data = []; /** * Data to be extended * * @var array */ private $callback_methods = []; /** * Array of payment requirements * * @var array */ private $payment_requirements = []; /** * An endpoint that validates registration method call * * @param array $args { * An array of elements that make up a post to update or insert. * * @type string $endpoint The endpoint to extend. * @type string $namespace Plugin namespace. * @type callable $schema_callback Callback executed to add schema data. * @type callable $data_callback Callback executed to add endpoint data. * @type string $schema_type The type of data, object or array. * } * * @throws Exception On failure to register. * @return boolean True on success. */ public function register_endpoint_data( $args ) { if ( ! is_string( $args['namespace'] ) ) { $this->throw_exception( 'You must provide a plugin namespace when extending a Store REST endpoint.' ); } if ( ! is_string( $args['endpoint'] ) || ! in_array( $args['endpoint'], $this->endpoints, true ) ) { $this->throw_exception( sprintf( 'You must provide a valid Store REST endpoint to extend, valid endpoints are: %1$s. You provided %2$s.', implode( ', ', $this->endpoints ), $args['endpoint'] ) ); } if ( isset( $args['schema_callback'] ) && ! is_callable( $args['schema_callback'] ) ) { $this->throw_exception( '$schema_callback must be a callable function.' ); } if ( isset( $args['data_callback'] ) && ! is_callable( $args['data_callback'] ) ) { $this->throw_exception( '$data_callback must be a callable function.' ); } if ( isset( $args['schema_type'] ) && ! in_array( $args['schema_type'], [ ARRAY_N, ARRAY_A ], true ) ) { $this->throw_exception( sprintf( 'Data type must be either ARRAY_N for a numeric array or ARRAY_A for an object like array. You provided %1$s.', $args['schema_type'] ) ); } $this->extend_data[ $args['endpoint'] ][ $args['namespace'] ] = [ 'schema_callback' => isset( $args['schema_callback'] ) ? $args['schema_callback'] : null, 'data_callback' => isset( $args['data_callback'] ) ? $args['data_callback'] : null, 'schema_type' => isset( $args['schema_type'] ) ? $args['schema_type'] : ARRAY_A, ]; return true; } /** * Add callback functions that can be executed by the cart/extensions endpoint. * * @param array $args { * An array of elements that make up the callback configuration. * * @type string $endpoint The endpoint to extend. * @type string $namespace Plugin namespace. * @type callable $callback The function/callable to execute. * } * * @throws RouteException On failure to register. * @returns boolean True on success. */ public function register_update_callback( $args ) { if ( ! array_key_exists( 'namespace', $args ) || ! is_string( $args['namespace'] ) ) { throw new RouteException( 'woocommerce_rest_cart_extensions_error', 'You must provide a plugin namespace when extending a Store REST endpoint.', 400 ); } if ( ! array_key_exists( 'callback', $args ) || ! is_callable( $args['callback'] ) ) { throw new RouteException( 'woocommerce_rest_cart_extensions_error', 'There is no valid callback supplied to register_update_callback.', 400 ); } $this->callback_methods[ $args['namespace'] ] = [ 'callback' => $args['callback'], ]; return true; } /** * Get callback for a specific endpoint and namespace. * * @param string $namespace The namespace to get callbacks for. * * @return callable The callback registered by the extension. * @throws RouteException When callback is not callable or parameters are incorrect. */ public function get_update_callback( $namespace ) { $method = null; if ( ! is_string( $namespace ) ) { throw new RouteException( 'woocommerce_rest_cart_extensions_error', 'You must provide a plugin namespace when extending a Store REST endpoint.', 400 ); } if ( ! array_key_exists( $namespace, $this->callback_methods ) ) { throw new RouteException( 'woocommerce_rest_cart_extensions_error', sprintf( 'There is no such namespace registered: %1$s.', $namespace ), 400 ); } if ( ! array_key_exists( 'callback', $this->callback_methods[ $namespace ] ) || ! is_callable( $this->callback_methods[ $namespace ]['callback'] ) ) { throw new RouteException( 'woocommerce_rest_cart_extensions_error', sprintf( 'There is no valid callback registered for: %1$s.', $namespace ), 400 ); } return $this->callback_methods[ $namespace ]['callback']; } /** * Returns the registered endpoint data * * @param string $endpoint A valid identifier. * @param array $passed_args Passed arguments from the Schema class. * @return object Returns an casted object with registered endpoint data. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_endpoint_data( $endpoint, array $passed_args = [] ) { $registered_data = []; if ( ! isset( $this->extend_data[ $endpoint ] ) ) { return (object) $registered_data; } foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { $data = []; if ( is_null( $callbacks['data_callback'] ) ) { continue; } try { $data = $callbacks['data_callback']( ...$passed_args ); if ( ! is_array( $data ) ) { throw new Exception( '$data_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $registered_data[ $namespace ] = $data; } return (object) $registered_data; } /** * Returns the registered endpoint schema * * @param string $endpoint A valid identifier. * @param array $passed_args Passed arguments from the Schema class. * @return array Returns an array with registered schema data. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_endpoint_schema( $endpoint, array $passed_args = [] ) { $registered_schema = []; if ( ! isset( $this->extend_data[ $endpoint ] ) ) { return (object) $registered_schema; } foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { $schema = []; if ( is_null( $callbacks['schema_callback'] ) ) { continue; } try { $schema = $callbacks['schema_callback']( ...$passed_args ); if ( ! is_array( $schema ) ) { throw new Exception( '$schema_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $schema = $this->format_extensions_properties( $namespace, $schema, $callbacks['schema_type'] ); $registered_schema[ $namespace ] = $schema; } return (object) $registered_schema; } /** * Registers and validates payment requirements callbacks. * * @param array $args { * Array of registration data. * * @type callable $data_callback Callback executed to add payment requirements data. * } * * @throws Exception On failure to register. * @return boolean True on success. */ public function register_payment_requirements( $args ) { if ( ! is_callable( $args['data_callback'] ) ) { $this->throw_exception( '$data_callback must be a callable function.' ); } $this->payment_requirements[] = $args['data_callback']; return true; } /** * Returns the additional payment requirements. * * @param array $initial_requirements list of requirements that should be added to the collected requirements. * @return array Returns a list of payment requirements. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_payment_requirements( array $initial_requirements = [ 'products' ] ) { $requirements = $initial_requirements; if ( empty( $this->payment_requirements ) ) { return $initial_requirements; } foreach ( $this->payment_requirements as $callback ) { $data = []; try { $data = $callback(); if ( ! is_array( $data ) ) { throw new Exception( '$data_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $requirements = array_merge( $requirements, $data ); } return array_unique( $requirements ); } /** * Throws error and/or silently logs it. * * @param string|Throwable $exception_or_error Error message or Exception. * @throws Exception An error to throw if we have debug enabled and user is admin. */ private function throw_exception( $exception_or_error ) { if ( is_string( $exception_or_error ) ) { $exception = new Exception( $exception_or_error ); } else { $exception = $exception_or_error; } // Always log an error. wc_caught_exception( $exception ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'manage_woocommerce' ) ) { throw $exception; } } /** * Format schema for an extension. * * @param string $namespace Error message or Exception. * @param array $schema An error to throw if we have debug enabled and user is admin. * @param string $schema_type How should data be shaped. * * @return array Formatted schema. */ private function format_extensions_properties( $namespace, $schema, $schema_type ) { if ( ARRAY_N === $schema_type ) { return [ /* translators: %s: extension namespace */ 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => $schema, ]; } return [ /* translators: %s: extension namespace */ 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'properties' => $schema, ]; } } packages/woocommerce-blocks/src/Assets.php 0000644 00000005427 15132754524 0014676 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; /** * Assets class. * * @deprecated 5.0.0 This class will be removed in a future release. This has been replaced by AssetsController. * @internal */ class Assets { /** * Initialize class features on init. * * @since 2.5.0 * @deprecated 5.0.0 */ public static function init() { _deprecated_function( 'Assets::init', '5.0.0' ); } /** * Register block scripts & styles. * * @since 2.5.0 * @deprecated 5.0.0 */ public static function register_assets() { _deprecated_function( 'Assets::register_assets', '5.0.0' ); } /** * Register the vendors style file. We need to do it after the other files * because we need to check if `wp-edit-post` has been enqueued. * * @deprecated 5.0.0 */ public static function enqueue_scripts() { _deprecated_function( 'Assets::enqueue_scripts', '5.0.0' ); } /** * Add body classes. * * @deprecated 5.0.0 * @param array $classes Array of CSS classnames. * @return array Modified array of CSS classnames. */ public static function add_theme_body_class( $classes = [] ) { _deprecated_function( 'Assets::add_theme_body_class', '5.0.0' ); return $classes; } /** * Add theme class to admin body. * * @deprecated 5.0.0 * @param array $classes String with the CSS classnames. * @return array Modified string of CSS classnames. */ public static function add_theme_admin_body_class( $classes = '' ) { _deprecated_function( 'Assets::add_theme_admin_body_class', '5.0.0' ); return $classes; } /** * Adds a redirect field to the login form so blocks can redirect users after login. * * @deprecated 5.0.0 */ public static function redirect_to_field() { _deprecated_function( 'Assets::redirect_to_field', '5.0.0' ); } /** * Queues a block script in the frontend. * * @since 2.3.0 * @since 2.6.0 Changed $name to $script_name and added $handle argument. * @since 2.9.0 Made it so scripts are not loaded in admin pages. * @deprecated 4.5.0 Block types register the scripts themselves. * * @param string $script_name Name of the script used to identify the file inside build folder. * @param string $handle Optional. Provided if the handle should be different than the script name. `wc-` prefix automatically added. * @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array. */ public static function register_block_script( $script_name, $handle = '', $dependencies = [] ) { _deprecated_function( 'register_block_script', '4.5.0' ); $asset_api = Package::container()->get( AssetApi::class ); $asset_api->register_block_script( $script_name, $handle, $dependencies ); } } packages/woocommerce-blocks/src/BlockTypesController.php 0000644 00000014740 15132754524 0017555 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\BlockTypes\AtomicBlock; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; /** * BlockTypesController class. * * @since 5.0.0 * @internal */ final class BlockTypesController { /** * Instance of the asset API. * * @var AssetApi */ protected $asset_api; /** * Instance of the asset data registry. * * @var AssetDataRegistry */ protected $asset_data_registry; /** * Constructor. * * @param AssetApi $asset_api Instance of the asset API. * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. */ public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) { $this->asset_api = $asset_api; $this->asset_data_registry = $asset_data_registry; $this->init(); } /** * Initialize class features. */ protected function init() { add_action( 'init', array( $this, 'register_blocks' ) ); add_filter( 'render_block', array( $this, 'add_data_attributes' ), 10, 2 ); add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) ); add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) ); } /** * Register blocks, hooking up assets and render functions as needed. */ public function register_blocks() { $block_types = $this->get_block_types(); foreach ( $block_types as $block_type ) { $block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type; $block_type_instance = new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() ); } foreach ( self::get_atomic_blocks() as $block_type ) { $block_type_instance = new AtomicBlock( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry(), $block_type ); } } /** * Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace. * * @param string $content Block content. * @param array $block Parsed block data. * @return string */ public function add_data_attributes( $content, $block ) { $block_name = $block['blockName']; $block_namespace = strtok( $block_name, '/' ); /** * WooCommerce Blocks Namespaces * * This hook defines which block namespaces should have block name and attribute data- attributes appended on render. * * @param array $allowed_namespaces List of namespaces. */ $allowed_namespaces = array_merge( [ 'woocommerce', 'woocommerce-checkout' ], (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', [] ) ); /** * WooCommerce Blocks Block Names * * This hook defines which block names should have block name and attribute data- attributes appended on render. * * @param array $allowed_namespaces List of namespaces. */ $allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', [] ); if ( ! in_array( $block_namespace, $allowed_namespaces, true ) && ! in_array( $block_name, $allowed_blocks, true ) ) { return $content; } $attributes = (array) $block['attrs']; $escaped_data_attributes = [ 'data-block-name="' . esc_attr( $block['blockName'] ) . '"', ]; foreach ( $attributes as $key => $value ) { if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } if ( ! is_scalar( $value ) ) { $value = wp_json_encode( $value ); } $escaped_data_attributes[] = 'data-' . esc_attr( strtolower( preg_replace( '/(?<!\ )[A-Z]/', '-$0', $key ) ) ) . '="' . esc_attr( $value ) . '"'; } return preg_replace( '/^<div /', '<div ' . implode( ' ', $escaped_data_attributes ) . ' ', trim( $content ) ); } /** * Adds a redirect field to the login form so blocks can redirect users after login. */ public function redirect_to_field() { // phpcs:ignore WordPress.Security.NonceVerification if ( empty( $_GET['redirect_to'] ) ) { return; } echo '<input type="hidden" name="redirect" value="' . esc_attr( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) . '" />'; // phpcs:ignore WordPress.Security.NonceVerification } /** * Hide legacy widgets with a feature complete block equivalent in the inserter * and prevent them from showing as an option in the Legacy Widget block. * * @param array $widget_types An array of widgets hidden in core. * @return array $widget_types An array inluding the WooCommerce widgets to hide. */ public function hide_legacy_widgets_with_block_equivalent( $widget_types ) { array_push( $widget_types, 'woocommerce_product_search', 'woocommerce_product_categories', 'woocommerce_recent_reviews' ); return $widget_types; } /** * Get list of block types. * * @return array */ protected function get_block_types() { global $wp_version, $pagenow; $block_types = [ 'AllReviews', 'FeaturedCategory', 'FeaturedProduct', 'HandpickedProducts', 'ProductBestSellers', 'ProductCategories', 'ProductCategory', 'ProductNew', 'ProductOnSale', 'ProductsByAttribute', 'ProductTopRated', 'ReviewsByProduct', 'ReviewsByCategory', 'ProductSearch', 'ProductTag', 'AllProducts', 'PriceFilter', 'AttributeFilter', 'StockFilter', 'ActiveFilters', ]; if ( Package::feature()->is_feature_plugin_build() ) { $block_types[] = 'Checkout'; $block_types[] = 'Cart'; } if ( Package::feature()->is_experimental_build() ) { $block_types[] = 'SingleProduct'; $block_types[] = 'CartI2'; $block_types[] = 'MiniCart'; } /** * This disables specific blocks in Widget Areas by not registering them. */ if ( in_array( $pagenow, [ 'widgets.php', 'themes.php', 'customize.php' ], true ) ) { $block_types = array_diff( $block_types, [ 'AllProducts', 'PriceFilter', 'AttributeFilter', 'StockFilter', 'ActiveFilters', 'Cart', 'Checkout', ] ); } return $block_types; } /** * Get atomic blocks types. * * @return array */ protected function get_atomic_blocks() { return [ 'product-title', 'product-button', 'product-image', 'product-price', 'product-rating', 'product-sale-badge', 'product-summary', 'product-sku', 'product-category-list', 'product-tag-list', 'product-stock-indicator', 'product-add-to-cart', ]; } } packages/woocommerce-blocks/src/Library.php 0000644 00000002076 15132754524 0015035 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\BlockTypes\AtomicBlock; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; /** * Library class. * * @deprecated 5.0.0 This class will be removed in a future release. This has been replaced by BlockTypesController. * @internal */ class Library { /** * Initialize block library features. * * @deprecated 5.0.0 */ public static function init() { _deprecated_function( 'Library::init', '5.0.0' ); } /** * Register custom tables within $wpdb object. * * @deprecated 5.0.0 */ public static function define_tables() { _deprecated_function( 'Library::define_tables', '5.0.0' ); } /** * Register blocks, hooking up assets and render functions as needed. * * @deprecated 5.0.0 */ public static function register_blocks() { _deprecated_function( 'Library::register_blocks', '5.0.0' ); } } packages/woocommerce-blocks/src/StoreApi/SchemaController.php 0000644 00000011364 15132754524 0020423 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi; use Exception; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\AbstractSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\BillingAddressSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ShippingAddressSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartShippingRateSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartItemSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartCouponSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartExtensionsSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartFeeSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ErrorSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ProductSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ImageAttachmentSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ProductAttributeSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ProductCategorySchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ProductCollectionDataSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ProductReviewSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\TermSchema; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * SchemaController class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class SchemaController { /** * Stores schema class instances. * * @var AbstractSchema[] */ protected $schemas = []; /** * Stores Rest Extending instance * * @var ExtendRestApi */ private $extend; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. */ public function __construct( ExtendRestApi $extend ) { $this->extend = $extend; $this->initialize(); } /** * Get a schema class instance. * * @throws Exception If the schema does not exist. * * @param string $name Name of schema. * @return AbstractSchema */ public function get( $name ) { if ( ! isset( $this->schemas[ $name ] ) ) { throw new Exception( $name . ' schema does not exist' ); } return $this->schemas[ $name ]; } /** * Load schema class instances. */ protected function initialize() { $this->schemas = []; $this->schemas[ ErrorSchema::IDENTIFIER ] = new ErrorSchema( $this->extend ); $this->schemas[ ImageAttachmentSchema::IDENTIFIER ] = new ImageAttachmentSchema( $this->extend ); $this->schemas[ TermSchema::IDENTIFIER ] = new TermSchema( $this->extend ); $this->schemas[ BillingAddressSchema::IDENTIFIER ] = new BillingAddressSchema( $this->extend ); $this->schemas[ ShippingAddressSchema::IDENTIFIER ] = new ShippingAddressSchema( $this->extend ); $this->schemas[ CartShippingRateSchema::IDENTIFIER ] = new CartShippingRateSchema( $this->extend ); $this->schemas[ CartCouponSchema::IDENTIFIER ] = new CartCouponSchema( $this->extend ); $this->schemas[ CartFeeSchema::IDENTIFIER ] = new CartFeeSchema( $this->extend ); $this->schemas[ CartItemSchema::IDENTIFIER ] = new CartItemSchema( $this->extend, $this->schemas[ ImageAttachmentSchema::IDENTIFIER ] ); $this->schemas[ CartSchema::IDENTIFIER ] = new CartSchema( $this->extend, $this->schemas[ CartItemSchema::IDENTIFIER ], $this->schemas[ CartCouponSchema::IDENTIFIER ], $this->schemas[ CartFeeSchema::IDENTIFIER ], $this->schemas[ CartShippingRateSchema::IDENTIFIER ], $this->schemas[ ShippingAddressSchema::IDENTIFIER ], $this->schemas[ BillingAddressSchema::IDENTIFIER ], $this->schemas[ ErrorSchema::IDENTIFIER ] ); $this->schemas[ CartExtensionsSchema::IDENTIFIER ] = new CartExtensionsSchema( $this->extend ); $this->schemas[ CheckoutSchema::IDENTIFIER ] = new CheckoutSchema( $this->extend, $this->schemas[ BillingAddressSchema::IDENTIFIER ], $this->schemas[ ShippingAddressSchema::IDENTIFIER ] ); $this->schemas[ ProductSchema::IDENTIFIER ] = new ProductSchema( $this->extend, $this->schemas[ ImageAttachmentSchema::IDENTIFIER ] ); $this->schemas[ ProductAttributeSchema::IDENTIFIER ] = new ProductAttributeSchema( $this->extend ); $this->schemas[ ProductCategorySchema::IDENTIFIER ] = new ProductCategorySchema( $this->extend, $this->schemas[ ImageAttachmentSchema::IDENTIFIER ] ); $this->schemas[ ProductCollectionDataSchema::IDENTIFIER ] = new ProductCollectionDataSchema( $this->extend ); $this->schemas[ ProductReviewSchema::IDENTIFIER ] = new ProductReviewSchema( $this->extend, $this->schemas[ ImageAttachmentSchema::IDENTIFIER ] ); } } packages/woocommerce-blocks/src/StoreApi/Formatters.php 0000644 00000002346 15132754524 0017305 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi; use \Exception; use Automattic\WooCommerce\Blocks\StoreApi\Formatters\DefaultFormatter; /** * Formatters class. * * Allows formatter classes to be registered. Formatters are exposed to extensions via the ExtendRestApi class. */ class Formatters { /** * Holds an array of formatter class instances. * * @var array */ private $formatters = []; /** * Get a new instance of a formatter class. * * @throws Exception An Exception is thrown if a non-existing formatter is used and the user is admin. * * @param string $name Name of the formatter. * @return FormatterInterface Formatter class instance. */ public function __get( $name ) { if ( ! isset( $this->formatters[ $name ] ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'manage_woocommerce' ) ) { throw new Exception( $name . ' formatter does not exist' ); } return new DefaultFormatter(); } return $this->formatters[ $name ]; } /** * Register a formatter class for usage. * * @param string $name Name of the formatter. * @param string $class A formatter class name. */ public function register( $name, $class ) { $this->formatters[ $name ] = new $class(); } } packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php 0000644 00000000542 15132754524 0024362 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * PartialOutOfStockException class. * * @internal This API is used internally by Blocks, this exception is thrown when an item in a draft order has a * quantity greater than what is available in stock. */ class PartialOutOfStockException extends StockAvailabilityException { } packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php 0000644 00000000474 15132754524 0023723 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * NotPurchasableException class. * * @internal This API is used internally by Blocks, this exception is thrown when an item in the cart is not able to be * purchased. */ class NotPurchasableException extends StockAvailabilityException { } packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php 0000644 00000004254 15132754524 0021223 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * Pagination class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class Pagination { /** * Add pagination headers to a response object. * * @param \WP_REST_Response $response Reference to the response object. * @param \WP_REST_Request $request The request object. * @param int $total_items Total items found. * @param int $total_pages Total pages found. * @return \WP_REST_Response */ public function add_headers( $response, $request, $total_items, $total_pages ) { $response->header( 'X-WP-Total', $total_items ); $response->header( 'X-WP-TotalPages', $total_pages ); $current_page = $this->get_current_page( $request ); $link_base = $this->get_link_base( $request ); if ( $current_page > 1 ) { $previous_page = $current_page - 1; if ( $previous_page > $total_pages ) { $previous_page = $total_pages; } $this->add_page_link( $response, 'prev', $previous_page, $link_base ); } if ( $total_pages > $current_page ) { $this->add_page_link( $response, 'next', ( $current_page + 1 ), $link_base ); } return $response; } /** * Get current page. * * @param \WP_REST_Request $request The request object. * @return int Get the page from the request object. */ protected function get_current_page( $request ) { return (int) $request->get_param( 'page' ); } /** * Get base for links from the request object. * * @param \WP_REST_Request $request The request object. * @return string */ protected function get_link_base( $request ) { return add_query_arg( $request->get_query_params(), rest_url( $request->get_route() ) ); } /** * Add a page link. * * @param \WP_REST_Response $response Reference to the response object. * @param string $name Page link name. e.g. prev. * @param int $page Page number. * @param string $link_base Base URL. */ protected function add_page_link( &$response, $name, $page, $link_base ) { $response->link_header( $name, add_query_arg( 'page', $page, $link_base ) ); } } packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php 0000644 00000020107 15132754524 0023124 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\ProductQuery; /** * Product Query filters class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ProductQueryFilters { /** * Get filtered min price for current products. * * @param \WP_REST_Request $request The request object. * @return array */ public function get_filtered_price( $request ) { global $wpdb; // Regenerate the products query without min/max price request params. unset( $request['min_price'], $request['max_price'] ); // Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products. $product_query = new ProductQuery(); add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 ); add_filter( 'posts_pre_query', '__return_empty_array' ); $query_args = $product_query->prepare_objects_query( $request ); $query_args['no_found_rows'] = true; $query_args['posts_per_page'] = -1; $query = new \WP_Query(); $result = $query->query( $query_args ); $product_query_sql = $query->request; remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 ); remove_filter( 'posts_pre_query', '__return_empty_array' ); $price_filter_sql = " SELECT min( min_price ) as min_price, MAX( max_price ) as max_price FROM {$wpdb->wc_product_meta_lookup} WHERE product_id IN ( {$product_query_sql} ) "; return $wpdb->get_row( $price_filter_sql ); // phpcs:ignore } /** * Get stock status counts for the current products. * * @param \WP_REST_Request $request The request object. * @return array status=>count pairs. */ public function get_stock_status_counts( $request ) { global $wpdb; $product_query = new ProductQuery(); $stock_status_options = array_map( 'esc_sql', array_keys( wc_get_product_stock_status_options() ) ); $hide_outofstock_items = get_option( 'woocommerce_hide_out_of_stock_items' ); if ( 'yes' === $hide_outofstock_items ) { unset( $stock_status_options['outofstock'] ); } add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 ); add_filter( 'posts_pre_query', '__return_empty_array' ); $query_args = $product_query->prepare_objects_query( $request ); unset( $query_args['stock_status'] ); $query_args['no_found_rows'] = true; $query_args['posts_per_page'] = -1; $query = new \WP_Query(); $result = $query->query( $query_args ); $product_query_sql = $query->request; remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 ); remove_filter( 'posts_pre_query', '__return_empty_array' ); $stock_status_counts = array(); foreach ( $stock_status_options as $status ) { $stock_status_count_sql = $this->generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options ); $result = $wpdb->get_row( $stock_status_count_sql ); // phpcs:ignore $stock_status_counts[ $status ] = $result->status_count; } return $stock_status_counts; } /** * Generate calculate query by stock status. * * @param string $status status to calculate. * @param string $product_query_sql product query for current filter state. * @param array $stock_status_options available stock status options. * * @return false|string */ private function generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options ) { if ( ! in_array( $status, $stock_status_options, true ) ) { return false; } global $wpdb; $status = esc_sql( $status ); return " SELECT COUNT( DISTINCT posts.ID ) as status_count FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status' AND postmeta.meta_value = '{$status}' WHERE posts.ID IN ( {$product_query_sql} ) "; } /** * Get attribute counts for the current products. * * @param \WP_REST_Request $request The request object. * @param array $attributes Attributes to count, either names or ids. * @return array termId=>count pairs. */ public function get_attribute_counts( $request, $attributes = [] ) { global $wpdb; // Remove paging and sorting params from the request. $request->set_param( 'page', null ); $request->set_param( 'per_page', null ); $request->set_param( 'order', null ); $request->set_param( 'orderby', null ); // Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products. $product_query = new ProductQuery(); add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 ); add_filter( 'posts_pre_query', '__return_empty_array' ); $query_args = $product_query->prepare_objects_query( $request ); $query_args['no_found_rows'] = true; $query_args['posts_per_page'] = -1; $query = new \WP_Query(); $result = $query->query( $query_args ); $product_query_sql = $query->request; remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 ); remove_filter( 'posts_pre_query', '__return_empty_array' ); if ( count( $attributes ) === count( array_filter( $attributes, 'is_numeric' ) ) ) { $attributes = array_map( 'wc_attribute_taxonomy_name_by_id', wp_parse_id_list( $attributes ) ); } $attributes_to_count = array_map( function( $attribute ) { $attribute = wc_sanitize_taxonomy_name( $attribute ); return esc_sql( $attribute ); }, $attributes ); $attributes_to_count_sql = 'AND term_taxonomy.taxonomy IN ("' . implode( '","', $attributes_to_count ) . '")'; $attribute_count_sql = " SELECT COUNT( DISTINCT posts.ID ) as term_count, terms.term_id as term_count_id FROM {$wpdb->posts} AS posts INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) INNER JOIN {$wpdb->terms} AS terms USING( term_id ) WHERE posts.ID IN ( {$product_query_sql} ) {$attributes_to_count_sql} GROUP BY terms.term_id "; $results = $wpdb->get_results( $attribute_count_sql ); // phpcs:ignore return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); } /** * Get rating counts for the current products. * * @param \WP_REST_Request $request The request object. * @return array rating=>count pairs. */ public function get_rating_counts( $request ) { global $wpdb; // Regenerate the products query without rating request params. unset( $request['rating'] ); // Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products. $product_query = new ProductQuery(); add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 ); add_filter( 'posts_pre_query', '__return_empty_array' ); $query_args = $product_query->prepare_objects_query( $request ); $query_args['no_found_rows'] = true; $query_args['posts_per_page'] = -1; $query = new \WP_Query(); $result = $query->query( $query_args ); $product_query_sql = $query->request; remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 ); remove_filter( 'posts_pre_query', '__return_empty_array' ); $rating_count_sql = " SELECT COUNT( DISTINCT product_id ) as product_count, ROUND( average_rating, 0 ) as rounded_average_rating FROM {$wpdb->wc_product_meta_lookup} WHERE product_id IN ( {$product_query_sql} ) AND average_rating > 0 GROUP BY rounded_average_rating ORDER BY rounded_average_rating ASC "; $results = $wpdb->get_results( $rating_count_sql ); // phpcs:ignore return array_map( 'absint', wp_list_pluck( $results, 'product_count', 'rounded_average_rating' ) ); } } packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php 0000644 00000037572 15132754524 0021611 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use WC_Tax; /** * Product Query class. * Helper class to handle product queries for the API. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ProductQuery { /** * Prepare query args to pass to WP_Query for a REST API request. * * @param \WP_REST_Request $request Request data. * @return array */ public function prepare_objects_query( $request ) { $args = [ 'offset' => $request['offset'], 'order' => $request['order'], 'orderby' => $request['orderby'], 'paged' => $request['page'], 'post__in' => $request['include'], 'post__not_in' => $request['exclude'], 'posts_per_page' => $request['per_page'] ? $request['per_page'] : -1, 'post_parent__in' => $request['parent'], 'post_parent__not_in' => $request['parent_exclude'], 'search' => $request['search'], // This uses search rather than s intentionally to handle searches internally. 'fields' => 'ids', 'ignore_sticky_posts' => true, 'post_status' => 'publish', 'date_query' => [], 'post_type' => 'product', ]; // If searching for a specific SKU, allow any post type. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = [ 'product', 'product_variation' ]; } // Taxonomy query to filter products by type, category, tag, shipping class, and attribute. $tax_query = []; // Filter product type by slug. if ( ! empty( $request['type'] ) ) { if ( 'variation' === $request['type'] ) { $args['post_type'] = 'product_variation'; } else { $args['post_type'] = 'product'; $tax_query[] = [ 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], ]; } } if ( 'date' === $args['orderby'] ) { $args['orderby'] = 'date ID'; } // Set before into date query. Date query must be specified as an array of an array. if ( isset( $request['before'] ) ) { $args['date_query'][0]['before'] = $request['before']; } // Set after into date query. Date query must be specified as an array of an array. if ( isset( $request['after'] ) ) { $args['date_query'][0]['after'] = $request['after']; } // Set date query column. Defaults to post_date. if ( isset( $request['date_column'] ) && ! empty( $args['date_query'][0] ) ) { $args['date_query'][0]['column'] = 'post_' . $request['date_column']; } // Set custom args to handle later during clauses. $custom_keys = [ 'sku', 'min_price', 'max_price', 'stock_status', ]; foreach ( $custom_keys as $key ) { if ( ! empty( $request[ $key ] ) ) { $args[ $key ] = $request[ $key ]; } } $operator_mapping = [ 'in' => 'IN', 'not_in' => 'NOT IN', 'and' => 'AND', ]; // Map between taxonomy name and arg key. $taxonomies = [ 'product_cat' => 'category', 'product_tag' => 'tag', ]; // Set tax_query for each passed arg. foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $operator = $request->get_param( $key . '_operator' ) && isset( $operator_mapping[ $request->get_param( $key . '_operator' ) ] ) ? $operator_mapping[ $request->get_param( $key . '_operator' ) ] : 'IN'; $tax_query[] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], 'operator' => $operator, ]; } } // Filter by attributes. if ( ! empty( $request['attributes'] ) ) { $att_queries = []; foreach ( $request['attributes'] as $attribute ) { if ( empty( $attribute['term_id'] ) && empty( $attribute['slug'] ) ) { continue; } if ( in_array( $attribute['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $operator = isset( $attribute['operator'], $operator_mapping[ $attribute['operator'] ] ) ? $operator_mapping[ $attribute['operator'] ] : 'IN'; $att_queries[] = [ 'taxonomy' => $attribute['attribute'], 'field' => ! empty( $attribute['term_id'] ) ? 'term_id' : 'slug', 'terms' => ! empty( $attribute['term_id'] ) ? $attribute['term_id'] : $attribute['slug'], 'operator' => $operator, ]; } } if ( 1 < count( $att_queries ) ) { // Add relation arg when using multiple attributes. $relation = $request->get_param( 'attribute_relation' ) && isset( $operator_mapping[ $request->get_param( 'attribute_relation' ) ] ) ? $operator_mapping[ $request->get_param( 'attribute_relation' ) ] : 'IN'; $tax_query[] = [ 'relation' => $relation, $att_queries, ]; } else { $tax_query = array_merge( $tax_query, $att_queries ); } } // Build tax_query if taxonomies are set. if ( ! empty( $tax_query ) ) { if ( ! empty( $args['tax_query'] ) ) { $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // phpcs:ignore } else { $args['tax_query'] = $tax_query; // phpcs:ignore } } // Filter featured. if ( is_bool( $request['featured'] ) ) { $args['tax_query'][] = [ 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', ]; } // Filter by on sale products. if ( is_bool( $request['on_sale'] ) ) { $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. $on_sale_ids = empty( $on_sale_ids ) ? [ 0 ] : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } $catalog_visibility = $request->get_param( 'catalog_visibility' ); $rating = $request->get_param( 'rating' ); $visibility_options = wc_get_product_visibility_options(); if ( in_array( $catalog_visibility, array_keys( $visibility_options ), true ) ) { $exclude_from_catalog = 'search' === $catalog_visibility ? '' : 'exclude-from-catalog'; $exclude_from_search = 'catalog' === $catalog_visibility ? '' : 'exclude-from-search'; $args['tax_query'][] = [ 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => [ $exclude_from_catalog, $exclude_from_search ], 'operator' => 'hidden' === $catalog_visibility ? 'AND' : 'NOT IN', 'rating_filter' => true, ]; } if ( $rating ) { $rating_terms = []; foreach ( $rating as $value ) { $rating_terms[] = 'rated-' . $value; } $args['tax_query'][] = [ 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => $rating_terms, ]; } $orderby = $request->get_param( 'orderby' ); $order = $request->get_param( 'order' ); $ordering_args = wc()->query->get_catalog_ordering_args( $orderby, $order ); $args['orderby'] = $ordering_args['orderby']; $args['order'] = $ordering_args['order']; if ( 'include' === $orderby ) { $args['orderby'] = 'post__in'; } elseif ( 'id' === $orderby ) { $args['orderby'] = 'ID'; // ID must be capitalized. } elseif ( 'slug' === $orderby ) { $args['orderby'] = 'name'; } if ( $ordering_args['meta_key'] ) { $args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore } return $args; } /** * Get results of query. * * @param \WP_REST_Request $request Request data. * @return array */ public function get_results( $request ) { $query_args = $this->prepare_objects_query( $request ); add_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10, 2 ); $query = new \WP_Query(); $results = $query->query( $query_args ); $total_posts = $query->found_posts; // Out-of-bounds, run the query again without LIMIT for total count. if ( $total_posts < 1 && $query_args['paged'] > 1 ) { unset( $query_args['paged'] ); $count_query = new \WP_Query(); $count_query->query( $query_args ); $total_posts = $count_query->found_posts; } remove_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10 ); return [ 'results' => $results, 'total' => (int) $total_posts, 'pages' => $query->query_vars['posts_per_page'] > 0 ? (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ) : 1, ]; } /** * Get objects. * * @param \WP_REST_Request $request Request data. * @return array */ public function get_objects( $request ) { $results = $this->get_results( $request ); return [ 'objects' => array_map( 'wc_get_product', $results['results'] ), 'total' => $results['total'], 'pages' => $results['pages'], ]; } /** * Get last modified date for all products. * * @return int timestamp. */ public function get_last_modified() { global $wpdb; return strtotime( $wpdb->get_var( "SELECT MAX( post_modified_gmt ) FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' );" ) ); } /** * Add in conditional search filters for products. * * @param array $args Query args. * @param \WC_Query $wp_query WC_Query object. * @return array */ public function add_query_clauses( $args, $wp_query ) { global $wpdb; if ( $wp_query->get( 'search' ) ) { $search = '%' . $wpdb->esc_like( $wp_query->get( 'search' ) ) . '%'; $search_query = wc_product_sku_enabled() ? $wpdb->prepare( " AND ( $wpdb->posts.post_title LIKE %s OR wc_product_meta_lookup.sku LIKE %s ) ", $search, $search ) : $wpdb->prepare( " AND $wpdb->posts.post_title LIKE %s ", $search ); $args['where'] .= $search_query; $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); } if ( $wp_query->get( 'sku' ) ) { $skus = explode( ',', $wp_query->get( 'sku' ) ); // Include the current string as a SKU too. if ( 1 < count( $skus ) ) { $skus[] = $wp_query->get( 'sku' ); } $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= ' AND wc_product_meta_lookup.sku IN ("' . implode( '","', array_map( 'esc_sql', $skus ) ) . '")'; } if ( $wp_query->get( 'stock_status' ) ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= ' AND wc_product_meta_lookup.stock_status IN ("' . implode( '","', array_map( 'esc_sql', $wp_query->get( 'stock_status' ) ) ) . '")'; } elseif ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); $args['where'] .= ' AND wc_product_meta_lookup.stock_status NOT IN ("outofstock")'; } if ( $wp_query->get( 'min_price' ) || $wp_query->get( 'max_price' ) ) { $args = $this->add_price_filter_clauses( $args, $wp_query ); } return $args; } /** * Add in conditional price filters. * * @param array $args Query args. * @param \WC_Query $wp_query WC_Query object. * @return array */ protected function add_price_filter_clauses( $args, $wp_query ) { global $wpdb; $adjust_for_taxes = $this->adjust_price_filters_for_displayed_taxes(); $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); if ( $wp_query->get( 'min_price' ) ) { $min_price_filter = $this->prepare_price_filter( $wp_query->get( 'min_price' ) ); if ( $adjust_for_taxes ) { $args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price_filter, 'min_price', '>=' ); } else { $args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', $min_price_filter ); } } if ( $wp_query->get( 'max_price' ) ) { $max_price_filter = $this->prepare_price_filter( $wp_query->get( 'max_price' ) ); if ( $adjust_for_taxes ) { $args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price_filter, 'max_price', '<=' ); } else { $args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', $max_price_filter ); } } return $args; } /** * Get query for price filters when dealing with displayed taxes. * * @param float $price_filter Price filter to apply. * @param string $column Price being filtered (min or max). * @param string $operator Comparison operator for column. * @return string Constructed query. */ protected function get_price_filter_query_for_displayed_taxes( $price_filter, $column = 'min_price', $operator = '>=' ) { global $wpdb; // Select only used tax classes to avoid unwanted calculations. $product_tax_classes = $wpdb->get_col( "SELECT DISTINCT tax_class FROM {$wpdb->wc_product_meta_lookup};" ); if ( empty( $product_tax_classes ) ) { return ''; } $or_queries = []; // We need to adjust the filter for each possible tax class and combine the queries into one. foreach ( $product_tax_classes as $tax_class ) { $adjusted_price_filter = $this->adjust_price_filter_for_tax_class( $price_filter, $tax_class ); $or_queries[] = $wpdb->prepare( '( wc_product_meta_lookup.tax_class = %s AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )', $tax_class, $adjusted_price_filter ); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared return $wpdb->prepare( ' AND ( wc_product_meta_lookup.tax_status = "taxable" AND ( 0=1 OR ' . implode( ' OR ', $or_queries ) . ') OR ( wc_product_meta_lookup.tax_status != "taxable" AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f ) ) ', $price_filter ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared } /** * If price filters need adjustment to work with displayed taxes, this returns true. * * This logic is used when prices are stored in the database differently to how they are being displayed, with regards * to taxes. * * @return boolean */ protected function adjust_price_filters_for_displayed_taxes() { $display = get_option( 'woocommerce_tax_display_shop' ); $database = wc_prices_include_tax() ? 'incl' : 'excl'; return $display !== $database; } /** * Converts price filter from subunits to decimal. * * @param string|int $price_filter Raw price filter in subunit format. * @return float Price filter in decimal format. */ protected function prepare_price_filter( $price_filter ) { return floatval( $price_filter / ( 10 ** wc_get_price_decimals() ) ); } /** * Adjusts a price filter based on a tax class and whether or not the amount includes or excludes taxes. * * This calculation logic is based on `wc_get_price_excluding_tax` and `wc_get_price_including_tax` in core. * * @param float $price_filter Price filter amount as entered. * @param string $tax_class Tax class for adjustment. * @return float */ protected function adjust_price_filter_for_tax_class( $price_filter, $tax_class ) { $tax_display = get_option( 'woocommerce_tax_display_shop' ); $tax_rates = WC_Tax::get_rates( $tax_class ); $base_tax_rates = WC_Tax::get_base_tax_rates( $tax_class ); // If prices are shown incl. tax, we want to remove the taxes from the filter amount to match prices stored excl. tax. if ( 'incl' === $tax_display ) { $taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $price_filter, $base_tax_rates, true ) : WC_Tax::calc_tax( $price_filter, $tax_rates, true ); return $price_filter - array_sum( $taxes ); } // If prices are shown excl. tax, add taxes to match the prices stored in the DB. $taxes = WC_Tax::calc_tax( $price_filter, $tax_rates, false ); return $price_filter + array_sum( $taxes ); } /** * Join wc_product_meta_lookup to posts if not already joined. * * @param string $sql SQL join. * @return string */ protected function append_product_sorting_table_join( $sql ) { global $wpdb; if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; } return $sql; } } packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php 0000644 00000000470 15132754524 0023045 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * OutOfStockException class. * * @internal This API is used internally by Blocks, this exception is thrown when an item in a draft order is out * of stock completely. */ class OutOfStockException extends StockAvailabilityException { } packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php 0000644 00000003134 15132754524 0025653 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use WP_Error; /** * InvalidStockLevelsInCartException class. * * @internal This API is used internally by Blocks, this exception is thrown if any items are out of stock * after each product on a draft order has been stock checked. */ class InvalidStockLevelsInCartException extends \Exception { /** * Sanitized error code. * * @var string */ public $error_code; /** * Additional error data. * * @var array */ public $additional_data = []; /** * All errors to display to the user. * * @var WP_Error */ public $error; /** * Setup exception. * * @param string $error_code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param WP_Error $error The WP_Error object containing all errors relating to stock availability. * @param array $additional_data Extra data (key value pairs) to expose in the error response. */ public function __construct( $error_code, $error, $additional_data = [] ) { $this->error_code = $error_code; $this->error = $error; $this->additional_data = array_filter( (array) $additional_data ); parent::__construct( '', 409 ); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns the list of messages. * * @return WP_Error */ public function getError() { return $this->error; } /** * Returns additional error data. * * @return array */ public function getAdditionalData() { return $this->additional_data; } } packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php 0000644 00000003136 15132754524 0024425 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * StockAvailabilityException class. * * @internal This API is used internally by Blocks, this exception is thrown when more than one of a product that * can only be purchased individually is in a cart. */ class StockAvailabilityException extends \Exception { /** * Sanitized error code. * * @var string */ public $error_code; /** * The name of the product that can only be purchased individually. * * @var string */ public $product_name; /** * Additional error data. * * @var array */ public $additional_data = []; /** * Setup exception. * * @param string $error_code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param string $product_name The name of the product that can only be purchased individually. * @param array $additional_data Extra data (key value pairs) to expose in the error response. */ public function __construct( $error_code, $product_name, $additional_data = [] ) { $this->error_code = $error_code; $this->product_name = $product_name; $this->additional_data = array_filter( (array) $additional_data ); parent::__construct(); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns additional error data. * * @return array */ public function getAdditionalData() { return $this->additional_data; } /** * Returns the product name. * * @return string */ public function getProductName() { return $this->product_name; } } packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php 0000644 00000044447 15132754524 0022261 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use \Exception; use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; /** * OrderController class. * Helper class which creates and syncs orders with the cart. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class OrderController { /** * Create order and set props based on global settings. * * @throws RouteException Exception if invalid data is detected. * * @return \WC_Order A new order object. */ public function create_order_from_cart() { if ( wc()->cart->is_empty() ) { throw new RouteException( 'woocommerce_rest_cart_empty', __( 'Cannot create order from empty cart.', 'woocommerce' ), 400 ); } add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) ); $order = new \WC_Order(); $order->set_status( 'checkout-draft' ); $order->set_created_via( 'store-api' ); $this->update_order_from_cart( $order ); remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) ); return $order; } /** * Update an order using data from the current cart. * * @param \WC_Order $order The order object to update. */ public function update_order_from_cart( \WC_Order $order ) { // Ensure cart is current. wc()->cart->calculate_shipping(); wc()->cart->calculate_totals(); // Update the current order to match the current cart. $this->update_line_items_from_cart( $order ); $this->update_addresses_from_cart( $order ); $order->set_currency( get_woocommerce_currency() ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->set_customer_id( get_current_user_id() ); $order->set_customer_ip_address( \WC_Geolocation::get_ip_address() ); $order->set_customer_user_agent( wc_get_user_agent() ); $order->update_meta_data( 'is_vat_exempt', wc()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' ); $order->calculate_totals(); } /** * Copies order data to customer object (not the session), so values persist for future checkouts. * * @param \WC_Order $order Order object. */ public function sync_customer_data_with_order( \WC_Order $order ) { if ( $order->get_customer_id() ) { $customer = new \WC_Customer( $order->get_customer_id() ); $customer->set_props( [ 'billing_first_name' => $order->get_billing_first_name(), 'billing_last_name' => $order->get_billing_last_name(), 'billing_company' => $order->get_billing_company(), 'billing_address_1' => $order->get_billing_address_1(), 'billing_address_2' => $order->get_billing_address_2(), 'billing_city' => $order->get_billing_city(), 'billing_state' => $order->get_billing_state(), 'billing_postcode' => $order->get_billing_postcode(), 'billing_country' => $order->get_billing_country(), 'billing_email' => $order->get_billing_email(), 'billing_phone' => $order->get_billing_phone(), 'shipping_first_name' => $order->get_shipping_first_name(), 'shipping_last_name' => $order->get_shipping_last_name(), 'shipping_company' => $order->get_shipping_company(), 'shipping_address_1' => $order->get_shipping_address_1(), 'shipping_address_2' => $order->get_shipping_address_2(), 'shipping_city' => $order->get_shipping_city(), 'shipping_state' => $order->get_shipping_state(), 'shipping_postcode' => $order->get_shipping_postcode(), 'shipping_country' => $order->get_shipping_country(), ] ); $shipping_phone_value = is_callable( [ $order, 'get_shipping_phone' ] ) ? $order->get_shipping_phone() : $order->get_meta( '_shipping_phone', true ); if ( is_callable( [ $customer, 'set_shipping_phone' ] ) ) { $customer->set_shipping_phone( $shipping_phone_value ); } else { $customer->update_meta_data( 'shipping_phone', $shipping_phone_value ); } $customer->save(); }; } /** * Final validation ran before payment is taken. * * By this point we have an order populated with customer data and items. * * @throws RouteException Exception if invalid data is detected. * @param \WC_Order $order Order object. */ public function validate_order_before_payment( \WC_Order $order ) { $needs_shipping = wc()->cart->needs_shipping(); $chosen_shipping_methods = wc()->session->get( 'chosen_shipping_methods' ); $this->validate_coupons( $order ); $this->validate_email( $order ); $this->validate_selected_shipping_methods( $needs_shipping, $chosen_shipping_methods ); $this->validate_addresses( $order ); } /** * Convert a coupon code to a coupon object. * * @param string $coupon_code Coupon code. * @return \WC_Coupon Coupon object. */ protected function get_coupon( $coupon_code ) { return new \WC_Coupon( $coupon_code ); } /** * Validate coupons applied to the order and remove those that are not valid. * * @throws RouteException Exception if invalid data is detected. * @param \WC_Order $order Order object. */ protected function validate_coupons( \WC_Order $order ) { $coupon_codes = $order->get_coupon_codes(); $coupons = array_filter( array_map( [ $this, 'get_coupon' ], $coupon_codes ) ); $validators = [ 'validate_coupon_email_restriction', 'validate_coupon_usage_limit' ]; $coupon_errors = []; foreach ( $coupons as $coupon ) { try { array_walk( $validators, function( $validator, $index, $params ) { call_user_func_array( [ $this, $validator ], $params ); }, [ $coupon, $order ] ); } catch ( Exception $error ) { $coupon_errors[ $coupon->get_code() ] = $error->getMessage(); } } if ( $coupon_errors ) { // Remove all coupons that were not valid. foreach ( $coupon_errors as $coupon_code => $message ) { wc()->cart->remove_coupon( $coupon_code ); } // Recalculate totals. wc()->cart->calculate_totals(); // Re-sync order with cart. $this->update_order_from_cart( $order ); // Return exception so customer can review before payment. throw new RouteException( 'woocommerce_rest_cart_coupon_errors', sprintf( /* translators: %s Coupon codes. */ __( 'Invalid coupons were removed from the cart: "%s"', 'woocommerce' ), implode( '", "', array_keys( $coupon_errors ) ) ), 409, [ 'removed_coupons' => $coupon_errors, ] ); } } /** * Validates the customer email. This is a required field. * * @throws RouteException Exception if invalid data is detected. * @param \WC_Order $order Order object. */ protected function validate_email( \WC_Order $order ) { $email = $order->get_billing_email(); if ( empty( $email ) ) { throw new RouteException( 'woocommerce_rest_missing_email_address', __( 'A valid email address is required', 'woocommerce' ), 400 ); } if ( ! is_email( $email ) ) { throw new RouteException( 'woocommerce_rest_invalid_email_address', sprintf( /* translators: %s provided email. */ __( 'The provided email address (%s) is not valid—please provide a valid email address', 'woocommerce' ), esc_html( $email ) ), 400 ); } } /** * Validates customer address data based on the locale to ensure required fields are set. * * @throws RouteException Exception if invalid data is detected. * @param \WC_Order $order Order object. */ protected function validate_addresses( \WC_Order $order ) { $errors = new \WP_Error(); $needs_shipping = wc()->cart->needs_shipping(); $billing_address = $order->get_address( 'billing' ); $shipping_address = $order->get_address( 'shipping' ); if ( $needs_shipping && ! $this->validate_allowed_country( $shipping_address['country'], (array) wc()->countries->get_shipping_countries() ) ) { throw new RouteException( 'woocommerce_rest_invalid_address_country', sprintf( /* translators: %s country code. */ __( 'Sorry, we do not ship orders to the provided country (%s)', 'woocommerce' ), $shipping_address['country'] ), 400, [ 'allowed_countries' => array_keys( wc()->countries->get_shipping_countries() ), ] ); } if ( ! $this->validate_allowed_country( $billing_address['country'], (array) wc()->countries->get_allowed_countries() ) ) { throw new RouteException( 'woocommerce_rest_invalid_address_country', sprintf( /* translators: %s country code. */ __( 'Sorry, we do not allow orders from the provided country (%s)', 'woocommerce' ), $billing_address['country'] ), 400, [ 'allowed_countries' => array_keys( wc()->countries->get_allowed_countries() ), ] ); } if ( $needs_shipping ) { $this->validate_address_fields( $shipping_address, 'shipping', $errors ); } $this->validate_address_fields( $billing_address, 'billing', $errors ); if ( ! $errors->has_errors() ) { return; } $errors_by_code = []; $error_codes = $errors->get_error_codes(); foreach ( $error_codes as $code ) { $errors_by_code[ $code ] = $errors->get_error_messages( $code ); } // Surface errors from first code. foreach ( $errors_by_code as $code => $error_messages ) { throw new RouteException( 'woocommerce_rest_invalid_address', sprintf( /* translators: %s Address type. */ __( 'There was a problem with the provided %s:', 'woocommerce' ) . ' ' . implode( ', ', $error_messages ), 'shipping' === $code ? __( 'shipping address', 'woocommerce' ) : __( 'billing address', 'woocommerce' ) ), 400, [ 'errors' => $errors_by_code, ] ); } } /** * Check all required address fields are set and return errors if not. * * @param string $country Country code. * @param array $allowed_countries List of valid country codes. * @return boolean True if valid. */ protected function validate_allowed_country( $country, array $allowed_countries ) { return array_key_exists( $country, $allowed_countries ); } /** * Check all required address fields are set and return errors if not. * * @param array $address Address array. * @param string $address_type billing or shipping address, used in error messages. * @param \WP_Error $errors Error object. */ protected function validate_address_fields( $address, $address_type, \WP_Error $errors ) { $all_locales = wc()->countries->get_country_locale(); $current_locale = isset( $all_locales[ $address['country'] ] ) ? $all_locales[ $address['country'] ] : []; /** * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array * is based on assets/js/base/components/cart-checkout/address-form/default-address-fields.js */ $address_fields = [ 'first_name' => [ 'label' => __( 'First name', 'woocommerce' ), 'required' => true, ], 'last_name' => [ 'label' => __( 'Last name', 'woocommerce' ), 'required' => true, ], 'company' => [ 'label' => __( 'Company', 'woocommerce' ), 'required' => false, ], 'address_1' => [ 'label' => __( 'Address', 'woocommerce' ), 'required' => true, ], 'address_2' => [ 'label' => __( 'Apartment, suite, etc.', 'woocommerce' ), 'required' => false, ], 'country' => [ 'label' => __( 'Country/Region', 'woocommerce' ), 'required' => true, ], 'city' => [ 'label' => __( 'City', 'woocommerce' ), 'required' => true, ], 'state' => [ 'label' => __( 'State/County', 'woocommerce' ), 'required' => true, ], 'postcode' => [ 'label' => __( 'Postal code', 'woocommerce' ), 'required' => true, ], ]; if ( $current_locale ) { foreach ( $current_locale as $key => $field ) { if ( isset( $address_fields[ $key ] ) ) { $address_fields[ $key ]['label'] = isset( $field['label'] ) ? $field['label'] : $address_fields[ $key ]['label']; $address_fields[ $key ]['required'] = isset( $field['required'] ) ? $field['required'] : $address_fields[ $key ]['required']; } } } foreach ( $address_fields as $address_field_key => $address_field ) { if ( empty( $address[ $address_field_key ] ) && $address_field['required'] ) { /* translators: %s Field label. */ $errors->add( $address_type, sprintf( __( '%s is required', 'woocommerce' ), $address_field['label'] ), $address_field_key ); } } } /** * Check email restrictions of a coupon against the order. * * @throws Exception Exception if invalid data is detected. * @param \WC_Coupon $coupon Coupon object applied to the cart. * @param \WC_Order $order Order object. */ protected function validate_coupon_email_restriction( \WC_Coupon $coupon, \WC_Order $order ) { $restrictions = $coupon->get_email_restrictions(); if ( ! empty( $restrictions ) && $order->get_billing_email() && ! wc()->cart->is_coupon_emails_allowed( [ $order->get_billing_email() ], $restrictions ) ) { throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ) ); } } /** * Check usage restrictions of a coupon against the order. * * @throws Exception Exception if invalid data is detected. * @param \WC_Coupon $coupon Coupon object applied to the cart. * @param \WC_Order $order Order object. */ protected function validate_coupon_usage_limit( \WC_Coupon $coupon, \WC_Order $order ) { $coupon_usage_limit = $coupon->get_usage_limit_per_user(); if ( $coupon_usage_limit > 0 ) { $data_store = $coupon->get_data_store(); $usage_count = $order->get_customer_id() ? $data_store->get_usage_by_user_id( $coupon, $order->get_customer_id() ) : $data_store->get_usage_by_email( $coupon, $order->get_billing_email() ); if ( $usage_count >= $coupon_usage_limit ) { throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ) ); } } } /** * Check there is a shipping method if it requires shipping. * * @throws RouteException Exception if invalid data is detected. * @param boolean $needs_shipping Current order needs shipping. * @param array $chosen_shipping_methods Array of shipping methods. */ public function validate_selected_shipping_methods( $needs_shipping, $chosen_shipping_methods = array() ) { if ( ! $needs_shipping || ! is_array( $chosen_shipping_methods ) ) { return; } foreach ( $chosen_shipping_methods as $chosen_shipping_method ) { if ( false === $chosen_shipping_method ) { throw new RouteException( 'woocommerce_rest_invalid_shipping_option', __( 'Sorry, this order requires a shipping option.', 'woocommerce' ), 400, [] ); } } } /** * Changes default order status to draft for orders created via this API. * * @return string */ public function default_order_status() { return 'checkout-draft'; } /** * Create order line items. * * @param \WC_Order $order The order object to update. */ protected function update_line_items_from_cart( \WC_Order $order ) { $cart_controller = new CartController(); $cart = $cart_controller->get_cart_instance(); $cart_hashes = $cart_controller->get_cart_hashes(); if ( $order->get_cart_hash() !== $cart_hashes['line_items'] ) { $order->set_cart_hash( $cart_hashes['line_items'] ); $order->remove_order_items( 'line_item' ); wc()->checkout->create_order_line_items( $order, $cart ); } if ( $order->get_meta_data( '_shipping_hash' ) !== $cart_hashes['shipping'] ) { $order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] ); $order->remove_order_items( 'shipping' ); wc()->checkout->create_order_shipping_lines( $order, wc()->session->get( 'chosen_shipping_methods' ), wc()->shipping()->get_packages() ); } if ( $order->get_meta_data( '_coupons_hash' ) !== $cart_hashes['coupons'] ) { $order->remove_order_items( 'coupon' ); $order->update_meta_data( '_coupons_hash', $cart_hashes['coupons'] ); wc()->checkout->create_order_coupon_lines( $order, $cart ); } if ( $order->get_meta_data( '_fees_hash' ) !== $cart_hashes['fees'] ) { $order->update_meta_data( '_fees_hash', $cart_hashes['fees'] ); $order->remove_order_items( 'fee' ); wc()->checkout->create_order_fee_lines( $order, $cart ); } if ( $order->get_meta_data( '_taxes_hash' ) !== $cart_hashes['taxes'] ) { $order->update_meta_data( '_taxes_hash', $cart_hashes['taxes'] ); $order->remove_order_items( 'tax' ); wc()->checkout->create_order_tax_lines( $order, $cart ); } } /** * Update address data from cart and/or customer session data. * * @param \WC_Order $order The order object to update. */ protected function update_addresses_from_cart( \WC_Order $order ) { $order->set_props( [ 'billing_first_name' => wc()->customer->get_billing_first_name(), 'billing_last_name' => wc()->customer->get_billing_last_name(), 'billing_company' => wc()->customer->get_billing_company(), 'billing_address_1' => wc()->customer->get_billing_address_1(), 'billing_address_2' => wc()->customer->get_billing_address_2(), 'billing_city' => wc()->customer->get_billing_city(), 'billing_state' => wc()->customer->get_billing_state(), 'billing_postcode' => wc()->customer->get_billing_postcode(), 'billing_country' => wc()->customer->get_billing_country(), 'billing_email' => wc()->customer->get_billing_email(), 'billing_phone' => wc()->customer->get_billing_phone(), 'shipping_first_name' => wc()->customer->get_shipping_first_name(), 'shipping_last_name' => wc()->customer->get_shipping_last_name(), 'shipping_company' => wc()->customer->get_shipping_company(), 'shipping_address_1' => wc()->customer->get_shipping_address_1(), 'shipping_address_2' => wc()->customer->get_shipping_address_2(), 'shipping_city' => wc()->customer->get_shipping_city(), 'shipping_state' => wc()->customer->get_shipping_state(), 'shipping_postcode' => wc()->customer->get_shipping_postcode(), 'shipping_country' => wc()->customer->get_shipping_country(), ] ); $shipping_phone_value = is_callable( [ wc()->customer, 'get_shipping_phone' ] ) ? wc()->customer->get_shipping_phone() : wc()->customer->get_meta( 'shipping_phone', true ); if ( is_callable( [ $order, 'set_shipping_phone' ] ) ) { $order->set_shipping_phone( $shipping_phone_value ); } else { $order->update_meta_data( '_shipping_phone', $shipping_phone_value ); } } } packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php 0000644 00000002430 15132754524 0021643 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; /** * NoticeHandler class. * Helper class to convert notices to exceptions. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class NoticeHandler { /** * Convert queued error notices into an exception. * * For example, Payment methods may add error notices during validate_fields call to prevent checkout. * Since we're not rendering notices at all, we need to convert them to exceptions. * * This method will find the first error message and thrown an exception instead. Discards notices once complete. * * @throws RouteException If an error notice is detected, Exception is thrown. * * @param string $error_code Error code for the thrown exceptions. */ public static function convert_notices_to_exceptions( $error_code = 'unknown_server_error' ) { if ( 0 === wc_notice_count( 'error' ) ) { return; } $error_notices = wc_get_notices( 'error' ); // Prevent notices from being output later on. wc_clear_notices(); foreach ( $error_notices as $error_notice ) { throw new RouteException( $error_code, wp_strip_all_tags( $error_notice['notice'] ), 400 ); } } } packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php 0000644 00000114631 15132754524 0022070 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\NoticeHandler; use Automattic\WooCommerce\Blocks\Utils\ArrayUtils; use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; use WP_Error; /** * Woo Cart Controller class. * Helper class to bridge the gap between the cart API and Woo core. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class CartController { /** * Makes the cart and sessions available to a route by loading them from core. */ public function load_cart() { if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) { include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; wc_load_cart(); } } /** * Based on the core cart class but returns errors rather than rendering notices directly. * * @todo Overriding the core add_to_cart method was necessary because core outputs notices when an item is added to * the cart. For us this would cause notices to build up and output on the store, out of context. Core would need * refactoring to split notices out from other cart actions. * * @throws RouteException Exception if invalid data is detected. * * @param array $request Add to cart request params. * @return string|Error */ public function add_to_cart( $request ) { $cart = $this->get_cart_instance(); $request = wp_parse_args( $request, [ 'id' => 0, 'quantity' => 1, 'variation' => [], 'cart_item_data' => [], ] ); $request = $this->filter_request_data( $this->parse_variation_data( $request ) ); $product = $this->get_product_for_cart( $request ); $cart_id = $cart->generate_cart_id( $this->get_product_id( $product ), $this->get_variation_id( $product ), $request['variation'], $request['cart_item_data'] ); $this->validate_add_to_cart( $product, $request ); $existing_cart_id = $cart->find_product_in_cart( $cart_id ); if ( $existing_cart_id ) { if ( $product->is_sold_individually() ) { throw new RouteException( 'woocommerce_rest_cart_product_sold_individually', sprintf( /* translators: %s: product name */ __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product->get_name() ), 400 ); } $cart->set_quantity( $existing_cart_id, $request['quantity'] + $cart->cart_contents[ $existing_cart_id ]['quantity'], true ); return $existing_cart_id; } $cart->cart_contents[ $cart_id ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $request['cart_item_data'], array( 'key' => $cart_id, 'product_id' => $this->get_product_id( $product ), 'variation_id' => $this->get_variation_id( $product ), 'variation' => $request['variation'], 'quantity' => $request['quantity'], 'data' => $product, 'data_hash' => wc_get_cart_item_data_hash( $product ), ) ), $cart_id ); $cart->cart_contents = apply_filters( 'woocommerce_cart_contents_changed', $cart->cart_contents ); do_action( 'woocommerce_add_to_cart', $cart_id, $this->get_product_id( $product ), $request['quantity'], $this->get_variation_id( $product ), $request['variation'], $request['cart_item_data'] ); return $cart_id; } /** * Based on core `set_quantity` method, but validates if an item is sold individually first. * * @throws RouteException Exception if invalid data is detected. * * @param string $item_id Cart item id. * @param integer $quantity Cart quantity. */ public function set_cart_item_quantity( $item_id, $quantity = 1 ) { $cart_item = $this->get_cart_item( $item_id ); if ( empty( $cart_item ) ) { throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item does not exist.', 'woocommerce' ), 404 ); } $product = $cart_item['data']; if ( ! $product instanceof \WC_Product ) { throw new RouteException( 'woocommerce_rest_cart_invalid_product', __( 'Cart item is invalid.', 'woocommerce' ), 404 ); } if ( $product->is_sold_individually() && $quantity > 1 ) { throw new RouteException( 'woocommerce_rest_cart_product_sold_individually', sprintf( /* translators: %s: product name */ __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product->get_name() ), 400 ); } $cart = $this->get_cart_instance(); $cart->set_quantity( $item_id, $quantity ); } /** * Validate all items in the cart and check for errors. * * @throws RouteException Exception if invalid data is detected. * * @param \WC_Product $product Product object associated with the cart item. * @param array $request Add to cart request params. */ public function validate_add_to_cart( \WC_Product $product, $request ) { if ( ! $product->is_purchasable() ) { $this->throw_default_product_exception( $product ); } if ( ! $product->is_in_stock() ) { throw new RouteException( 'woocommerce_rest_cart_product_no_stock', sprintf( /* translators: %s: product name */ __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woocommerce' ), $product->get_name() ), 400 ); } if ( $product->managing_stock() && ! $product->backorders_allowed() ) { $qty_remaining = $this->get_remaining_stock_for_product( $product ); $qty_in_cart = $this->get_product_quantity_in_cart( $product ); if ( $qty_remaining < $qty_in_cart + $request['quantity'] ) { throw new RouteException( 'woocommerce_rest_cart_product_no_stock', sprintf( /* translators: 1: product name 2: quantity in stock */ __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $qty_remaining, $product ) ), 400 ); } } /** * Hook: woocommerce_add_to_cart_validation (legacy). * * Allow 3rd parties to validate if an item can be added to the cart. This is a legacy hook from Woo core. * This filter will be deprecated because it encourages usage of wc_add_notice. For the API we need to capture * notices and convert to exceptions instead. */ $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $this->get_product_id( $product ), $request['quantity'], $this->get_variation_id( $product ), $request['variation'] ); if ( ! $passed_validation ) { // Validation did not pass - see if an error notice was thrown. NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_add_to_cart_error' ); // If no notice was thrown, throw the default notice instead. $this->throw_default_product_exception( $product ); } /** * Fire action to validate add to cart. Functions hooking into this should throw an \Exception to prevent * add to cart from occuring. * * @param \WC_Product $product Product object being added to the cart. * @param array $request Add to cart request params including id, quantity, and variation attributes. */ do_action( 'wooocommerce_store_api_validate_add_to_cart', $product, $request ); } /** * Generates the error message for out of stock products and adds product names to it. * * @param string $singular The message to use when only one product is in the list. * @param string $plural The message to use when more than one product is in the list. * @param array $items The list of cart items whose names should be inserted into the message. * @returns string The translated and correctly pluralised message. */ private function add_product_names_to_message( $singular, $plural, $items ) { $product_names = wc_list_pluck( $items, 'getProductName' ); $message = ( count( $items ) > 1 ) ? $plural : $singular; return sprintf( $message, ArrayUtils::natural_language_join( $product_names, true ) ); } /** * Takes a string describing the type of stock extension, whether there is a single product or multiple products * causing this exception and returns an appropriate error message. * * @param string $exception_type The type of exception encountered. * @param string $singular_or_plural Whether to get the error message for a single product or multiple. * * @return string */ private function get_error_message_for_stock_exception_type( $exception_type, $singular_or_plural ) { $stock_error_messages = [ 'out_of_stock' => [ /* translators: %s: product name. */ 'singular' => __( '%s is out of stock and cannot be purchased. Please remove it from your cart.', 'woocommerce' ), /* translators: %s: product names. */ 'plural' => __( '%s are out of stock and cannot be purchased. Please remove them from your cart.', 'woocommerce' ), ], 'not_purchasable' => [ /* translators: %s: product name. */ 'singular' => __( '%s cannot be purchased. Please remove it from your cart.', 'woocommerce' ), /* translators: %s: product names. */ 'plural' => __( '%s cannot be purchased. Please remove them from your cart.', 'woocommerce' ), ], 'too_many_in_cart' => [ /* translators: %s: product names. */ 'singular' => __( 'There are too many %s in the cart. Only 1 can be purchased. Please reduce the quantity in your cart.', 'woocommerce' ), /* translators: %s: product names. */ 'plural' => __( 'There are too many %s in the cart. Only 1 of each can be purchased. Please reduce the quantities in your cart.', 'woocommerce' ), ], 'partial_out_of_stock' => [ /* translators: %s: product names. */ 'singular' => __( 'There is not enough %s in stock. Please reduce the quantity in your cart.', 'woocommerce' ), /* translators: %s: product names. */ 'plural' => __( 'There are not enough %s in stock. Please reduce the quantities in your cart.', 'woocommerce' ), ], ]; if ( isset( $stock_error_messages[ $exception_type ] ) && isset( $stock_error_messages[ $exception_type ][ $singular_or_plural ] ) ) { return $stock_error_messages[ $exception_type ][ $singular_or_plural ]; } return __( 'There was an error with an item in your cart.', 'woocommerce' ); } /** * Validate all items in the cart and check for errors. * * @throws InvalidStockLevelsInCartException Exception if invalid data is detected due to insufficient stock levels. */ public function validate_cart_items() { $cart = $this->get_cart_instance(); $cart_items = $this->get_cart_items(); $out_of_stock_products = []; $too_many_in_cart_products = []; $partial_out_of_stock_products = []; $not_purchasable_products = []; foreach ( $cart_items as $cart_item_key => $cart_item ) { try { $this->validate_cart_item( $cart_item ); } catch ( TooManyInCartException $error ) { $too_many_in_cart_products[] = $error; } catch ( NotPurchasableException $error ) { $not_purchasable_products[] = $error; } catch ( PartialOutOfStockException $error ) { $partial_out_of_stock_products[] = $error; } catch ( OutOfStockException $error ) { $out_of_stock_products[] = $error; } } $error = new WP_Error(); if ( count( $out_of_stock_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'out_of_stock', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'out_of_stock', 'plural' ); $error->add( 409, $this->add_product_names_to_message( $singular_error, $plural_error, $out_of_stock_products ) ); } if ( count( $not_purchasable_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'not_purchasable', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'not_purchasable', 'plural' ); $error->add( 409, $this->add_product_names_to_message( $singular_error, $plural_error, $not_purchasable_products ) ); } if ( count( $too_many_in_cart_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'too_many_in_cart', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'too_many_in_cart', 'plural' ); $error->add( 409, $this->add_product_names_to_message( $singular_error, $plural_error, $too_many_in_cart_products ) ); } if ( count( $partial_out_of_stock_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'partial_out_of_stock', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'partial_out_of_stock', 'plural' ); $error->add( 409, $this->add_product_names_to_message( $singular_error, $plural_error, $partial_out_of_stock_products ) ); } if ( $error->has_errors() ) { throw new InvalidStockLevelsInCartException( 'woocommerce_stock_availability_error', $error ); } // Before running the woocommerce_check_cart_items hook, unhook validation from the core cart. remove_action( 'woocommerce_check_cart_items', array( $cart, 'check_cart_items' ), 1 ); remove_action( 'woocommerce_check_cart_items', array( $cart, 'check_cart_coupons' ), 1 ); /** * Hook: woocommerce_check_cart_items * * Allow 3rd parties to validate cart items. This is a legacy hook from Woo core. * This filter will be deprecated because it encourages usage of wc_add_notice. For the API we need to capture * notices and convert to exceptions instead. */ do_action( 'woocommerce_check_cart_items' ); NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_cart_item_error' ); } /** * This method will take arrays of exceptions relating to stock, and will convert them to a WP_Error object. * * @param TooManyInCartException[] $too_many_in_cart_products Array of TooManyInCartExceptions. * @param NotPurchasableException[] $not_purchasable_products Array of NotPurchasableExceptions. * @param PartialOutOfStockException[] $partial_out_of_stock_products Array of PartialOutOfStockExceptions. * @param OutOfStockException[] $out_of_stock_products Array of OutOfStockExceptions. * * @return WP_Error[] The WP_Error object returned. Will have errors if any exceptions were in the args. It will be empty if they do not. */ private function stock_exceptions_to_wp_errors( $too_many_in_cart_products, $not_purchasable_products, $partial_out_of_stock_products, $out_of_stock_products ) { $too_many_in_cart_error = new WP_Error(); $out_of_stock_error = new WP_Error(); $partial_out_of_stock_error = new WP_Error(); $not_purchasable_error = new WP_Error(); if ( count( $out_of_stock_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'out_of_stock', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'out_of_stock', 'plural' ); $out_of_stock_error->add( 'woocommerce-blocks-product-out-of-stock', $this->add_product_names_to_message( $singular_error, $plural_error, $out_of_stock_products ) ); } if ( count( $not_purchasable_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'not_purchasable', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'not_purchasable', 'plural' ); $not_purchasable_error->add( 'woocommerce-blocks-product-not-purchasable', $this->add_product_names_to_message( $singular_error, $plural_error, $not_purchasable_products ) ); } if ( count( $too_many_in_cart_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'too_many_in_cart', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'too_many_in_cart', 'plural' ); $too_many_in_cart_error->add( 'woocommerce-blocks-too-many-of-product-in-cart', $this->add_product_names_to_message( $singular_error, $plural_error, $too_many_in_cart_products ) ); } if ( count( $partial_out_of_stock_products ) > 0 ) { $singular_error = $this->get_error_message_for_stock_exception_type( 'partial_out_of_stock', 'singular' ); $plural_error = $this->get_error_message_for_stock_exception_type( 'partial_out_of_stock', 'plural' ); $partial_out_of_stock_error->add( 'woocommerce-blocks-product-partially-out-of-stock', $this->add_product_names_to_message( $singular_error, $plural_error, $partial_out_of_stock_products ) ); } return array_filter( [ $too_many_in_cart_error, $partial_out_of_stock_error, $out_of_stock_error, $not_purchasable_error, ], function( $error ) { return $error->has_errors(); } ); } /** * Validates an existing cart item and returns any errors. * * @throws TooManyInCartException Exception if more than one product that can only be purchased individually is in * the cart. * @throws PartialOutOfStockException Exception if an item has a quantity greater than what is available in stock. * @throws OutOfStockException Exception thrown when an item is entirely out of stock. * @throws NotPurchasableException Exception thrown when an item is not purchasable. * @param array $cart_item Cart item array. */ public function validate_cart_item( $cart_item ) { $product = $cart_item['data']; if ( ! $product instanceof \WC_Product ) { return; } if ( ! $product->is_purchasable() ) { throw new NotPurchasableException( 'woocommerce_rest_cart_product_not_purchasable', $product->get_name() ); } if ( $product->is_sold_individually() && $cart_item['quantity'] > 1 ) { throw new TooManyInCartException( 'woocommerce_rest_cart_product_sold_individually', $product->get_name() ); } if ( ! $product->is_in_stock() ) { throw new OutOfStockException( 'woocommerce_rest_cart_product_no_stock', $product->get_name() ); } if ( $product->managing_stock() && ! $product->backorders_allowed() ) { $qty_remaining = $this->get_remaining_stock_for_product( $product ); $qty_in_cart = $this->get_product_quantity_in_cart( $product ); if ( $qty_remaining < $qty_in_cart ) { throw new PartialOutOfStockException( 'woocommerce_rest_cart_product_partially_no_stock', $product->get_name() ); } } /** * Fire action to validate add to cart. Functions hooking into this should throw an \Exception to prevent * add to cart from occurring. * * @param \WC_Product $product Product object being added to the cart. * @param array $cart_item Cart item array. */ do_action( 'wooocommerce_store_api_validate_cart_item', $product, $cart_item ); } /** * Validate all coupons in the cart and check for errors. * * @throws RouteException Exception if invalid data is detected. */ public function validate_cart_coupons() { $cart_coupons = $this->get_cart_coupons(); foreach ( $cart_coupons as $code ) { $coupon = new \WC_Coupon( $code ); $this->validate_cart_coupon( $coupon ); } } /** * Validate all items in the cart and get a list of errors. * * @return WP_Error[] An array of WP_Errors describing the cart's error state. */ public function get_cart_item_errors() { $errors = []; $cart_items = $this->get_cart_items(); $too_many_in_cart_exceptions = []; $not_purchasable_exceptions = []; $partial_out_of_stock_exceptions = []; $out_of_stock_exceptions = []; foreach ( $cart_items as $cart_item_key => $cart_item ) { try { $this->validate_cart_item( $cart_item ); } catch ( RouteException $error ) { $errors[] = new WP_Error( $error->getErrorCode(), $error->getMessage() ); } catch ( TooManyInCartException $error ) { $too_many_in_cart_exceptions[] = $error; } catch ( NotPurchasableException $error ) { $not_purchasable_exceptions[] = $error; } catch ( PartialOutOfStockException $error ) { $partial_out_of_stock_exceptions[] = $error; } catch ( OutOfStockException $error ) { $out_of_stock_exceptions[] = $error; } } if ( count( $errors ) > 0 ) { return $errors; } return $this->stock_exceptions_to_wp_errors( $too_many_in_cart_exceptions, $not_purchasable_exceptions, $partial_out_of_stock_exceptions, $out_of_stock_exceptions ); } /** * Validate all items in the cart and get a list of errors. * * @throws RouteException Exception if invalid data is detected. */ public function get_cart_coupon_errors() { $errors = []; $cart_coupons = $this->get_cart_coupons(); foreach ( $cart_coupons as $code ) { try { $coupon = new \WC_Coupon( $code ); $this->validate_cart_coupon( $coupon ); } catch ( RouteException $error ) { $errors[] = new \WP_Error( $error->getErrorCode(), $error->getMessage() ); } } return $errors; } /** * Get main instance of cart class. * * @throws RouteException When cart cannot be loaded. * @return \WC_Cart */ public function get_cart_instance() { $cart = wc()->cart; if ( ! $cart || ! $cart instanceof \WC_Cart ) { throw new RouteException( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woocommerce' ), 500 ); } return $cart; } /** * Return a cart item from the woo core cart class. * * @param string $item_id Cart item id. * @return array */ public function get_cart_item( $item_id ) { $cart = $this->get_cart_instance(); return isset( $cart->cart_contents[ $item_id ] ) ? $cart->cart_contents[ $item_id ] : []; } /** * Returns all cart items. * * @param callable $callback Optional callback to apply to the array filter. * @return array */ public function get_cart_items( $callback = null ) { $cart = $this->get_cart_instance(); return $callback ? array_filter( $cart->get_cart(), $callback ) : array_filter( $cart->get_cart() ); } /** * Get hashes for items in the current cart. Useful for tracking changes. * * @return array */ public function get_cart_hashes() { $cart = $this->get_cart_instance(); return [ 'line_items' => $cart->get_cart_hash(), 'shipping' => md5( wp_json_encode( $cart->shipping_methods ) ), 'fees' => md5( wp_json_encode( $cart->get_fees() ) ), 'coupons' => md5( wp_json_encode( $cart->get_applied_coupons() ) ), 'taxes' => md5( wp_json_encode( $cart->get_taxes() ) ), ]; } /** * Empty cart contents. */ public function empty_cart() { $cart = $this->get_cart_instance(); $cart->empty_cart(); } /** * See if cart has applied coupon by code. * * @param string $coupon_code Cart coupon code. * @return bool */ public function has_coupon( $coupon_code ) { $cart = $this->get_cart_instance(); return $cart->has_discount( $coupon_code ); } /** * Returns all applied coupons. * * @param callable $callback Optional callback to apply to the array filter. * @return array */ public function get_cart_coupons( $callback = null ) { $cart = $this->get_cart_instance(); return $callback ? array_filter( $cart->get_applied_coupons(), $callback ) : array_filter( $cart->get_applied_coupons() ); } /** * Get shipping packages from the cart with calculated shipping rates. * * @todo this can be refactored once https://github.com/woocommerce/woocommerce/pull/26101 lands. * * @param bool $calculate_rates Should rates for the packages also be returned. * @return array */ public function get_shipping_packages( $calculate_rates = true ) { $cart = $this->get_cart_instance(); // See if we need to calculate anything. if ( ! $cart->needs_shipping() ) { return []; } $packages = $cart->get_shipping_packages(); // Add extra package data to array. if ( count( $packages ) ) { $packages = array_map( function( $key, $package, $index ) { $package['package_id'] = isset( $package['package_id'] ) ? $package['package_id'] : $key; $package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : $this->get_package_name( $package, $index ); return $package; }, array_keys( $packages ), $packages, range( 1, count( $packages ) ) ); } return $calculate_rates ? wc()->shipping()->calculate_shipping( $packages ) : $packages; } /** * Creates a name for a package. * * @param array $package Shipping package from WooCommerce. * @param int $index Package number. * @return string */ protected function get_package_name( $package, $index ) { return apply_filters( 'woocommerce_shipping_package_name', $index > 1 ? sprintf( /* translators: %d: shipping package number */ _x( 'Shipping method %d', 'shipping packages', 'woocommerce' ), $index ) : _x( 'Shipping method', 'shipping packages', 'woocommerce' ), $package['package_id'], $package ); } /** * Selects a shipping rate. * * @param int|string $package_id ID of the package to choose a rate for. * @param string $rate_id ID of the rate being chosen. */ public function select_shipping_rate( $package_id, $rate_id ) { $cart = $this->get_cart_instance(); $session_data = wc()->session->get( 'chosen_shipping_methods' ) ? wc()->session->get( 'chosen_shipping_methods' ) : []; $session_data[ $package_id ] = $rate_id; wc()->session->set( 'chosen_shipping_methods', $session_data ); } /** * Based on the core cart class but returns errors rather than rendering notices directly. * * @todo Overriding the core apply_coupon method was necessary because core outputs notices when a coupon gets * applied. For us this would cause notices to build up and output on the store, out of context. Core would need * refactoring to split notices out from other cart actions. * * @throws RouteException Exception if invalid data is detected. * * @param string $coupon_code Coupon code. */ public function apply_coupon( $coupon_code ) { $cart = $this->get_cart_instance(); $applied_coupons = $this->get_cart_coupons(); $coupon = new \WC_Coupon( $coupon_code ); if ( $coupon->get_code() !== $coupon_code ) { throw new RouteException( 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s coupon code */ __( '"%s" is an invalid coupon code.', 'woocommerce' ), esc_html( $coupon_code ) ), 400 ); } if ( $this->has_coupon( $coupon_code ) ) { throw new RouteException( 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s coupon code */ __( 'Coupon code "%s" has already been applied.', 'woocommerce' ), esc_html( $coupon_code ) ), 400 ); } if ( ! $coupon->is_valid() ) { throw new RouteException( 'woocommerce_rest_cart_coupon_error', wp_strip_all_tags( $coupon->get_error_message() ), 400 ); } // Prevents new coupons being added if individual use coupons are already in the cart. $individual_use_coupons = $this->get_cart_coupons( function( $code ) { $coupon = new \WC_Coupon( $code ); return $coupon->get_individual_use(); } ); foreach ( $individual_use_coupons as $code ) { $individual_use_coupon = new \WC_Coupon( $code ); if ( false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $coupon, $individual_use_coupon, $applied_coupons ) ) { throw new RouteException( 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s: coupon code */ __( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $code ), 400 ); } } if ( $coupon->get_individual_use() ) { $coupons_to_remove = array_diff( $applied_coupons, apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $coupon, $applied_coupons ) ); foreach ( $coupons_to_remove as $code ) { $cart->remove_coupon( $code ); } $applied_coupons = array_diff( $applied_coupons, $coupons_to_remove ); } $applied_coupons[] = $coupon_code; $cart->set_applied_coupons( $applied_coupons ); do_action( 'woocommerce_applied_coupon', $coupon_code ); } /** * Validates an existing cart coupon and returns any errors. * * @throws RouteException Exception if invalid data is detected. * * @param \WC_Coupon $coupon Coupon object applied to the cart. */ protected function validate_cart_coupon( \WC_Coupon $coupon ) { if ( ! $coupon->is_valid() ) { $cart = $this->get_cart_instance(); $cart->remove_coupon( $coupon->get_code() ); $cart->calculate_totals(); throw new RouteException( 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %1$s coupon code, %2$s reason. */ __( 'The "%1$s" coupon has been removed from your cart: %2$s', 'woocommerce' ), $coupon->get_code(), wp_strip_all_tags( $coupon->get_error_message() ) ), 409 ); } } /** * Gets the qty of a product across line items. * * @param \WC_Product $product Product object. * @return int */ protected function get_product_quantity_in_cart( $product ) { $cart = $this->get_cart_instance(); $product_quantities = $cart->get_cart_item_quantities(); $product_id = $product->get_stock_managed_by_id(); return isset( $product_quantities[ $product_id ] ) ? $product_quantities[ $product_id ] : 0; } /** * Gets remaining stock for a product. * * @param \WC_Product $product Product object. * @return int */ protected function get_remaining_stock_for_product( $product ) { $reserve_stock = new ReserveStock(); $draft_order = wc()->session->get( 'store_api_draft_order', 0 ); $qty_reserved = $reserve_stock->get_reserved_stock( $product, $draft_order ); return $product->get_stock_quantity() - $qty_reserved; } /** * Get a product object to be added to the cart. * * @throws RouteException Exception if invalid data is detected. * * @param array $request Add to cart request params. * @return \WC_Product|Error Returns a product object if purchasable. */ protected function get_product_for_cart( $request ) { $product = wc_get_product( $request['id'] ); if ( ! $product || 'trash' === $product->get_status() ) { throw new RouteException( 'woocommerce_rest_cart_invalid_product', __( 'This product cannot be added to the cart.', 'woocommerce' ), 400 ); } return $product; } /** * For a given product, get the product ID. * * @param \WC_Product $product Product object associated with the cart item. * @return int */ protected function get_product_id( \WC_Product $product ) { return $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); } /** * For a given product, get the variation ID. * * @param \WC_Product $product Product object associated with the cart item. * @return int */ protected function get_variation_id( \WC_Product $product ) { return $product->is_type( 'variation' ) ? $product->get_id() : 0; } /** * Default exception thrown when an item cannot be added to the cart. * * @throws RouteException Exception with code woocommerce_rest_cart_product_is_not_purchasable. * * @param \WC_Product $product Product object associated with the cart item. */ protected function throw_default_product_exception( \WC_Product $product ) { throw new RouteException( 'woocommerce_rest_cart_product_is_not_purchasable', sprintf( /* translators: %s: product name */ __( '"%s" is not available for purchase.', 'woocommerce' ), $product->get_name() ), 400 ); } /** * Filter data for add to cart requests. * * @param array $request Add to cart request params. * @return array Updated request array. */ protected function filter_request_data( $request ) { $product_id = $request['id']; $variation_id = 0; $product = wc_get_product( $product_id ); if ( $product->is_type( 'variation' ) ) { $product_id = $product->get_parent_id(); $variation_id = $product->get_id(); } $request['cart_item_data'] = (array) apply_filters( 'woocommerce_add_cart_item_data', $request['cart_item_data'], $product_id, $variation_id, $request['quantity'] ); if ( $product->is_sold_individually() ) { $request['quantity'] = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $request['quantity'], $product_id, $variation_id, $request['cart_item_data'] ); } return $request; } /** * If variations are set, validate and format the values ready to add to the cart. * * @throws RouteException Exception if invalid data is detected. * * @param array $request Add to cart request params. * @return array Updated request array. */ protected function parse_variation_data( $request ) { $product = $this->get_product_for_cart( $request ); // Remove variation request if not needed. if ( ! $product->is_type( array( 'variation', 'variable' ) ) ) { $request['variation'] = []; return $request; } // Flatten data and format posted values. $variable_product_attributes = $this->get_variable_product_attributes( $product ); $request['variation'] = $this->sanitize_variation_data( wp_list_pluck( $request['variation'], 'value', 'attribute' ), $variable_product_attributes ); // If we have a parent product, find the variation ID. if ( $product->is_type( 'variable' ) ) { $request['id'] = $this->get_variation_id_from_variation_data( $request, $product ); } // Now we have a variation ID, get the valid set of attributes for this variation. They will have an attribute_ prefix since they are from meta. $expected_attributes = wc_get_product_variation_attributes( $request['id'] ); $missing_attributes = []; foreach ( $variable_product_attributes as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } $prefixed_attribute_name = 'attribute_' . sanitize_title( $attribute['name'] ); $expected_value = isset( $expected_attributes[ $prefixed_attribute_name ] ) ? $expected_attributes[ $prefixed_attribute_name ] : ''; $attribute_label = wc_attribute_label( $attribute['name'] ); if ( isset( $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ] ) ) { $given_value = $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ]; if ( $expected_value === $given_value ) { continue; } // If valid values are empty, this is an 'any' variation so get all possible values. if ( '' === $expected_value && in_array( $given_value, $attribute->get_slugs(), true ) ) { continue; } throw new RouteException( 'woocommerce_rest_invalid_variation_data', /* translators: %1$s: Attribute name, %2$s: Allowed values. */ sprintf( __( 'Invalid value posted for %1$s. Allowed values: %2$s', 'woocommerce' ), $attribute_label, implode( ', ', $attribute->get_slugs() ) ), 400 ); } // If no attribute was posted, only error if the variation has an 'any' attribute which requires a value. if ( '' === $expected_value ) { $missing_attributes[] = $attribute_label; } } if ( ! empty( $missing_attributes ) ) { throw new RouteException( 'woocommerce_rest_missing_variation_data', /* translators: %s: Attribute name. */ __( 'Missing variation data for variable product.', 'woocommerce' ) . ' ' . sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ), 400 ); } return $request; } /** * Try to match request data to a variation ID and return the ID. * * @throws RouteException Exception if variation cannot be found. * * @param array $request Add to cart request params. * @param \WC_Product $product Product being added to the cart. * @return int Matching variation ID. */ protected function get_variation_id_from_variation_data( $request, $product ) { $data_store = \WC_Data_Store::load( 'product' ); $match_attributes = $request['variation']; $variation_id = $data_store->find_matching_product_variation( $product, $match_attributes ); if ( empty( $variation_id ) ) { throw new RouteException( 'woocommerce_rest_variation_id_from_variation_data', __( 'No matching variation found.', 'woocommerce' ), 400 ); } return $variation_id; } /** * Format and sanitize variation data posted to the API. * * Labels are converted to names (e.g. Size to pa_size), and values are cleaned. * * @throws RouteException Exception if variation cannot be found. * * @param array $variation_data Key value pairs of attributes and values. * @param array $variable_product_attributes Product attributes we're expecting. * @return array */ protected function sanitize_variation_data( $variation_data, $variable_product_attributes ) { $return = []; foreach ( $variable_product_attributes as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } $attribute_label = wc_attribute_label( $attribute['name'] ); $variation_attribute_name = wc_variation_attribute_name( $attribute['name'] ); // Attribute labels e.g. Size. if ( isset( $variation_data[ $attribute_label ] ) ) { $return[ $variation_attribute_name ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute_label ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute_label ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); continue; } // Attribute slugs e.g. pa_size. if ( isset( $variation_data[ $attribute['name'] ] ) ) { $return[ $variation_attribute_name ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute['name'] ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute['name'] ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); } } return $return; } /** * Get product attributes from the variable product (which may be the parent if the product object is a variation). * * @throws RouteException Exception if product is invalid. * * @param \WC_Product $product Product being added to the cart. * @return array */ protected function get_variable_product_attributes( $product ) { if ( $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); } if ( ! $product || 'trash' === $product->get_status() ) { throw new RouteException( 'woocommerce_rest_cart_invalid_parent_product', __( 'This product cannot be added to the cart.', 'woocommerce' ), 400 ); } return $product->get_attributes(); } } packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php 0000644 00000000532 15132754524 0023473 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Utilities; /** * TooManyInCartException class. * * @internal This API is used internally by Blocks, this exception is thrown when more than one of a product that * can only be purchased individually is in a cart. */ class TooManyInCartException extends StockAvailabilityException { } packages/woocommerce-blocks/src/StoreApi/Routes/Products.php 0000644 00000031674 15132754524 0020251 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\Pagination; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\ProductQuery; /** * Products class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class Products extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of posts and add the post title filter option to \WP_Query. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $response = new \WP_REST_Response(); $product_query = new ProductQuery(); // Only get objects during GET requests. if ( \WP_REST_Server::READABLE === $request->get_method() ) { $query_results = $product_query->get_objects( $request ); $response_objects = []; foreach ( $query_results['objects'] as $object ) { $data = rest_ensure_response( $this->schema->get_item_response( $object ) ); $response_objects[] = $this->prepare_response_for_collection( $data ); } $response->set_data( $response_objects ); } else { $query_results = $product_query->get_results( $request ); } $response = ( new Pagination() )->add_headers( $response, $request, $query_results['total'], $query_results['pages'] ); $response->header( 'Last-Modified', $product_query->get_last_modified() ); return $response; } /** * Prepare links for the request. * * @param \WC_Product $item Product object. * @param \WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $item, $request ) { $links = array( 'self' => array( 'href' => rest_url( $this->get_namespace() . $this->get_path() . '/' . $item->get_id() ), ), 'collection' => array( 'href' => rest_url( $this->get_namespace() . $this->get_path() ), ), ); if ( $item->get_parent_id() ) { $links['up'] = array( 'href' => rest_url( $this->get_namespace() . $this->get_path() . '/' . $item->get_parent_id() ), ); } return $links; } /** * Get the query params for collections of products. * * @return array */ public function get_collection_params() { $params = []; $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; $params['page'] = array( 'description' => __( 'Current page of the collection.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set. Defaults to no limit if left blank.', 'woocommerce' ), 'type' => 'integer', 'default' => 10, 'minimum' => 0, 'maximum' => 100, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['search'] = array( 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['after'] = array( 'description' => __( 'Limit response to resources created after a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['before'] = array( 'description' => __( 'Limit response to resources created before a given ISO8601 compliant date.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); $params['date_column'] = array( 'description' => __( 'When limiting response using after/before, which date column to compare against.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'date_gmt', 'modified', 'modified_gmt', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => [], 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => [], 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'modified', 'id', 'include', 'title', 'slug', 'price', 'popularity', 'rating', 'menu_order', 'comment_count', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['parent'] = array( 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => [], 'sanitize_callback' => 'wp_parse_id_list', ); $params['parent_exclude'] = array( 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'sanitize_callback' => 'wp_parse_id_list', 'default' => [], ); $params['type'] = array( 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array_keys( wc_get_product_types() ), [ 'variation' ] ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['sku'] = array( 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['featured'] = array( 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'wc_string_to_bool', 'validate_callback' => 'rest_validate_request_arg', ); $params['category'] = array( 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['category_operator'] = array( 'description' => __( 'Operator to compare product category terms.', 'woocommerce' ), 'type' => 'string', 'enum' => [ 'in', 'not in', 'and' ], 'default' => 'in', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag'] = array( 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag_operator'] = array( 'description' => __( 'Operator to compare product tags.', 'woocommerce' ), 'type' => 'string', 'enum' => [ 'in', 'not in', 'and' ], 'default' => 'in', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['on_sale'] = array( 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'wc_string_to_bool', 'validate_callback' => 'rest_validate_request_arg', ); $params['min_price'] = array( 'description' => __( 'Limit result set to products based on a minimum price, provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['max_price'] = array( 'description' => __( 'Limit result set to products based on a maximum price, provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['stock_status'] = array( 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array_keys( wc_get_product_stock_status_options() ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), 'default' => [], ); $params['attributes'] = array( 'description' => __( 'Limit result set to products with selected global attributes.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'attribute' => array( 'description' => __( 'Attribute taxonomy name.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wc_sanitize_taxonomy_name', ), 'term_id' => array( 'description' => __( 'List of attribute term IDs.', 'woocommerce' ), 'type' => 'array', 'items' => [ 'type' => 'integer', ], 'sanitize_callback' => 'wp_parse_id_list', ), 'slug' => array( 'description' => __( 'List of attribute slug(s). If a term ID is provided, this will be ignored.', 'woocommerce' ), 'type' => 'array', 'items' => [ 'type' => 'string', ], 'sanitize_callback' => 'wp_parse_slug_list', ), 'operator' => array( 'description' => __( 'Operator to compare product attribute terms.', 'woocommerce' ), 'type' => 'string', 'enum' => [ 'in', 'not in', 'and' ], ), ), ), 'default' => [], ); $params['attribute_relation'] = array( 'description' => __( 'The logical relationship between attributes when filtering across multiple at once.', 'woocommerce' ), 'type' => 'string', 'enum' => [ 'in', 'and' ], 'default' => 'and', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['catalog_visibility'] = array( 'description' => __( 'Determines if hidden or visible catalog products are shown.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'any', 'visible', 'catalog', 'search', 'hidden' ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['rating'] = array( 'description' => __( 'Limit result set to products with a certain average rating.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', 'enum' => range( 1, 5 ), ), 'default' => [], 'sanitize_callback' => 'wp_parse_id_list', ); return $params; } } packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php 0000644 00000006257 15132754524 0017466 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use WP_Error; use WP_REST_Request; use WP_REST_Response; /** * Batch Route class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class Batch extends AbstractRoute implements RouteInterface { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/batch'; } /** * Constructor. */ public function __construct() {} /** * Get arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return array( 'callback' => [ $this, 'get_response' ], 'methods' => 'POST', 'permission_callback' => '__return_true', 'args' => array( 'validation' => array( 'type' => 'string', 'enum' => array( 'require-all-validate', 'normal' ), 'default' => 'normal', ), 'requests' => array( 'required' => true, 'type' => 'array', 'maxItems' => 25, 'items' => array( 'type' => 'object', 'properties' => array( 'method' => array( 'type' => 'string', 'enum' => array( 'POST', 'PUT', 'PATCH', 'DELETE' ), 'default' => 'POST', ), 'path' => array( 'type' => 'string', 'required' => true, ), 'body' => array( 'type' => 'object', 'properties' => array(), 'additionalProperties' => true, ), 'headers' => array( 'type' => 'object', 'properties' => array(), 'additionalProperties' => array( 'type' => array( 'string', 'array' ), 'items' => array( 'type' => 'string', ), ), ), ), ), ), ), ); } /** * Get the route response. * * @see WP_REST_Server::serve_batch_request_v1 * https://developer.wordpress.org/reference/classes/wp_rest_server/serve_batch_request_v1/ * * @throws RouteException On error. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function get_response( WP_REST_Request $request ) { try { foreach ( $request['requests'] as $args ) { if ( ! stristr( $args['path'], 'wc/store' ) ) { throw new RouteException( 'woocommerce_rest_invalid_path', __( 'Invalid path provided.', 'woocommerce' ), 400 ); } } $response = rest_get_server()->serve_batch_request_v1( $request ); } catch ( RouteException $error ) { $response = $this->get_route_error_response( $error->getErrorCode(), $error->getMessage(), $error->getCode(), $error->getAdditionalData() ); } catch ( \Exception $error ) { $response = $this->get_route_error_response( 'unknown_server_error', $error->getMessage(), 500 ); } if ( is_wp_error( $response ) ) { $response = $this->error_to_response( $response ); } $response->header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response->header( 'X-WC-Store-API-Nonce-Timestamp', time() ); $response->header( 'X-WC-Store-API-User', get_current_user_id() ); return $response; } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php 0000644 00000003134 15132754524 0022753 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductAttributeTerms class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductAttributeTerms extends AbstractTermsRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/attributes/(?P<attribute_id>[\d]+)/terms'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => array( 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute.', 'woocommerce' ), 'type' => 'integer', ), ), [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of attribute terms. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $attribute = wc_get_attribute( $request['attribute_id'] ); if ( ! $attribute || ! taxonomy_exists( $attribute->slug ) ) { throw new RouteException( 'woocommerce_rest_taxonomy_invalid', __( 'Attribute does not exist.', 'woocommerce' ), 404 ); } return $this->get_terms_response( $attribute->slug, $request ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php 0000644 00000007052 15132754524 0020332 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartItems class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartItems extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/items'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'get_response' ), 'permission_callback' => '__return_true', 'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ), ], [ 'methods' => \WP_REST_Server::DELETABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Get a collection of cart items. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $cart_items = $this->cart_controller->get_cart_items(); $items = []; foreach ( $cart_items as $cart_item ) { $data = $this->prepare_item_for_response( $cart_item, $request ); $items[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $items ); return $response; } /** * Creates one item from the collection. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { // Do not allow key to be specified during creation. if ( ! empty( $request['key'] ) ) { throw new RouteException( 'woocommerce_rest_cart_item_exists', __( 'Cannot create an existing cart item.', 'woocommerce' ), 400 ); } $result = $this->cart_controller->add_to_cart( [ 'id' => $request['id'], 'quantity' => $request['quantity'], 'variation' => $request['variation'], ] ); $response = rest_ensure_response( $this->prepare_item_for_response( $this->cart_controller->get_cart_item( $result ), $request ) ); $response->set_status( 201 ); return $response; } /** * Deletes all items in the cart. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_delete_response( \WP_REST_Request $request ) { $this->cart_controller->empty_cart(); return new \WP_REST_Response( [], 200 ); } /** * Prepare links for the request. * * @param array $cart_item Object to prepare. * @param \WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $cart_item, $request ) { $base = $this->get_namespace() . $this->get_path(); $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $cart_item['key'] ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); return $links; } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php 0000644 00000003244 15132754524 0022675 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductAttributesById class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductAttributesById extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/attributes/(?P<id>[\d]+)'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a single item. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $object = wc_get_attribute( (int) $request['id'] ); if ( ! $object || 0 === $object->id ) { throw new RouteException( 'woocommerce_rest_attribute_invalid_id', __( 'Invalid attribute ID.', 'woocommerce' ), 404 ); } $data = $this->prepare_item_for_response( $object, $request ); $response = rest_ensure_response( $data ); return $response; } } packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php 0000644 00000007454 15132754524 0021304 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartItemsByKey class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartItemsByKey extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/items/(?P<key>[\w-]{32})'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => [ 'key' => [ 'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ), 'type' => 'string', ], ], [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'get_response' ), 'permission_callback' => '__return_true', 'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ), ], [ 'methods' => \WP_REST_Server::DELETABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Get a single cart items. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $cart_item = $this->cart_controller->get_cart_item( $request['key'] ); if ( empty( $cart_item ) ) { throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item does not exist.', 'woocommerce' ), 404 ); } $data = $this->prepare_item_for_response( $cart_item, $request ); $response = rest_ensure_response( $data ); return $response; } /** * Update a single cart item. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_update_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); if ( isset( $request['quantity'] ) ) { $this->cart_controller->set_cart_item_quantity( $request['key'], $request['quantity'] ); } return rest_ensure_response( $this->prepare_item_for_response( $this->cart_controller->get_cart_item( $request['key'] ), $request ) ); } /** * Delete a single cart item. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_delete_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); $cart_item = $this->cart_controller->get_cart_item( $request['key'] ); if ( empty( $cart_item ) ) { throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item does not exist.', 'woocommerce' ), 404 ); } $cart->remove_cart_item( $request['key'] ); return new \WP_REST_Response( null, 204 ); } /** * Prepare links for the request. * * @param array $cart_item Object to prepare. * @param \WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $cart_item, $request ) { $base = $this->get_namespace() . $this->get_path(); $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $cart_item['key'] ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); return $links; } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php 0000644 00000015020 15132754524 0021416 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use WP_Comment_Query; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\Pagination; /** * ProductReviews class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductReviews extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/reviews'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of reviews. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $prepared_args = array( 'type' => 'review', 'status' => 'approve', 'no_found_rows' => false, 'offset' => $request['offset'], 'order' => $request['order'], 'number' => $request['per_page'], 'post__in' => $request['product_id'], ); /** * Map category id to list of product ids. */ if ( ! empty( $request['category_id'] ) ) { $category_ids = $request['category_id']; $child_ids = []; foreach ( $category_ids as $category_id ) { $child_ids = array_merge( $child_ids, get_term_children( $category_id, 'product_cat' ) ); } $category_ids = array_unique( array_merge( $category_ids, $child_ids ) ); $product_ids = get_objects_in_term( $category_ids, 'product_cat' ); $prepared_args['post__in'] = isset( $prepared_args['post__in'] ) ? array_merge( $prepared_args['post__in'], $product_ids ) : $product_ids; } if ( 'rating' === $request['orderby'] ) { $prepared_args['meta_query'] = array( // phpcs:ignore 'relation' => 'OR', array( 'key' => 'rating', 'compare' => 'EXISTS', ), array( 'key' => 'rating', 'compare' => 'NOT EXISTS', ), ); } $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); if ( empty( $request['offset'] ) ) { $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); } $query = new WP_Comment_Query(); $query_result = $query->query( $prepared_args ); $response_objects = array(); foreach ( $query_result as $review ) { $data = $this->prepare_item_for_response( $review, $request ); $response_objects[] = $this->prepare_response_for_collection( $data ); } $total_reviews = (int) $query->found_comments; $max_pages = (int) $query->max_num_pages; if ( $total_reviews < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); $query = new WP_Comment_Query(); $prepared_args['count'] = true; $total_reviews = $query->query( $prepared_args ); $max_pages = $request['per_page'] ? ceil( $total_reviews / $request['per_page'] ) : 1; } $response = rest_ensure_response( $response_objects ); $response = ( new Pagination() )->add_headers( $response, $request, $total_reviews, $max_pages ); return $response; } /** * Prepends internal property prefix to query parameters to match our response fields. * * @param string $query_param Query parameter. * @return string */ protected function normalize_query_param( $query_param ) { $prefix = 'comment_'; switch ( $query_param ) { case 'id': $normalized = $prefix . 'ID'; break; case 'product': $normalized = $prefix . 'post_ID'; break; case 'rating': $normalized = 'meta_value_num'; break; default: $normalized = $prefix . $query_param; break; } return $normalized; } /** * Get the query params for collections of products. * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; $params['page'] = array( 'description' => __( 'Current page of the collection.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set. Defaults to no limit if left blank.', 'woocommerce' ), 'type' => 'integer', 'default' => 10, 'minimum' => 0, 'maximum' => 100, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'date', 'enum' => array( 'date', 'date_gmt', 'id', 'rating', 'product', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['category_id'] = array( 'description' => __( 'Limit result set to reviews from specific category IDs.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); $params['product_id'] = array( 'description' => __( 'Limit result set to reviews from specific product IDs.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php 0000644 00000004621 15132754524 0021764 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartCouponsByCode class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartCouponsByCode extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/coupons/(?P<code>[\w-]+)'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => [ 'code' => [ 'description' => __( 'Unique identifier for the coupon within the cart.', 'woocommerce' ), 'type' => 'string', ], ], [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => \WP_REST_Server::DELETABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Get a single cart coupon. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) { throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); } return $this->prepare_item_for_response( $request['code'], $request ); } /** * Delete a single cart coupon. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_delete_response( \WP_REST_Request $request ) { if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) { throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); } $cart = $this->cart_controller->get_cart_instance(); $cart->remove_coupon( $request['code'] ); $cart->calculate_totals(); return new \WP_REST_Response( null, 204 ); } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php 0000644 00000002166 15132754524 0022066 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductCategories class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductCategories extends AbstractTermsRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/categories'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of terms. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { return $this->get_terms_response( 'product_cat', $request ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php 0000644 00000007221 15132754524 0020675 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartCoupons class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartCoupons extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/coupons'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ), ], [ 'methods' => \WP_REST_Server::DELETABLE, 'permission_callback' => '__return_true', 'callback' => [ $this, 'get_response' ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Get a collection of cart coupons. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $cart_coupons = $this->cart_controller->get_cart_coupons(); $items = []; foreach ( $cart_coupons as $coupon_code ) { $response = rest_ensure_response( $this->schema->get_item_response( $coupon_code ) ); $response->add_links( $this->prepare_links( $coupon_code, $request ) ); $response = $this->prepare_response_for_collection( $response ); $items[] = $response; } $response = rest_ensure_response( $items ); return $response; } /** * Add a coupon to the cart and return the result. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_coupons_enabled() ) { throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 ); } try { $this->cart_controller->apply_coupon( $request['code'] ); } catch ( \WC_REST_Exception $e ) { throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); } $response = $this->prepare_item_for_response( $request['code'], $request ); $response->set_status( 201 ); return $response; } /** * Deletes all coupons in the cart. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_delete_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); $cart->remove_coupons(); $cart->calculate_totals(); return new \WP_REST_Response( [], 200 ); } /** * Prepare links for the request. * * @param string $coupon_code Coupon code. * @param \WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $coupon_code, $request ) { $base = $this->get_namespace() . $this->get_path(); $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $coupon_code ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); return $links; } } packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php 0000644 00000003503 15132754524 0021517 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartApplyCoupon class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartApplyCoupon extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/apply-coupon'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'code' => [ 'description' => __( 'Unique identifier for the coupon within the cart.', 'woocommerce' ), 'type' => 'string', ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_coupons_enabled() ) { throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 ); } $cart = $this->cart_controller->get_cart_instance(); $coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) ); try { $this->cart_controller->apply_coupon( $coupon_code ); } catch ( \WC_REST_Exception $e ) { throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); } return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php 0000644 00000014124 15132754524 0022663 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\ProductQueryFilters; /** * ProductCollectionData route. * Get aggregate data from a collection of products. * * Supports the same parameters as /products, but returns a different response. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductCollectionData extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/collection-data'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of posts and add the post title filter option to \WP_Query. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $data = [ 'min_price' => null, 'max_price' => null, 'attribute_counts' => null, 'stock_status_counts' => null, 'rating_counts' => null, ]; $filters = new ProductQueryFilters(); if ( ! empty( $request['calculate_price_range'] ) ) { $filter_request = clone $request; $filter_request->set_param( 'min_price', null ); $filter_request->set_param( 'max_price', null ); $price_results = $filters->get_filtered_price( $filter_request ); $data['min_price'] = $price_results->min_price; $data['max_price'] = $price_results->max_price; } if ( ! empty( $request['calculate_stock_status_counts'] ) ) { $filter_request = clone $request; $counts = $filters->get_stock_status_counts( $filter_request ); $data['stock_status_counts'] = []; foreach ( $counts as $key => $value ) { $data['stock_status_counts'][] = (object) [ 'status' => $key, 'count' => $value, ]; } } if ( ! empty( $request['calculate_attribute_counts'] ) ) { $taxonomy__or_queries = []; $taxonomy__and_queries = []; foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) { if ( ! empty( $attributes_to_count['taxonomy'] ) ) { if ( empty( $attributes_to_count['query_type'] ) || 'or' === $attributes_to_count['query_type'] ) { $taxonomy__or_queries[] = $attributes_to_count['taxonomy']; } else { $taxonomy__and_queries[] = $attributes_to_count['taxonomy']; } } } $data['attribute_counts'] = []; // Or type queries need special handling because the attribute, if set, needs removing from the query first otherwise counts would not be correct. if ( $taxonomy__or_queries ) { foreach ( $taxonomy__or_queries as $taxonomy ) { $filter_request = clone $request; $filter_attributes = $filter_request->get_param( 'attributes' ); if ( ! empty( $filter_attributes ) ) { $filter_attributes = array_filter( $filter_attributes, function( $query ) use ( $taxonomy ) { return $query['attribute'] !== $taxonomy; } ); } $filter_request->set_param( 'attributes', $filter_attributes ); $counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] ); foreach ( $counts as $key => $value ) { $data['attribute_counts'][] = (object) [ 'term' => $key, 'count' => $value, ]; } } } if ( $taxonomy__and_queries ) { $counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries ); foreach ( $counts as $key => $value ) { $data['attribute_counts'][] = (object) [ 'term' => $key, 'count' => $value, ]; } } } if ( ! empty( $request['calculate_rating_counts'] ) ) { $filter_request = clone $request; $counts = $filters->get_rating_counts( $filter_request ); $data['rating_counts'] = []; foreach ( $counts as $key => $value ) { $data['rating_counts'][] = (object) [ 'rating' => $key, 'count' => $value, ]; } } return rest_ensure_response( $this->schema->get_item_response( $data ) ); } /** * Get the query params for collections of products. * * @return array */ public function get_collection_params() { $params = ( new Products( $this->schema ) )->get_collection_params(); $params['calculate_price_range'] = [ 'description' => __( 'If true, calculates the minimum and maximum product prices for the collection.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, ]; $params['calculate_stock_status_counts'] = [ 'description' => __( 'If true, calculates stock counts for products in the collection.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, ]; $params['calculate_attribute_counts'] = [ 'description' => __( 'If requested, calculates attribute term counts for products in the collection.', 'woocommerce' ), 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'taxonomy' => [ 'description' => __( 'Taxonomy name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'query_type' => [ 'description' => __( 'Query type being performed which may affect counts. Valid values include "and" and "or".', 'woocommerce' ), 'type' => 'string', 'enum' => [ 'and', 'or' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], 'default' => [], ]; $params['calculate_rating_counts'] = [ 'description' => __( 'If true, calculates rating counts for products in the collection.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, ]; return $params; } } packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php 0000644 00000003321 15132754524 0021320 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartRemoveItem class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartRemoveItem extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/remove-item'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'key' => [ 'description' => __( 'Unique identifier (key) for the cart item.', 'woocommerce' ), 'type' => 'string', ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); $cart_item = $this->cart_controller->get_cart_item( $request['key'] ); if ( empty( $cart_item ) ) { throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item no longer exists or is invalid.', 'woocommerce' ), 409 ); } $cart->remove_cart_item( $request['key'] ); $this->maybe_release_stock(); return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php 0000644 00000052136 15132754524 0020207 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\InvalidStockLevelsInCartException; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\AbstractSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\OrderController; use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException; use Automattic\WooCommerce\Blocks\Payments\PaymentResult; use Automattic\WooCommerce\Blocks\Payments\PaymentContext; /** * Checkout class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class Checkout extends AbstractCartRoute { /** * Holds the current order being processed. * * @var \WC_Order */ private $order = null; /** * Order controller class instance. * * @var OrderController */ protected $order_controller; /** * Constructor accepts two types of schema; one for the item being returned, and one for the cart as a whole. These * may be the same depending on the route. * * @param CartSchema $cart_schema Schema class for the cart. * @param AbstractSchema $item_schema Schema class for this route's items if it differs from the cart schema. * @param CartController $cart_controller Cart controller class. * @param OrderController $order_controller Order controller class. */ public function __construct( CartSchema $cart_schema, AbstractSchema $item_schema = null, CartController $cart_controller, OrderController $order_controller ) { $this->schema = is_null( $item_schema ) ? $cart_schema : $item_schema; $this->cart_schema = $cart_schema; $this->cart_controller = $cart_controller; $this->order_controller = $order_controller; } /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/checkout'; } /** * Checks if a nonce is required for the route. * * @param \WP_REST_Request $request Request. * @return bool */ protected function requires_nonce( \WP_REST_Request $request ) { return true; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => array_merge( [ 'payment_data' => [ 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woocommerce' ), 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'key' => [ 'type' => 'string', ], 'value' => [ 'type' => [ 'string', 'boolean' ], ], ], ], ], ], $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) ), ], [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'get_response' ), 'permission_callback' => '__return_true', 'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Prepare a single item for response. Handles setting the status based on the payment result. * * @param mixed $item Item to format to schema. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, \WP_REST_Request $request ) { $response = parent::prepare_item_for_response( $item, $request ); $status_codes = [ 'success' => 200, 'pending' => 202, 'failure' => 400, 'error' => 500, ]; if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); } return $response; } /** * Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $this->create_or_update_draft_order(); return $this->prepare_item_for_response( (object) [ 'order' => $this->order, 'payment_result' => new PaymentResult(), ], $request ); } /** * Update the current order. * * @internal Customer data is updated first so OrderController::update_addresses_from_cart uses up to date data. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_update_response( \WP_REST_Request $request ) { $this->update_customer_from_request( $request ); $this->create_or_update_draft_order(); $this->update_order_from_request( $request ); return $this->prepare_item_for_response( (object) [ 'order' => $this->order, 'payment_result' => new PaymentResult(), ], $request ); } /** * Update and process an order. * * 1. Obtain Draft Order * 2. Process Request * 3. Process Customer * 4. Validate Order * 5. Process Payment * * @throws RouteException On error. * @throws InvalidStockLevelsInCartException On error. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { /** * Validate items etc are allowed in the order before the order is processed. This will fix violations and tell * the customer. */ $this->cart_controller->validate_cart_items(); $this->cart_controller->validate_cart_coupons(); /** * Obtain Draft Order and process request data. * * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart * uses the up to date customer address. */ $this->update_customer_from_request( $request ); $this->create_or_update_draft_order(); $this->update_order_from_request( $request ); /** * Process customer data. * * Update order with customer details, and sign up a user account as necessary. */ $this->process_customer( $request ); /** * Validate order. * * This logic ensures the order is valid before payment is attempted. */ $this->order_controller->validate_order_before_payment( $this->order ); /** * WooCommerce Blocks Checkout Order Processed (experimental). * * This hook informs extensions that $order has completed processing and is ready for payment. * * This is similar to existing core hook woocommerce_checkout_order_processed. We're using a new action: * - To keep the interface focused (only pass $order, not passing request data). * - This also explicitly indicates these orders are from checkout block/StoreAPI. * * @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3238 * @internal This Hook is experimental and may change or be removed. * * @param \WC_Order $order Order object. */ do_action( '__experimental_woocommerce_blocks_checkout_order_processed', $this->order ); /** * Process the payment and return the results. */ $payment_result = new PaymentResult(); if ( $this->order->needs_payment() ) { $this->process_payment( $request, $payment_result ); } else { $this->process_without_payment( $request, $payment_result ); } return $this->prepare_item_for_response( (object) [ 'order' => wc_get_order( $this->order ), 'payment_result' => $payment_result, ], $request ); } /** * Get route response when something went wrong. * * @param string $error_code String based error code. * @param string $error_message User facing error message. * @param int $http_status_code HTTP status. Defaults to 500. * @param array $additional_data Extra data (key value pairs) to expose in the error response. * @return \WP_Error WP Error object. */ protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) { $error_from_message = new \WP_Error( $error_code, $error_message ); // 409 is when there was a conflict, so we return the cart so the client can resolve it. if ( 409 === $http_status_code ) { return $this->add_data_to_error_object( $error_from_message, $additional_data, $http_status_code, true ); } return $this->add_data_to_error_object( $error_from_message, $additional_data, $http_status_code ); } /** * Get route response when something went wrong. * * @param \WP_Error $error_object User facing error message. * @param int $http_status_code HTTP status. Defaults to 500. * @param array $additional_data Extra data (key value pairs) to expose in the error response. * @return \WP_Error WP Error object. */ protected function get_route_error_response_from_object( $error_object, $http_status_code = 500, $additional_data = [] ) { // 409 is when there was a conflict, so we return the cart so the client can resolve it. if ( 409 === $http_status_code ) { return $this->add_data_to_error_object( $error_object, $additional_data, $http_status_code, true ); } return $this->add_data_to_error_object( $error_object, $additional_data, $http_status_code ); } /** * Adds additional data to the \WP_Error object. * * @param \WP_Error $error The error object to add the cart to. * @param array $data The data to add to the error object. * @param int $http_status_code The HTTP status code this error should return. * @param bool $include_cart Whether the cart should be included in the error data. * @returns \WP_Error The \WP_Error with the cart added. */ private function add_data_to_error_object( $error, $data, $http_status_code, bool $include_cart = false ) { $data = array_merge( $data, [ 'status' => $http_status_code ] ); if ( $include_cart ) { $data = array_merge( $data, [ 'cart' => wc()->api->get_endpoint_data( '/wc/store/cart' ) ] ); } $error->add_data( $data ); return $error; } /** * Gets draft order data from the customer session. * * @return array */ private function get_draft_order_id() { return wc()->session->get( 'store_api_draft_order', 0 ); } /** * Updates draft order data in the customer session. * * @param integer $order_id Draft order ID. */ private function set_draft_order_id( $order_id ) { wc()->session->set( 'store_api_draft_order', $order_id ); } /** * Whether the passed argument is a draft order or an order that is * pending/failed and the cart hasn't changed. * * @param \WC_Order $order_object Order object to check. * @return boolean Whether the order is valid as a draft order. */ private function is_valid_draft_order( $order_object ) { if ( ! $order_object instanceof \WC_Order ) { return false; } // Draft orders are okay. if ( $order_object->has_status( 'checkout-draft' ) ) { return true; } // Pending and failed orders can be retried if the cart hasn't changed. if ( $order_object->needs_payment() && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) { return true; } return false; } /** * Create or update a draft order based on the cart. * * @throws RouteException On error. */ private function create_or_update_draft_order() { $this->order = $this->get_draft_order_id() ? wc_get_order( $this->get_draft_order_id() ) : null; if ( ! $this->is_valid_draft_order( $this->order ) ) { $this->order = $this->order_controller->create_order_from_cart(); } else { $this->order_controller->update_order_from_cart( $this->order ); } /** * WooCommerce Blocks Checkout Update Order Meta (experimental). * * This hook gives extensions the chance to add or update meta data on the $order. * * This is similar to existing core hook woocommerce_checkout_update_order_meta. * We're using a new action: * - To keep the interface focused (only pass $order, not passing request data). * - This also explicitly indicates these orders are from checkout block/StoreAPI. * * @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3686 * @internal This Hook is experimental and may change or be removed. * * @param \WC_Order $order Order object. */ do_action( '__experimental_woocommerce_blocks_checkout_update_order_meta', $this->order ); // Confirm order is valid before proceeding further. if ( ! $this->order instanceof \WC_Order ) { throw new RouteException( 'woocommerce_rest_checkout_missing_order', __( 'Unable to create order', 'woocommerce' ), 500 ); } // Store order ID to session. $this->set_draft_order_id( $this->order->get_id() ); // Try to reserve stock for 10 mins, if available. try { $reserve_stock = new ReserveStock(); $reserve_stock->reserve_stock_for_order( $this->order, 10 ); } catch ( ReserveStockException $e ) { $error_data = $e->getErrorData(); throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); } } /** * Updates the current customer session using data from the request (e.g. address data). * * Address session data is synced to the order itself later on by OrderController::update_order_from_cart() * * @param \WP_REST_Request $request Full details about the request. */ private function update_customer_from_request( \WP_REST_Request $request ) { $customer = wc()->customer; if ( isset( $request['billing_address'] ) ) { foreach ( $request['billing_address'] as $key => $value ) { if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { $customer->{"set_billing_$key"}( $value ); } } } if ( isset( $request['shipping_address'] ) ) { foreach ( $request['shipping_address'] as $key => $value ) { if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) { $customer->{"set_shipping_$key"}( $value ); } elseif ( 'phone' === $key ) { $customer->update_meta_data( 'shipping_phone', $value ); } } } $customer->save(); } /** * Update the current order using the posted values from the request. * * @param \WP_REST_Request $request Full details about the request. */ private function update_order_from_request( \WP_REST_Request $request ) { $this->order->set_customer_note( $request['customer_note'] ?? '' ); $this->order->set_payment_method( $this->get_request_payment_method( $request ) ); /** * WooCommerce Blocks Checkout Update Order From Request (experimental). * * This hook gives extensions the chance to update orders based on the data in the request. This can be used in * conjunction with the ExtendRestAPI class to post custom data and then process it. * * @internal This Hook is experimental and may change or be removed. * * @param \WC_Order $order Order object. * @param \WP_REST_Request $request Full details about the request. */ do_action( '__experimental_woocommerce_blocks_checkout_update_order_from_request', $this->order, $request ); $this->order->save(); } /** * For orders which do not require payment, just update status. * * @param \WP_REST_Request $request Request object. * @param PaymentResult $payment_result Payment result object. */ private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. $this->order->update_status( 'pending' ); $this->order->payment_complete(); // Mark the payment as successful. $payment_result->set_status( 'success' ); $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); } /** * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. * * @throws RouteException On error. * * @param \WP_REST_Request $request Request object. * @param PaymentResult $payment_result Payment result object. */ private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { try { // Transition the order to pending before making payment. $this->order->update_status( 'pending' ); // Prepare the payment context object to pass through payment hooks. $context = new PaymentContext(); $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); $context->set_payment_data( $this->get_request_payment_data( $request ) ); $context->set_order( $this->order ); /** * Process payment with context. * * @hook woocommerce_rest_checkout_process_payment_with_context * * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. * * @param PaymentContext $context Holds context for the payment, including order ID and payment method. * @param PaymentResult $payment_result Result object for the transaction. */ do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); if ( ! $payment_result instanceof PaymentResult ) { throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woocommerce' ), 500 ); } } catch ( \Exception $e ) { throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 400 ); } } /** * Gets the chosen payment method ID from the request. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return string */ private function get_request_payment_method_id( \WP_REST_Request $request ) { $payment_method_id = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) ); if ( empty( $payment_method_id ) ) { throw new RouteException( 'woocommerce_rest_checkout_missing_payment_method', __( 'No payment method provided.', 'woocommerce' ), 400 ); } return $payment_method_id; } /** * Gets the chosen payment method from the request. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WC_Payment_Gateway */ private function get_request_payment_method( \WP_REST_Request $request ) { $payment_method_id = $this->get_request_payment_method_id( $request ); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $payment_method_id ] ) ) { throw new RouteException( 'woocommerce_rest_checkout_payment_method_disabled', __( 'This payment gateway is not available.', 'woocommerce' ), 400 ); } return $available_gateways[ $payment_method_id ]; } /** * Gets and formats payment request data. * * @param \WP_REST_Request $request Request object. * @return array */ private function get_request_payment_data( \WP_REST_Request $request ) { static $payment_data = []; if ( ! empty( $payment_data ) ) { return $payment_data; } if ( ! empty( $request['payment_data'] ) ) { foreach ( $request['payment_data'] as $data ) { $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); } } return $payment_data; } /** * Order processing relating to customer account. * * Creates a customer account as needed (based on request & store settings) and updates the order with the new customer ID. * Updates the order with user details (e.g. address). * * @throws RouteException API error object with error details. * @param \WP_REST_Request $request Request object. */ private function process_customer( \WP_REST_Request $request ) { try { $create_account = Package::container()->get( CreateAccount::class ); $create_account->from_order_request( $request ); $this->order->set_customer_id( get_current_user_id() ); $this->order->save(); } catch ( \Exception $error ) { switch ( $error->getMessage() ) { case 'registration-error-invalid-email': throw new RouteException( 'registration-error-invalid-email', __( 'Please provide a valid email address.', 'woocommerce' ), 400 ); case 'registration-error-email-exists': throw new RouteException( 'registration-error-email-exists', __( 'An account is already registered with your email address. Please log in before proceeding.', 'woocommerce' ), 400 ); } } // Persist customer address data to account. $this->order_controller->sync_customer_data_with_order( $this->order ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php 0000644 00000003262 15132754524 0021311 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartUpdateItem class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartUpdateItem extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/update-item'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'key' => [ 'description' => __( 'Unique identifier (key) for the cart item to update.', 'woocommerce' ), 'type' => 'string', ], 'quantity' => [ 'description' => __( 'New quantity of the item in the cart.', 'woocommerce' ), 'type' => 'integer', ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); if ( isset( $request['quantity'] ) ) { $this->cart_controller->set_cart_item_quantity( $request['key'], $request['quantity'] ); } return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php 0000644 00000005604 15132754524 0020561 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartAddItem class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartAddItem extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/add-item'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'id' => [ 'description' => __( 'The cart item product or variation ID.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'sanitize_callback' => 'absint', ], ], 'quantity' => [ 'description' => __( 'Quantity of this item in the cart.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'sanitize_callback' => 'wc_stock_amount', ], ], 'variation' => [ 'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'attribute' => [ 'description' => __( 'Variation attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'value' => [ 'description' => __( 'Variation attribute value.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], ], ], ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { // Do not allow key to be specified during creation. if ( ! empty( $request['key'] ) ) { throw new RouteException( 'woocommerce_rest_cart_item_exists', __( 'Cannot create an existing cart item.', 'woocommerce' ), 400 ); } $cart = $this->cart_controller->get_cart_instance(); $result = $this->cart_controller->add_to_cart( [ 'id' => $request['id'], 'quantity' => $request['quantity'], 'variation' => $request['variation'], ] ); $response = rest_ensure_response( $this->schema->get_item_response( $cart ) ); $response->set_status( 201 ); return $response; } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php 0000644 00000003213 15132754524 0022630 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductCategoriesById class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductCategoriesById extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/categories/(?P<id>[\d]+)'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a single item. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $object = get_term( (int) $request['id'], 'product_cat' ); if ( ! $object || 0 === $object->id ) { throw new RouteException( 'woocommerce_rest_category_invalid_id', __( 'Invalid category ID.', 'woocommerce' ), 404 ); } $data = $this->prepare_item_for_response( $object, $request ); return rest_ensure_response( $data ); } } packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php 0000644 00000013270 15132754524 0022032 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\AbstractSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; /** * Abstract Cart Route * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ abstract class AbstractCartRoute extends AbstractRoute { /** * Schema class for this route's response. * * @var AbstractSchema|CartSchema */ protected $schema; /** * Schema class for the cart. * * @var CartSchema */ protected $cart_schema; /** * Cart controller class instance. * * @var CartController */ protected $cart_controller; /** * Constructor accepts two types of schema; one for the item being returned, and one for the cart as a whole. These * may be the same depending on the route. * * @param CartSchema $cart_schema Schema class for the cart. * @param AbstractSchema $item_schema Schema class for this route's items if it differs from the cart schema. * @param CartController $cart_controller Cart controller class. */ public function __construct( CartSchema $cart_schema, AbstractSchema $item_schema = null, CartController $cart_controller ) { $this->schema = is_null( $item_schema ) ? $cart_schema : $item_schema; $this->cart_schema = $cart_schema; $this->cart_controller = $cart_controller; } /** * Get the route response based on the type of request. * * @param \WP_REST_Request $request Request object. * @return \WP_Error|\WP_REST_Response */ public function get_response( \WP_REST_Request $request ) { $this->cart_controller->load_cart(); $this->calculate_totals(); if ( $this->requires_nonce( $request ) ) { $nonce_check = $this->check_nonce( $request ); if ( is_wp_error( $nonce_check ) ) { return $this->add_nonce_headers( $this->error_to_response( $nonce_check ) ); } } try { $response = parent::get_response( $request ); } catch ( RouteException $error ) { $response = $this->get_route_error_response( $error->getErrorCode(), $error->getMessage(), $error->getCode(), $error->getAdditionalData() ); } catch ( \Exception $error ) { $response = $this->get_route_error_response( 'unknown_server_error', $error->getMessage(), 500 ); } if ( is_wp_error( $response ) ) { $response = $this->error_to_response( $response ); } return $this->add_nonce_headers( $response ); } /** * Add nonce headers to a response object. * * @param \WP_REST_Response $response The response object. * @return \WP_REST_Response */ protected function add_nonce_headers( \WP_REST_Response $response ) { $response->header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response->header( 'X-WC-Store-API-Nonce-Timestamp', time() ); $response->header( 'X-WC-Store-API-User', get_current_user_id() ); return $response; } /** * Checks if a nonce is required for the route. * * @param \WP_REST_Request $request Request. * @return bool */ protected function requires_nonce( \WP_REST_Request $request ) { return 'GET' !== $request->get_method(); } /** * Ensures the cart totals are calculated before an API response is generated. */ protected function calculate_totals() { wc()->cart->get_cart(); wc()->cart->calculate_fees(); wc()->cart->calculate_shipping(); wc()->cart->calculate_totals(); } /** * If there is a draft order, releases stock. * * @return void */ protected function maybe_release_stock() { $draft_order = wc()->session->get( 'store_api_draft_order', 0 ); if ( ! $draft_order ) { return; } wc_release_stock_for_order( $draft_order ); } /** * For non-GET endpoints, require and validate a nonce to prevent CSRF attacks. * * Nonces will mismatch if the logged in session cookie is different! If using a client to test, set this cookie * to match the logged in cookie in your browser. * * @param \WP_REST_Request $request Request object. * @return \WP_Error|boolean */ protected function check_nonce( \WP_REST_Request $request ) { $nonce = $request->get_header( 'X-WC-Store-API-Nonce' ); if ( apply_filters( 'woocommerce_store_api_disable_nonce_check', false ) ) { return true; } if ( null === $nonce ) { return $this->get_route_error_response( 'woocommerce_rest_missing_nonce', __( 'Missing the X-WC-Store-API-Nonce header. This endpoint requires a valid nonce.', 'woocommerce' ), 401 ); } if ( ! wp_verify_nonce( $nonce, 'wc_store_api' ) ) { return $this->get_route_error_response( 'woocommerce_rest_invalid_nonce', __( 'X-WC-Store-API-Nonce is invalid.', 'woocommerce' ), 403 ); } return true; } /** * Get route response when something went wrong. * * @param string $error_code String based error code. * @param string $error_message User facing error message. * @param int $http_status_code HTTP status. Defaults to 500. * @param array $additional_data Extra data (key value pairs) to expose in the error response. * @return \WP_Error WP Error object. */ protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) { switch ( $http_status_code ) { case 409: // If there was a conflict, return the cart so the client can resolve it. $cart = $this->cart_controller->get_cart_instance(); return new \WP_Error( $error_code, $error_message, array_merge( $additional_data, [ 'status' => $http_status_code, 'cart' => $this->cart_schema->get_item_response( $cart ), ] ) ); } return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] ); } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php 0000644 00000002556 15132754524 0022132 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductAttributes class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductAttributes extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/attributes'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of attributes. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $ids = wc_get_attribute_taxonomy_ids(); $return = []; foreach ( $ids as $id ) { $object = wc_get_attribute( $id ); $data = $this->prepare_item_for_response( $object, $request ); $return[] = $this->prepare_response_for_collection( $data ); } return rest_ensure_response( $return ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php 0000644 00000004341 15132754524 0021670 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartRemoveCoupon class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartRemoveCoupon extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/remove-coupon'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'code' => [ 'description' => __( 'Unique identifier for the coupon within the cart.', 'woocommerce' ), 'type' => 'string', ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_coupons_enabled() ) { throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 ); } $cart = $this->cart_controller->get_cart_instance(); $coupon_code = wc_format_coupon_code( $request['code'] ); $coupon = new \WC_Coupon( $coupon_code ); if ( $coupon->get_code() !== $coupon_code || ! $coupon->is_valid() ) { throw new RouteException( 'woocommerce_rest_cart_coupon_error', __( 'Invalid coupon code.', 'woocommerce' ), 400 ); } if ( ! $this->cart_controller->has_coupon( $coupon_code ) ) { throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon cannot be removed because it is not already applied to the cart.', 'woocommerce' ), 409 ); } $cart = $this->cart_controller->get_cart_instance(); $cart->remove_coupon( $coupon_code ); $cart->calculate_totals(); return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php 0000644 00000001055 15132754524 0021353 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * RouteInterface. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ interface RouteInterface { /** * Get the namespace for this route. * * @return string */ public function get_namespace(); /** * Get the path of this REST route. * * @return string */ public function get_path(); /** * Get arguments for this REST route. * * @return array An array of endpoints. */ public function get_args(); } packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php 0000644 00000002144 15132754524 0020673 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductTags class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductTags extends AbstractTermsRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/tags'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => $this->get_collection_params(), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a collection of terms. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { return $this->get_terms_response( 'product_tag', $request ); } } packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php 0000644 00000021101 15132754524 0021210 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\AbstractSchema; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\InvalidStockLevelsInCartException; use WP_Error; /** * AbstractRoute class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ abstract class AbstractRoute implements RouteInterface { /** * Schema class instance. * * @var AbstractSchema */ protected $schema; /** * Constructor. * * @param AbstractSchema $schema Schema class for this route. */ public function __construct( AbstractSchema $schema ) { $this->schema = $schema; } /** * Get the namespace for this route. * * @return string */ public function get_namespace() { return 'wc/store'; } /** * Get item schema properties. * * @return array */ public function get_item_schema() { return $this->schema->get_item_schema(); } /** * Get the route response based on the type of request. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public function get_response( \WP_REST_Request $request ) { $response = null; try { switch ( $request->get_method() ) { case 'POST': $response = $this->get_route_post_response( $request ); break; case 'PUT': case 'PATCH': $response = $this->get_route_update_response( $request ); break; case 'DELETE': $response = $this->get_route_delete_response( $request ); break; default: $response = $this->get_route_response( $request ); break; } } catch ( RouteException $error ) { $response = $this->get_route_error_response( $error->getErrorCode(), $error->getMessage(), $error->getCode(), $error->getAdditionalData() ); } catch ( InvalidStockLevelsInCartException $error ) { $response = $this->get_route_error_response_from_object( $error->getError(), $error->getCode(), $error->getAdditionalData() ); } catch ( \Exception $error ) { $response = $this->get_route_error_response( 'unknown_server_error', $error->getMessage(), 500 ); } if ( is_wp_error( $response ) ) { $response = $this->error_to_response( $response ); } return $response; } /** * Converts an error to a response object. Based on \WP_REST_Server. * * @param WP_Error $error WP_Error instance. * @return WP_REST_Response List of associative arrays with code and message keys. */ protected function error_to_response( $error ) { $error_data = $error->get_error_data(); $status = isset( $error_data, $error_data['status'] ) ? $error_data['status'] : 500; $errors = []; foreach ( (array) $error->errors as $code => $messages ) { foreach ( (array) $messages as $message ) { $errors[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ), ); } } $data = array_shift( $errors ); if ( count( $errors ) ) { $data['additional_errors'] = $errors; } return new \WP_REST_Response( $data, $status ); } /** * Get route response for GET requests. * * When implemented, should return a \WP_REST_Response. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. */ protected function get_route_response( \WP_REST_Request $request ) { throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 ); } /** * Get route response for POST requests. * * When implemented, should return a \WP_REST_Response. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. */ protected function get_route_post_response( \WP_REST_Request $request ) { throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 ); } /** * Get route response for PUT requests. * * When implemented, should return a \WP_REST_Response. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. */ protected function get_route_update_response( \WP_REST_Request $request ) { throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 ); } /** * Get route response for DELETE requests. * * When implemented, should return a \WP_REST_Response. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. */ protected function get_route_delete_response( \WP_REST_Request $request ) { throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 ); } /** * Get route response when something went wrong. * * @param string $error_code String based error code. * @param string $error_message User facing error message. * @param int $http_status_code HTTP status. Defaults to 500. * @param array $additional_data Extra data (key value pairs) to expose in the error response. * @return \WP_Error WP Error object. */ protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) { return new \WP_Error( $error_code, $error_message, array_merge( $additional_data, [ 'status' => $http_status_code ] ) ); } /** * Get route response when something went wrong and the supplied error is a WP_Error. This currently only happens * when an item in the cart is out of stock, partially out of stock, can only be bought individually, or when the * item is not purchasable. * * @param WP_Error $error_object The WP_Error object containing the error. * @param int $http_status_code HTTP status. Defaults to 500. * @param array $additional_data Extra data (key value pairs) to expose in the error response. * @return WP_Error WP Error object. */ protected function get_route_error_response_from_object( $error_object, $http_status_code = 500, $additional_data = [] ) { $error_object->add_data( array_merge( $additional_data, [ 'status' => $http_status_code ] ) ); return $error_object; } /** * Prepare a single item for response. * * @param mixed $item Item to format to schema. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response $response Response data. */ public function prepare_item_for_response( $item, \WP_REST_Request $request ) { $response = rest_ensure_response( $this->schema->get_item_response( $item ) ); $response->add_links( $this->prepare_links( $item, $request ) ); return $response; } /** * Retrieves the context param. * * Ensures consistent descriptions between endpoints, and populates enum from schema. * * @param array $args Optional. Additional arguments for context parameter. Default empty array. * @return array Context parameter details. */ protected function get_context_param( $args = array() ) { $param_details = array( 'description' => __( 'Scope under which the request is made; determines fields present in response.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $schema = $this->get_item_schema(); if ( empty( $schema['properties'] ) ) { return array_merge( $param_details, $args ); } $contexts = array(); foreach ( $schema['properties'] as $attributes ) { if ( ! empty( $attributes['context'] ) ) { $contexts = array_merge( $contexts, $attributes['context'] ); } } if ( ! empty( $contexts ) ) { $param_details['enum'] = array_unique( $contexts ); rsort( $param_details['enum'] ); } return array_merge( $param_details, $args ); } /** * Prepares a response for insertion into a collection. * * @param \WP_REST_Response $response Response object. * @return array|mixed Response data, ready for insertion into collection data. */ protected function prepare_response_for_collection( \WP_REST_Response $response ) { $data = (array) $response->get_data(); $server = rest_get_server(); $links = $server::get_compact_response_links( $response ); if ( ! empty( $links ) ) { $data['_links'] = $links; } return $data; } /** * Prepare links for the request. * * @param mixed $item Item to prepare. * @param \WP_REST_Request $request Request object. * @return array */ protected function prepare_links( $item, $request ) { return []; } /** * Retrieves the query params for the collections. * * @return array Query parameters for the collection. */ public function get_collection_params() { return array( 'context' => $this->get_context_param(), ); } } packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php 0000644 00000002575 15132754524 0021421 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ReserveStockRouteExceptionException class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class RouteException extends \Exception { /** * Sanitized error code. * * @var string */ public $error_code; /** * Additional error data. * * @var array */ public $additional_data = []; /** * Setup exception. * * @param string $error_code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'. * @param int $http_status_code Proper HTTP status code to respond with, e.g. 400. * @param array $additional_data Extra data (key value pairs) to expose in the error response. */ public function __construct( $error_code, $message, $http_status_code = 400, $additional_data = [] ) { $this->error_code = $error_code; $this->additional_data = array_filter( (array) $additional_data ); parent::__construct( $message, $http_status_code ); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns additional error data. * * @return array */ public function getAdditionalData() { return $this->additional_data; } } packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php 0000644 00000002326 15132754524 0017327 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * Cart class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class Cart extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_instance() ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php 0000644 00000004543 15132754524 0023010 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartSelectShippingRate class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartSelectShippingRate extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/select-shipping-rate'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'package_id' => array( 'description' => __( 'The ID of the package being shipped.', 'woocommerce' ), 'type' => [ 'integer', 'string' ], 'required' => true, ), 'rate_id' => [ 'description' => __( 'The chosen rate ID for the package.', 'woocommerce' ), 'type' => 'string', 'required' => true, ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_shipping_enabled() ) { throw new RouteException( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woocommerce' ), 404 ); } if ( ! isset( $request['package_id'] ) ) { throw new RouteException( 'woocommerce_rest_cart_missing_package_id', __( 'Invalid Package ID.', 'woocommerce' ), 400 ); } $cart = $this->cart_controller->get_cart_instance(); $package_id = wc_clean( wp_unslash( $request['package_id'] ) ); $rate_id = wc_clean( wp_unslash( $request['rate_id'] ) ); try { $this->cart_controller->select_shipping_rate( $package_id, $rate_id ); } catch ( \WC_Rest_Exception $e ) { throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); } $cart->calculate_shipping(); $cart->calculate_totals(); return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php 0000644 00000011352 15132754524 0022232 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\Pagination; use WP_Term_Query; /** * AbstractTermsRoute class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ abstract class AbstractTermsRoute extends AbstractRoute { /** * Get the query params for collections of attributes. * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; $params['page'] = array( 'description' => __( 'Current page of the collection.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set. Defaults to no limit if left blank.', 'woocommerce' ), 'type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['search'] = array( 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['order'] = array( 'description' => __( 'Sort ascending or descending.', 'woocommerce' ), 'type' => 'string', 'default' => 'asc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'description' => __( 'Sort by term property.', 'woocommerce' ), 'type' => 'string', 'default' => 'name', 'enum' => array( 'name', 'slug', 'count', ), 'validate_callback' => 'rest_validate_request_arg', ); $params['hide_empty'] = array( 'description' => __( 'If true, empty terms will not be returned.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, ); return $params; } /** * Get terms matching passed in args. * * @param string $taxonomy Taxonomy to get terms from. * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ protected function get_terms_response( $taxonomy, $request ) { $page = (int) $request['page']; $per_page = $request['per_page'] ? (int) $request['per_page'] : 0; $prepared_args = array( 'taxonomy' => $taxonomy, 'exclude' => $request['exclude'], 'include' => $request['include'], 'order' => $request['order'], 'orderby' => $request['orderby'], 'hide_empty' => (bool) $request['hide_empty'], 'number' => $per_page, 'offset' => $per_page > 0 ? ( $page - 1 ) * $per_page : 0, 'search' => $request['search'], ); $term_query = new WP_Term_Query(); $objects = $term_query->query( $prepared_args ); $return = []; foreach ( $objects as $object ) { $data = $this->prepare_item_for_response( $object, $request ); $return[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $return ); // See if pagination is needed before calculating. if ( $per_page > 0 && ( count( $objects ) === $per_page || $page > 1 ) ) { $term_count = $this->get_term_count( $taxonomy, $prepared_args ); $response = ( new Pagination() )->add_headers( $response, $request, $term_count, ceil( $term_count / $per_page ) ); } return $response; } /** * Get count of terms for current query. * * @param string $taxonomy Taxonomy to get terms from. * @param array $args Array of args to pass to wp_count_terms. * @return int */ protected function get_term_count( $taxonomy, $args ) { $count_args = $args; unset( $count_args['number'], $count_args['offset'] ); return (int) wp_count_terms( $taxonomy, $count_args ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php 0000644 00000003221 15132754524 0021402 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * CartExtensions class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartExtensions extends AbstractCartRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/extensions'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'namespace' => [ 'description' => __( 'Extension\'s name - this will be used to ensure the data in the request is routed appropriately.', 'woocommerce' ), 'type' => 'string', ], 'data' => [ 'description' => __( 'Additional data to pass to the extension', 'woocommerce' ), 'type' => 'object', ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { try { return $this->schema->get_item_response( $request ); } catch ( \WC_REST_Exception $e ) { throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); } } } packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php 0000644 00000003116 15132754524 0021007 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; /** * ProductsById class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductsById extends AbstractRoute { /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/products/(?P<id>[\d]+)'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ), ], 'schema' => [ $this->schema, 'get_public_item_schema' ], ]; } /** * Get a single item. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { $object = wc_get_product( (int) $request['id'] ); if ( ! $object || 0 === $object->get_id() ) { throw new RouteException( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), 404 ); } return rest_ensure_response( $this->schema->get_item_response( $object ) ); } } packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php 0000644 00000020143 15132754524 0022211 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Routes; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\BillingAddressSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\ShippingAddressSchema; /** * CartUpdateCustomer class. * * Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartUpdateCustomer extends AbstractCartRoute { /** * Get the namespace for this route. * * @return string */ public function get_namespace() { return 'wc/store'; } /** * Get the path of this REST route. * * @return string */ public function get_path() { return '/cart/update-customer'; } /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { return [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], 'permission_callback' => '__return_true', 'args' => [ 'billing_address' => [ 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'properties' => $this->schema->billing_address_schema->get_properties(), 'sanitize_callback' => [ $this->schema->billing_address_schema, 'sanitize_callback' ], 'validate_callback' => [ $this->schema->billing_address_schema, 'validate_callback' ], ], 'shipping_address' => [ 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'properties' => $this->schema->shipping_address_schema->get_properties(), 'sanitize_callback' => [ $this->schema->shipping_address_schema, 'sanitize_callback' ], 'validate_callback' => [ $this->schema->shipping_address_schema, 'validate_callback' ], ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], 'allow_batch' => [ 'v1' => true ], ]; } /** * Handle the request and return a valid response for this endpoint. * * @throws RouteException On error. * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { $cart = $this->cart_controller->get_cart_instance(); $billing = isset( $request['billing_address'] ) ? $request['billing_address'] : []; $shipping = isset( $request['shipping_address'] ) ? $request['shipping_address'] : []; // If the cart does not need shipping, shipping address is forced to match billing address unless defined. if ( ! $cart->needs_shipping() && ! isset( $request['shipping_address'] ) ) { $shipping = isset( $request['billing_address'] ) ? $request['billing_address'] : [ 'first_name' => wc()->customer->get_billing_first_name(), 'last_name' => wc()->customer->get_billing_last_name(), 'company' => wc()->customer->get_billing_company(), 'address_1' => wc()->customer->get_billing_address_1(), 'address_2' => wc()->customer->get_billing_address_2(), 'city' => wc()->customer->get_billing_city(), 'state' => wc()->customer->get_billing_state(), 'postcode' => wc()->customer->get_billing_postcode(), 'country' => wc()->customer->get_billing_country(), 'phone' => wc()->customer->get_billing_phone(), ]; } wc()->customer->set_props( array( 'billing_first_name' => isset( $billing['first_name'] ) ? $billing['first_name'] : null, 'billing_last_name' => isset( $billing['last_name'] ) ? $billing['last_name'] : null, 'billing_company' => isset( $billing['company'] ) ? $billing['company'] : null, 'billing_address_1' => isset( $billing['address_1'] ) ? $billing['address_1'] : null, 'billing_address_2' => isset( $billing['address_2'] ) ? $billing['address_2'] : null, 'billing_city' => isset( $billing['city'] ) ? $billing['city'] : null, 'billing_state' => isset( $billing['state'] ) ? $billing['state'] : null, 'billing_postcode' => isset( $billing['postcode'] ) ? $billing['postcode'] : null, 'billing_country' => isset( $billing['country'] ) ? $billing['country'] : null, 'billing_phone' => isset( $billing['phone'] ) ? $billing['phone'] : null, 'billing_email' => isset( $request['billing_address'], $request['billing_address']['email'] ) ? $request['billing_address']['email'] : null, 'shipping_first_name' => isset( $shipping['first_name'] ) ? $shipping['first_name'] : null, 'shipping_last_name' => isset( $shipping['last_name'] ) ? $shipping['last_name'] : null, 'shipping_company' => isset( $shipping['company'] ) ? $shipping['company'] : null, 'shipping_address_1' => isset( $shipping['address_1'] ) ? $shipping['address_1'] : null, 'shipping_address_2' => isset( $shipping['address_2'] ) ? $shipping['address_2'] : null, 'shipping_city' => isset( $shipping['city'] ) ? $shipping['city'] : null, 'shipping_state' => isset( $shipping['state'] ) ? $shipping['state'] : null, 'shipping_postcode' => isset( $shipping['postcode'] ) ? $shipping['postcode'] : null, 'shipping_country' => isset( $shipping['country'] ) ? $shipping['country'] : null, ) ); $shipping_phone_value = isset( $shipping['phone'] ) ? $shipping['phone'] : null; // @todo Remove custom shipping_phone handling (requires WC 5.6+) if ( is_callable( [ wc()->customer, 'set_shipping_phone' ] ) ) { wc()->customer->set_shipping_phone( $shipping_phone_value ); } else { wc()->customer->update_meta_data( 'shipping_phone', $shipping_phone_value ); } wc()->customer->save(); $this->calculate_totals(); $this->maybe_update_order(); return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } /** * If there is a draft order, update customer data there also. * * @return void */ protected function maybe_update_order() { $draft_order_id = wc()->session->get( 'store_api_draft_order', 0 ); $draft_order = $draft_order_id ? wc_get_order( $draft_order_id ) : false; if ( ! $draft_order ) { return; } $draft_order->set_props( [ 'billing_first_name' => wc()->customer->get_billing_first_name(), 'billing_last_name' => wc()->customer->get_billing_last_name(), 'billing_company' => wc()->customer->get_billing_company(), 'billing_address_1' => wc()->customer->get_billing_address_1(), 'billing_address_2' => wc()->customer->get_billing_address_2(), 'billing_city' => wc()->customer->get_billing_city(), 'billing_state' => wc()->customer->get_billing_state(), 'billing_postcode' => wc()->customer->get_billing_postcode(), 'billing_country' => wc()->customer->get_billing_country(), 'billing_email' => wc()->customer->get_billing_email(), 'billing_phone' => wc()->customer->get_billing_phone(), 'shipping_first_name' => wc()->customer->get_shipping_first_name(), 'shipping_last_name' => wc()->customer->get_shipping_last_name(), 'shipping_company' => wc()->customer->get_shipping_company(), 'shipping_address_1' => wc()->customer->get_shipping_address_1(), 'shipping_address_2' => wc()->customer->get_shipping_address_2(), 'shipping_city' => wc()->customer->get_shipping_city(), 'shipping_state' => wc()->customer->get_shipping_state(), 'shipping_postcode' => wc()->customer->get_shipping_postcode(), 'shipping_country' => wc()->customer->get_shipping_country(), ] ); $shipping_phone_value = is_callable( [ wc()->customer, 'get_shipping_phone' ] ) ? wc()->customer->get_shipping_phone() : wc()->customer->get_meta( 'shipping_phone', true ); if ( is_callable( [ $draft_order, 'set_shipping_phone' ] ) ) { $draft_order->set_shipping_phone( $shipping_phone_value ); } else { $draft_order->update_meta_data( '_shipping_phone', $shipping_phone_value ); } $draft_order->save(); } } packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php 0000644 00000000742 15132754524 0023067 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Formatters; /** * FormatterInterface. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ interface FormatterInterface { /** * Format a given value and return the result. * * @param mixed $value Value to format. * @param array $options Options that influence the formatting. * @return mixed */ public function format( $value, array $options = [] ); } packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php 0000644 00000001016 15132754524 0022546 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Formatters; /** * Default Formatter. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class DefaultFormatter implements FormatterInterface { /** * Format a given value and return the result. * * @param mixed $value Value to format. * @param array $options Options that influence the formatting. * @return mixed */ public function format( $value, array $options = [] ) { return $value; } } packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php 0000644 00000002671 15132754524 0022764 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Formatters; /** * Currency Formatter. * * Formats an array of monetary values by inserting currency data. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CurrencyFormatter implements FormatterInterface { /** * Format a given value and return the result. * * @param array $value Value to format. * @param array $options Options that influence the formatting. * @return array */ public function format( $value, array $options = [] ) { $position = get_option( 'woocommerce_currency_pos' ); $symbol = html_entity_decode( get_woocommerce_currency_symbol() ); $prefix = ''; $suffix = ''; switch ( $position ) { case 'left_space': $prefix = $symbol . ' '; break; case 'left': $prefix = $symbol; break; case 'right_space': $suffix = ' ' . $symbol; break; case 'right': $suffix = $symbol; break; } return array_merge( (array) $value, [ 'currency_code' => get_woocommerce_currency(), 'currency_symbol' => $symbol, 'currency_minor_unit' => wc_get_price_decimals(), 'currency_decimal_separator' => wc_get_price_decimal_separator(), 'currency_thousand_separator' => wc_get_price_thousand_separator(), 'currency_prefix' => $prefix, 'currency_suffix' => $suffix, ] ); } } packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php 0000644 00000001611 15132754524 0022067 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Formatters; /** * Html Formatter. * * Formats HTML in API responses. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class HtmlFormatter implements FormatterInterface { /** * Format a given value and return the result. * * The wptexturize, convert_chars, and trim functions are also used in the `the_title` filter. * The function wp_kses_post removes disallowed HTML tags. * * @param string|array $value Value to format. * @param array $options Options that influence the formatting. * @return string */ public function format( $value, array $options = [] ) { if ( is_array( $value ) ) { return array_map( [ $this, 'format' ], $value ); } return is_scalar( $value ) ? wp_kses_post( trim( convert_chars( wptexturize( $value ) ) ) ) : $value; } } packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php 0000644 00000001565 15132754524 0022262 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Formatters; /** * Money Formatter. * * Formats monetary values using store settings. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class MoneyFormatter implements FormatterInterface { /** * Format a given value and return the result. * * @param mixed $value Value to format. * @param array $options Options that influence the formatting. * @return mixed */ public function format( $value, array $options = [] ) { $options = wp_parse_args( $options, [ 'decimals' => wc_get_price_decimals(), 'rounding_mode' => PHP_ROUND_HALF_UP, ] ); return (string) intval( round( ( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( $options['decimals'] ) ), 0, absint( $options['rounding_mode'] ) ) ); } } packages/woocommerce-blocks/src/StoreApi/RoutesController.php 0000644 00000011044 15132754524 0020477 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi; use Routes\AbstractRoute; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\OrderController; /** * RoutesController class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class RoutesController { /** * Stores schemas. * * @var SchemaController */ protected $schemas; /** * Stores routes. * * @var AbstractRoute[] */ protected $routes = []; /** * Constructor. * * @param SchemaController $schemas Schema controller class passed to each route. */ public function __construct( SchemaController $schemas ) { $this->schemas = $schemas; $this->initialize(); } /** * Get a route class instance. * * @throws Exception If the schema does not exist. * * @param string $name Name of schema. * @return AbstractRoute */ public function get( $name ) { if ( ! isset( $this->routes[ $name ] ) ) { throw new Exception( $name . ' route does not exist' ); } return $this->routes[ $name ]; } /** * Register defined list of routes with WordPress. */ public function register_routes() { foreach ( $this->routes as $route ) { register_rest_route( $route->get_namespace(), $route->get_path(), $route->get_args() ); } } /** * Load route class instances. */ protected function initialize() { global $wp_version; $cart_controller = new CartController(); $order_controller = new OrderController(); $this->routes = [ 'cart' => new Routes\Cart( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-add-item' => new Routes\CartAddItem( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-apply-coupon' => new Routes\CartApplyCoupon( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-coupons' => new Routes\CartCoupons( $this->schemas->get( 'cart' ), $this->schemas->get( 'cart-coupon' ), $cart_controller ), 'cart-coupons-by-code' => new Routes\CartCouponsByCode( $this->schemas->get( 'cart' ), $this->schemas->get( 'cart-coupon' ), $cart_controller ), 'cart-extensions' => new Routes\CartExtensions( $this->schemas->get( 'cart' ), $this->schemas->get( 'cart-extensions' ), $cart_controller ), 'cart-items' => new Routes\CartItems( $this->schemas->get( 'cart' ), $this->schemas->get( 'cart-item' ), $cart_controller ), 'cart-items-by-key' => new Routes\CartItemsByKey( $this->schemas->get( 'cart' ), $this->schemas->get( 'cart-item' ), $cart_controller ), 'cart-remove-coupon' => new Routes\CartRemoveCoupon( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-remove-item' => new Routes\CartRemoveItem( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-select-shipping-rate' => new Routes\CartSelectShippingRate( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-update-item' => new Routes\CartUpdateItem( $this->schemas->get( 'cart' ), null, $cart_controller ), 'cart-update-customer' => new Routes\CartUpdateCustomer( $this->schemas->get( 'cart' ), null, $cart_controller ), 'checkout' => new Routes\Checkout( $this->schemas->get( 'cart' ), $this->schemas->get( 'checkout' ), $cart_controller, $order_controller ), 'product-attributes' => new Routes\ProductAttributes( $this->schemas->get( 'product-attribute' ) ), 'product-attributes-by-id' => new Routes\ProductAttributesById( $this->schemas->get( 'product-attribute' ) ), 'product-attribute-terms' => new Routes\ProductAttributeTerms( $this->schemas->get( 'term' ) ), 'product-categories' => new Routes\ProductCategories( $this->schemas->get( 'product-category' ) ), 'product-categories-by-id' => new Routes\ProductCategoriesById( $this->schemas->get( 'product-category' ) ), 'product-collection-data' => new Routes\ProductCollectionData( $this->schemas->get( 'product-collection-data' ) ), 'product-reviews' => new Routes\ProductReviews( $this->schemas->get( 'product-review' ) ), 'product-tags' => new Routes\ProductTags( $this->schemas->get( 'term' ) ), 'products' => new Routes\Products( $this->schemas->get( 'product' ) ), 'products-by-id' => new Routes\ProductsById( $this->schemas->get( 'product' ) ), ]; // Batching requires WP 5.6. if ( version_compare( $wp_version, '5.6', '>=' ) ) { $this->routes['batch'] = new Routes\Batch(); } } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php 0000644 00000002441 15132754524 0022630 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use AutomateWoo\Exception; use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; /** * Class CartExtensionsSchema * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartExtensionsSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'cart-extensions'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart-extensions'; /** * Cart extensions schema properties. * * @return array */ public function get_properties() { return []; } /** * Handle the request and return a valid response for this endpoint. * * @param \WP_REST_Request $request Request containing data for the extension callback. * @throws RouteException When callback is not callable or parameters are incorrect. * * @return array */ public function get_item_response( $request = null ) { try { $callback = $this->extend->get_update_callback( $request['namespace'] ); } catch ( RouteException $e ) { throw $e; } if ( is_callable( $callback ) ) { $callback( $request['data'] ); } return rest_ensure_response( wc()->api->get_endpoint_data( '/wc/store/cart' ) ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php 0000644 00000007024 15132754524 0022547 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\RestApi\Routes; /** * BillingAddressSchema class. * * Provides a generic billing address schema for composition in other schemas. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class BillingAddressSchema extends AbstractAddressSchema { /** * The schema item name. * * @var string */ protected $title = 'billing_address'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'billing-address'; /** * Term properties. * * @return array */ public function get_properties() { $properties = parent::get_properties(); return array_merge( $properties, [ 'email' => [ 'description' => __( 'Email', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], ] ); } /** * Sanitize and format the given address object. * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return array */ public function sanitize_callback( $address, $request, $param ) { $address = parent::sanitize_callback( $address, $request, $param ); $address['email'] = wc_clean( wp_unslash( $address['email'] ) ); return $address; } /** * Validate the given address object. * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return true|\WP_Error */ public function validate_callback( $address, $request, $param ) { $errors = parent::validate_callback( $address, $request, $param ); $address = $this->sanitize_callback( $address, $request, $param ); $errors = is_wp_error( $errors ) ? $errors : new \WP_Error(); if ( ! empty( $address['email'] ) && ! is_email( $address['email'] ) ) { $errors->add( 'invalid_email', __( 'The provided email address is not valid', 'woocommerce' ) ); } return $errors->has_errors( $errors ) ? $errors : true; } /** * Convert a term object into an object suitable for the response. * * @param \WC_Order|\WC_Customer $address An object with billing address. * * @throws RouteException When the invalid object types are provided. * @return stdClass */ public function get_item_response( $address ) { if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) { return (object) $this->prepare_html_response( [ 'first_name' => $address->get_billing_first_name(), 'last_name' => $address->get_billing_last_name(), 'company' => $address->get_billing_company(), 'address_1' => $address->get_billing_address_1(), 'address_2' => $address->get_billing_address_2(), 'city' => $address->get_billing_city(), 'state' => $address->get_billing_state(), 'postcode' => $address->get_billing_postcode(), 'country' => $address->get_billing_country(), 'email' => $address->get_billing_email(), 'phone' => $address->get_billing_phone(), ] ); } throw new RouteException( 'invalid_object_type', sprintf( /* translators: Placeholders are class and method names */ __( '%1$s requires an instance of %2$s or %3$s for the address', 'woocommerce' ), 'BillingAddressSchema::get_item_response', 'WC_Customer', 'WC_Order' ), 500 ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php 0000644 00000014567 15132754524 0022475 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * ProductReviewSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductReviewSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'product_review'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'product-review'; /** * Image attachment schema instance. * * @var ImageAttachmentSchema */ protected $image_attachment_schema; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. * @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance. */ public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) { $this->image_attachment_schema = $image_attachment_schema; parent::__construct( $extend ); } /** * Product review schema properties. * * @return array */ public function get_properties() { $properties = [ 'id' => [ 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'date_created' => [ 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'formatted_date_created' => [ 'description' => __( "The date the review was created, in the site's timezone in human-readable format.", 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'date_created_gmt' => [ 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), 'type' => 'string', 'format' => 'date-time', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'product_id' => [ 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'product_name' => [ 'description' => __( 'Name of the product that the review belongs to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'product_permalink' => [ 'description' => __( 'Permalink of the product that the review belongs to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'product_image' => [ 'description' => __( 'Image of the product that the review belongs to.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => $this->image_attachment_schema->get_properties(), ], 'reviewer' => [ 'description' => __( 'Reviewer name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'review' => [ 'description' => __( 'The content of the review.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'sanitize_callback' => 'wp_filter_post_kses', ], 'readonly' => true, ], 'rating' => [ 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'verified' => [ 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ]; if ( get_option( 'show_avatars' ) ) { $avatar_properties = array(); $avatar_sizes = rest_get_avatar_sizes(); foreach ( $avatar_sizes as $size ) { $avatar_properties[ $size ] = array( /* translators: %d: avatar image size in pixels */ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ); } $properties['reviewer_avatar_urls'] = array( 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => $avatar_properties, ); } return $properties; } /** * Convert a WooCommerce product into an object suitable for the response. * * @param \WP_Comment $review Product review object. * @return array */ public function get_item_response( \WP_Comment $review ) { $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true ); $data = [ 'id' => (int) $review->comment_ID, 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), 'formatted_date_created' => get_comment_date( 'F j, Y', $review->comment_ID ), 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), 'product_id' => (int) $review->comment_post_ID, 'product_name' => get_the_title( (int) $review->comment_post_ID ), 'product_permalink' => get_permalink( (int) $review->comment_post_ID ), 'product_image' => $this->image_attachment_schema->get_item_response( get_post_thumbnail_id( (int) $review->comment_post_ID ) ), 'reviewer' => $review->comment_author, 'review' => $review->comment_content, 'rating' => $rating, 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), 'reviewer_avatar_urls' => rest_get_avatar_urls( $review->comment_author_email ), ]; if ( 'view' === $context ) { $data['review'] = wpautop( $data['review'] ); } return $data; } } packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php 0000644 00000004435 15132754524 0020573 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * TermSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class TermSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'term'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'term'; /** * Term properties. * * @return array */ public function get_properties() { return [ 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'String based identifier for the term.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Term description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'parent' => array( 'description' => __( 'Parent term ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'count' => array( 'description' => __( 'Number of objects (posts of any type) assigned to the term.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ]; } /** * Convert a term object into an object suitable for the response. * * @param \WP_Term $term Term object. * @return array */ public function get_item_response( $term ) { return [ 'id' => (int) $term->term_id, 'name' => $this->prepare_html_response( $term->name ), 'slug' => $term->slug, 'description' => $this->prepare_html_response( $term->description ), 'parent' => (int) $term->parent, 'count' => (int) $term->count, ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php 0000644 00000027033 15132754524 0021426 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * AbstractSchema class. * * For REST Route Schemas * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ abstract class AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'Schema'; /** * Rest extend instance * * @var ExtendRestApi */ protected $extend; /** * Extending key that gets added to endpoint. * * @var string */ const EXTENDING_KEY = 'extensions'; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. */ public function __construct( ExtendRestApi $extend ) { $this->extend = $extend; } /** * Returns the full item schema. * * @return array */ public function get_item_schema() { return array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->title, 'type' => 'object', 'properties' => $this->get_properties(), ); } /** * Recursive removal of arg_options. * * @param array $properties Schema properties. */ protected function remove_arg_options( $properties ) { return array_map( function( $property ) { if ( isset( $property['properties'] ) ) { $property['properties'] = $this->remove_arg_options( $property['properties'] ); } elseif ( isset( $property['items']['properties'] ) ) { $property['items']['properties'] = $this->remove_arg_options( $property['items']['properties'] ); } unset( $property['arg_options'] ); return $property; }, (array) $properties ); } /** * Returns the public schema. * * @return array */ public function get_public_item_schema() { $schema = $this->get_item_schema(); if ( isset( $schema['properties'] ) ) { $schema['properties'] = $this->remove_arg_options( $schema['properties'] ); } return $schema; } /** * Returns extended data for a specific endpoint. * * @param string $endpoint The endpoint identifer. * @param array ...$passed_args An array of arguments to be passed to callbacks. * @return object the data that will get added. */ protected function get_extended_data( $endpoint, ...$passed_args ) { return $this->extend->get_endpoint_data( $endpoint, $passed_args ); } /** * Gets an array of schema defaults recursively. * * @param array $properties Schema property data. * @return array Array of defaults, pulled from arg_options */ protected function get_recursive_schema_property_defaults( $properties ) { $defaults = []; foreach ( $properties as $property_key => $property_value ) { if ( isset( $property_value['arg_options']['default'] ) ) { $defaults[ $property_key ] = $property_value['arg_options']['default']; } elseif ( isset( $property_value['properties'] ) ) { $defaults[ $property_key ] = $this->get_recursive_schema_property_defaults( $property_value['properties'] ); } } return $defaults; } /** * Gets a function that validates recursively. * * @param array $properties Schema property data. * @return function Anonymous validation callback. */ protected function get_recursive_validate_callback( $properties ) { /** * Validate a request argument based on details registered to the route. * * @param mixed $values * @param \WP_REST_Request $request * @param string $param * @return true|\WP_Error */ return function ( $values, $request, $param ) use ( $properties ) { foreach ( $properties as $property_key => $property_value ) { $current_value = isset( $values[ $property_key ] ) ? $values[ $property_key ] : null; if ( isset( $property_value['arg_options']['validate_callback'] ) ) { $callback = $property_value['arg_options']['validate_callback']; $result = is_callable( $callback ) ? $callback( $current_value, $request, $param ) : false; } else { $result = rest_validate_value_from_schema( $current_value, $property_value, $param . ' > ' . $property_key ); } if ( ! $result || is_wp_error( $result ) ) { return $result; } if ( isset( $property_value['properties'] ) ) { $validate_callback = $this->get_recursive_validate_callback( $property_value['properties'] ); return $validate_callback( $current_value, $request, $param . ' > ' . $property_key ); } } return true; }; } /** * Gets a function that sanitizes recursively. * * @param array $properties Schema property data. * @return function Anonymous validation callback. */ protected function get_recursive_sanitize_callback( $properties ) { /** * Validate a request argument based on details registered to the route. * * @param mixed $values * @param \WP_REST_Request $request * @param string $param * @return true|\WP_Error */ return function ( $values, $request, $param ) use ( $properties ) { foreach ( $properties as $property_key => $property_value ) { $current_value = isset( $values[ $property_key ] ) ? $values[ $property_key ] : null; if ( isset( $property_value['arg_options']['sanitize_callback'] ) ) { $callback = $property_value['arg_options']['sanitize_callback']; $current_value = is_callable( $callback ) ? $callback( $current_value, $request, $param ) : $current_value; } else { $current_value = rest_sanitize_value_from_schema( $current_value, $property_value, $param . ' > ' . $property_key ); } if ( is_wp_error( $current_value ) ) { return $current_value; } if ( isset( $property_value['properties'] ) ) { $sanitize_callback = $this->get_recursive_sanitize_callback( $property_value['properties'] ); return $sanitize_callback( $current_value, $request, $param . ' > ' . $property_key ); } } return true; }; } /** * Returns extended schema for a specific endpoint. * * @param string $endpoint The endpoint identifer. * @param array ...$passed_args An array of arguments to be passed to callbacks. * @return array the data that will get added. */ protected function get_extended_schema( $endpoint, ...$passed_args ) { $extended_schema = $this->extend->get_endpoint_schema( $endpoint, $passed_args ); $defaults = $this->get_recursive_schema_property_defaults( $extended_schema ); return [ 'type' => 'object', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'default' => $defaults, 'validate_callback' => $this->get_recursive_validate_callback( $extended_schema ), 'sanitize_callback' => $this->get_recursive_sanitize_callback( $extended_schema ), ], 'properties' => $extended_schema, ]; } /** * Apply a schema get_item_response callback to an array of items and return the result. * * @param AbstractSchema $schema Schema class instance. * @param array $items Array of items. * @return array Array of values from the callback function. */ protected function get_item_responses_from_schema( AbstractSchema $schema, $items ) { $items = array_filter( $items ); if ( empty( $items ) ) { return []; } return array_values( array_map( [ $schema, 'get_item_response' ], $items ) ); } /** * Retrieves an array of endpoint arguments from the item schema for the controller. * * @uses rest_get_endpoint_args_for_schema() * @param string $method Optional. HTTP method of the request. * @return array Endpoint arguments. */ public function get_endpoint_args_for_item_schema( $method = \WP_REST_Server::CREATABLE ) { $schema = $this->get_item_schema(); $endpoint_args = rest_get_endpoint_args_for_schema( $schema, $method ); $endpoint_args = $this->remove_arg_options( $endpoint_args ); return $endpoint_args; } /** * Force all schema properties to be readonly. * * @param array $properties Schema. * @return array Updated schema. */ protected function force_schema_readonly( $properties ) { return array_map( function( $property ) { $property['readonly'] = true; if ( isset( $property['items']['properties'] ) ) { $property['items']['properties'] = $this->force_schema_readonly( $property['items']['properties'] ); } return $property; }, (array) $properties ); } /** * Returns consistent currency schema used across endpoints for prices. * * @return array */ protected function get_store_currency_properties() { return [ 'currency_code' => [ 'description' => __( 'Currency code (in ISO format) for returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'currency_symbol' => [ 'description' => __( 'Currency symbol for the currency which can be used to format returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'currency_minor_unit' => [ 'description' => __( 'Currency minor unit (number of digits after the decimal separator) for returned prices.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'currency_decimal_separator' => array( 'description' => __( 'Decimal separator for the currency which can be used to format returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'currency_thousand_separator' => array( 'description' => __( 'Thousand separator for the currency which can be used to format returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'currency_prefix' => array( 'description' => __( 'Price prefix for the currency which can be used to format returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'currency_suffix' => array( 'description' => __( 'Price prefix for the currency which can be used to format returned prices.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ]; } /** * Adds currency data to an array of monetary values. * * @param array $values Monetary amounts. * @return array Monetary amounts with currency data appended. */ protected function prepare_currency_response( $values ) { return $this->extend->get_formatter( 'currency' )->format( $values ); } /** * Convert monetary values from WooCommerce to string based integers, using * the smallest unit of a currency. * * @param string|float $amount Monetary amount with decimals. * @param int $decimals Number of decimals the amount is formatted with. * @param int $rounding_mode Defaults to the PHP_ROUND_HALF_UP constant. * @return string The new amount. */ protected function prepare_money_response( $amount, $decimals = 2, $rounding_mode = PHP_ROUND_HALF_UP ) { return $this->extend->get_formatter( 'money' )->format( $amount, [ 'decimals' => $decimals, 'rounding_mode' => $rounding_mode, ] ); } /** * Prepares HTML based content, such as post titles and content, for the API response. * * @param string|array $response Data to format. * @return string|array Formatted data. */ protected function prepare_html_response( $response ) { return $this->extend->get_formatter( 'html' )->format( $response ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php 0000644 00000004221 15132754524 0022744 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\RestApi\Routes; /** * ShippingAddressSchema class. * * Provides a generic shipping address schema for composition in other schemas. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ShippingAddressSchema extends AbstractAddressSchema { /** * The schema item name. * * @var string */ protected $title = 'shipping_address'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'shipping-address'; /** * Convert a term object into an object suitable for the response. * * @param \WC_Order|\WC_Customer $address An object with shipping address. * * @throws RouteException When the invalid object types are provided. * @return stdClass */ public function get_item_response( $address ) { if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) { if ( is_callable( [ $address, 'get_shipping_phone' ] ) ) { $shipping_phone = $address->get_shipping_phone(); } else { $shipping_phone = $address->get_meta( $address instanceof \WC_Customer ? 'shipping_phone' : '_shipping_phone', true ); } return (object) $this->prepare_html_response( [ 'first_name' => $address->get_shipping_first_name(), 'last_name' => $address->get_shipping_last_name(), 'company' => $address->get_shipping_company(), 'address_1' => $address->get_shipping_address_1(), 'address_2' => $address->get_shipping_address_2(), 'city' => $address->get_shipping_city(), 'state' => $address->get_shipping_state(), 'postcode' => $address->get_shipping_postcode(), 'country' => $address->get_shipping_country(), 'phone' => $shipping_phone, ] ); } throw new RouteException( 'invalid_object_type', sprintf( /* translators: Placeholders are class and method names */ __( '%1$s requires an instance of %2$s or %3$s for the address', 'woocommerce' ), 'ShippingAddressSchema::get_item_response', 'WC_Customer', 'WC_Order' ), 500 ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php 0000644 00000004221 15132754524 0022114 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * OrderCouponSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class OrderCouponSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'order_coupon'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'order-coupon'; /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'code' => [ 'description' => __( 'The coupons unique code.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'totals' => [ 'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'total_discount' => [ 'description' => __( 'Total discount applied by this coupon.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_discount_tax' => [ 'description' => __( 'Total tax removed due to discount applied by this coupon.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ] ), ], ]; } /** * Convert an order coupon to an object suitable for the response. * * @param \WC_Order_Item_Coupon $coupon Order coupon array. * @return array */ public function get_item_response( \WC_Order_Item_Coupon $coupon ) { return [ 'code' => $coupon->get_code(), 'totals' => (object) $this->prepare_currency_response( [ 'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ), 'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ), ] ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php 0000644 00000036701 15132754524 0020556 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; use WC_Tax; use WP_Error; /** * CartSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class CartSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'cart'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart'; /** * Item schema instance. * * @var CartItemSchema */ public $item_schema; /** * Coupon schema instance. * * @var CartCouponSchema */ public $coupon_schema; /** * Fee schema instance. * * @var CartFeeSchema */ public $fee_schema; /** * Shipping rates schema instance. * * @var CartShippingRateSchema */ public $shipping_rate_schema; /** * Shipping address schema instance. * * @var ShippingAddressSchema */ public $shipping_address_schema; /** * Billing address schema instance. * * @var BillingAddressSchema */ public $billing_address_schema; /** * Error schema instance. * * @var ErrorSchema */ public $error_schema; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. * @param CartItemSchema $item_schema Item schema instance. * @param CartCouponSchema $coupon_schema Coupon schema instance. * @param CartFeeSchema $fee_schema Fee schema instance. * @param CartShippingRateSchema $shipping_rate_schema Shipping rates schema instance. * @param ShippingAddressSchema $shipping_address_schema Shipping address schema instance. * @param BillingAddressSchema $billing_address_schema Billing address schema instance. * @param ErrorSchema $error_schema Error schema instance. */ public function __construct( ExtendRestApi $extend, CartItemSchema $item_schema, CartCouponSchema $coupon_schema, CartFeeSchema $fee_schema, CartShippingRateSchema $shipping_rate_schema, ShippingAddressSchema $shipping_address_schema, BillingAddressSchema $billing_address_schema, ErrorSchema $error_schema ) { $this->item_schema = $item_schema; $this->coupon_schema = $coupon_schema; $this->fee_schema = $fee_schema; $this->shipping_rate_schema = $shipping_rate_schema; $this->shipping_address_schema = $shipping_address_schema; $this->billing_address_schema = $billing_address_schema; $this->error_schema = $error_schema; parent::__construct( $extend ); } /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'coupons' => [ 'description' => __( 'List of applied cart coupons.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->force_schema_readonly( $this->coupon_schema->get_properties() ), ], ], 'shipping_rates' => [ 'description' => __( 'List of available shipping rates for the cart.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->force_schema_readonly( $this->shipping_rate_schema->get_properties() ), ], ], 'shipping_address' => [ 'description' => __( 'Current set shipping address for the customer.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => $this->force_schema_readonly( $this->shipping_address_schema->get_properties() ), ], 'billing_address' => [ 'description' => __( 'Current set billing address for the customer.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => $this->force_schema_readonly( $this->billing_address_schema->get_properties() ), ], 'items' => [ 'description' => __( 'List of cart items.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->force_schema_readonly( $this->item_schema->get_properties() ), ], ], 'items_count' => [ 'description' => __( 'Number of items in the cart.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'items_weight' => [ 'description' => __( 'Total weight (in grams) of all products in the cart.', 'woocommerce' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'needs_payment' => [ 'description' => __( 'True if the cart needs payment. False for carts with only free products and no shipping costs.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'needs_shipping' => [ 'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'has_calculated_shipping' => [ 'description' => __( 'True if the cart meets the criteria for showing shipping costs, and rates have been calculated and included in the totals.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'fees' => [ 'description' => __( 'List of cart fees.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->force_schema_readonly( $this->fee_schema->get_properties() ), ], ], 'totals' => [ 'description' => __( 'Cart total amounts provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'total_items' => [ 'description' => __( 'Total price of items in the cart.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_items_tax' => [ 'description' => __( 'Total tax on items in the cart.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_fees' => [ 'description' => __( 'Total price of any applied fees.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_fees_tax' => [ 'description' => __( 'Total tax on fees.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_discount' => [ 'description' => __( 'Total discount from applied coupons.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_discount_tax' => [ 'description' => __( 'Total tax removed due to discount from applied coupons.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_shipping' => [ 'description' => __( 'Total price of shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ), 'type' => [ 'string', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_shipping_tax' => [ 'description' => __( 'Total tax on shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ), 'type' => [ 'string', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_price' => [ 'description' => __( 'Total price the customer will pay.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_tax' => [ 'description' => __( 'Total tax applied to items and shipping.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'tax_lines' => [ 'description' => __( 'Lines of taxes applied to items and shipping.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'description' => __( 'The name of the tax.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'price' => [ 'description' => __( 'The amount of tax charged.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'rate' => [ 'description' => __( 'The rate at which tax is applied.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], ] ), ], 'errors' => [ 'description' => __( 'List of cart item errors, for example, items in the cart which are out of stock.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ), ], ], 'payment_requirements' => [ 'description' => __( 'List of required payment gateway features to process the order.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'generated_timestamp' => [ 'description' => __( 'The time at which this cart data was prepared', 'woocommerce' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), ]; } /** * Convert a woo cart into an object suitable for the response. * * @param \WC_Cart $cart Cart class instance. * @return array */ public function get_item_response( $cart ) { $controller = new CartController(); // Get cart errors first so if recalculations are performed, it's reflected in the response. $cart_errors = $this->get_cart_errors( $cart ); // The core cart class will not include shipping in the cart totals if `show_shipping()` returns false. This can // happen if an address is required, or through the use of hooks. This tracks if shipping has actually been // calculated so we can avoid returning costs and rates prematurely. $has_calculated_shipping = $cart->show_shipping(); // Get shipping packages to return in the response from the cart. $shipping_packages = $has_calculated_shipping ? $controller->get_shipping_packages() : []; return [ 'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ), 'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ), 'shipping_address' => $this->shipping_address_schema->get_item_response( wc()->customer ), 'billing_address' => $this->billing_address_schema->get_item_response( wc()->customer ), 'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ), 'items_count' => $cart->get_cart_contents_count(), 'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ), 'needs_payment' => $cart->needs_payment(), 'needs_shipping' => $cart->needs_shipping(), 'has_calculated_shipping' => $has_calculated_shipping, 'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ), 'totals' => (object) $this->prepare_currency_response( [ 'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ), 'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ), 'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ), 'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ), 'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ), 'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ), 'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null, 'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null, // Explicitly request context='edit'; default ('view') will render total as markup. 'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ), 'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ), 'tax_lines' => $this->get_tax_lines( $cart ), ] ), 'errors' => $cart_errors, 'payment_requirements' => $this->extend->get_payment_requirements(), 'generated_timestamp' => time(), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), ]; } /** * Get tax lines from the cart and format to match schema. * * @param \WC_Cart $cart Cart class instance. * @return array */ protected function get_tax_lines( $cart ) { $cart_tax_totals = $cart->get_tax_totals(); $tax_lines = []; foreach ( $cart_tax_totals as $cart_tax_total ) { $tax_lines[] = array( 'name' => $cart_tax_total->label, 'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ), 'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ), ); } return $tax_lines; } /** * Get cart validation errors. * * @param \WC_Cart $cart Cart class instance. * @return array */ protected function get_cart_errors( $cart ) { $controller = new CartController(); $item_errors = array_filter( $controller->get_cart_item_errors(), function ( WP_Error $error ) { return $error->has_errors(); } ); $coupon_errors = $controller->get_cart_coupon_errors(); return array_values( array_map( [ $this->error_schema, 'get_item_response' ], array_merge( $item_errors, $coupon_errors ) ) ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php 0000644 00000040464 15132754524 0021376 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; /** * CartItemSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class CartItemSchema extends ProductSchema { /** * The schema item name. * * @var string */ protected $title = 'cart_item'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart-item'; /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'key' => [ 'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'id' => [ 'description' => __( 'The cart item product or variation ID.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'quantity' => [ 'description' => __( 'Quantity of this item in the cart.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'quantity_limit' => [ 'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'short_description' => [ 'description' => __( 'Product short description in HTML format.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'description' => [ 'description' => __( 'Product full description in HTML format.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sku' => [ 'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'low_stock_remaining' => [ 'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ), 'type' => [ 'integer', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'backorders_allowed' => [ 'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ), 'type' => [ 'boolean' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'show_backorder_badge' => [ 'description' => __( 'True if the product is on backorder.', 'woocommerce' ), 'type' => [ 'boolean' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sold_individually' => [ 'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'permalink' => [ 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'images' => [ 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->image_attachment_schema->get_properties(), ], ], 'variation' => [ 'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'attribute' => [ 'description' => __( 'Variation attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'value' => [ 'description' => __( 'Variation attribute value.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'item_data' => [ 'description' => __( 'Metadata related to the cart item', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'description' => __( 'Name of the metadata.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'value' => [ 'description' => __( 'Value of the metadata.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'display' => [ 'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'prices' => [ 'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'price' => [ 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'regular_price' => [ 'description' => __( 'Regular product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sale_price' => [ 'description' => __( 'Sale product price, if applicable.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'price_range' => [ 'description' => __( 'Price range, if applicable.', 'woocommerce' ), 'type' => [ 'object', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'min_amount' => [ 'description' => __( 'Price amount.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'max_amount' => [ 'description' => __( 'Price amount.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], 'raw_prices' => [ 'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ), 'type' => [ 'object', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'precision' => [ 'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'price' => [ 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'regular_price' => [ 'description' => __( 'Regular product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sale_price' => [ 'description' => __( 'Sale product price, if applicable.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ] ), ], 'totals' => [ 'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'line_subtotal' => [ 'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'line_subtotal_tax' => [ 'description' => __( 'Line subtotal tax.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'line_total' => [ 'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'line_total_tax' => [ 'description' => __( 'Line total tax.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ] ), ], 'catalog_visibility' => [ 'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), ]; } /** * Convert a WooCommerce cart item to an object suitable for the response. * * @param array $cart_item Cart item array. * @return array */ public function get_item_response( $cart_item ) { $product = $cart_item['data']; return [ 'key' => $cart_item['key'], 'id' => $product->get_id(), 'quantity' => wc_stock_amount( $cart_item['quantity'] ), 'quantity_limit' => $this->get_product_quantity_limit( $product ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), 'sku' => $this->prepare_html_response( $product->get_sku() ), 'low_stock_remaining' => $this->get_low_stock_remaining( $product ), 'backorders_allowed' => (bool) $product->backorders_allowed(), 'show_backorder_badge' => (bool) $product->backorders_require_notification() && $product->is_on_backorder( $cart_item['quantity'] ), 'sold_individually' => $product->is_sold_individually(), 'permalink' => $product->get_permalink(), 'images' => $this->get_images( $product ), 'variation' => $this->format_variation_data( $cart_item['variation'], $product ), 'item_data' => $this->get_item_data( $cart_item ), 'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ), 'totals' => (object) $this->prepare_currency_response( [ 'line_subtotal' => $this->prepare_money_response( $cart_item['line_subtotal'], wc_get_price_decimals() ), 'line_subtotal_tax' => $this->prepare_money_response( $cart_item['line_subtotal_tax'], wc_get_price_decimals() ), 'line_total' => $this->prepare_money_response( $cart_item['line_total'], wc_get_price_decimals() ), 'line_total_tax' => $this->prepare_money_response( $cart_item['line_tax'], wc_get_price_decimals() ), ] ), 'catalog_visibility' => $product->get_catalog_visibility(), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER, $cart_item ), ]; } /** * Get an array of pricing data. * * @param \WC_Product $product Product instance. * @param string $tax_display_mode If returned prices are incl or excl of tax. * @return array */ protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) { $tax_display_mode = $this->get_tax_display_mode( $tax_display_mode ); $price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode ); $prices = parent::prepare_product_price_response( $product, $tax_display_mode ); // Add raw prices (prices with greater precision). $prices['raw_prices'] = [ 'precision' => wc_get_rounding_precision(), 'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ), 'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ), 'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ), ]; return $prices; } /** * Returns the remaining stock for a product if it has stock. * * This also factors in draft orders. * * @param \WC_Product $product Product instance. * @return integer|null */ protected function get_remaining_stock( \WC_Product $product ) { if ( is_null( $product->get_stock_quantity() ) ) { return null; } $draft_order = wc()->session->get( 'store_api_draft_order', 0 ); $reserve_stock = new ReserveStock(); $reserved_stock = $reserve_stock->get_reserved_stock( $product, $draft_order ); return $product->get_stock_quantity() - $reserved_stock; } /** * Format variation data, for example convert slugs such as attribute_pa_size to Size. * * @param array $variation_data Array of data from the cart. * @param \WC_Product $product Product data. * @return array */ protected function format_variation_data( $variation_data, $product ) { $return = []; if ( ! is_iterable( $variation_data ) ) { return $return; } foreach ( $variation_data as $key => $value ) { $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $key ) ) ); if ( taxonomy_exists( $taxonomy ) ) { // If this is a term slug, get the term's nice name. $term = get_term_by( 'slug', $value, $taxonomy ); if ( ! is_wp_error( $term ) && $term && $term->name ) { $value = $term->name; } $label = wc_attribute_label( $taxonomy ); } else { // If this is a custom option slug, get the options name. $value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product ); $label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product ); } $return[] = [ 'attribute' => $this->prepare_html_response( $label ), 'value' => $this->prepare_html_response( $value ), ]; } return $return; } /** * Format cart item data removing any HTML tag. * * @param array $cart_item Cart item array. * @return array */ protected function get_item_data( $cart_item ) { $item_data = apply_filters( 'woocommerce_get_item_data', array(), $cart_item ); return array_map( [ $this, 'format_item_data_element' ], $item_data ); } /** * Remove HTML tags from cart item data and set the `hidden` property to * `__experimental_woocommerce_blocks_hidden`. * * @param array $item_data_element Individual element of a cart item data. * @return array */ protected function format_item_data_element( $item_data_element ) { if ( array_key_exists( '__experimental_woocommerce_blocks_hidden', $item_data_element ) ) { $item_data_element['hidden'] = $item_data_element['__experimental_woocommerce_blocks_hidden']; } return array_map( 'wp_strip_all_tags', $item_data_element ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php 0000644 00000066750 15132754524 0021314 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * ProductSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ProductSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'product'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'product'; /** * Image attachment schema instance. * * @var ImageAttachmentSchema */ protected $image_attachment_schema; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. * @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance. */ public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) { $this->image_attachment_schema = $image_attachment_schema; parent::__construct( $extend ); } /** * Product schema properties. * * @return array */ public function get_properties() { return [ 'id' => [ 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'parent' => [ 'description' => __( 'ID of the parent product, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'type' => [ 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'variation' => [ 'description' => __( 'Product variation attributes, if applicable.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'permalink' => [ 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'short_description' => [ 'description' => __( 'Product short description in HTML format.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'description' => [ 'description' => __( 'Product full description in HTML format.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'on_sale' => [ 'description' => __( 'Is the product on sale?', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sku' => [ 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'prices' => [ 'description' => __( 'Price data provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'price' => [ 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'regular_price' => [ 'description' => __( 'Regular product price.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sale_price' => [ 'description' => __( 'Sale product price, if applicable.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'price_range' => [ 'description' => __( 'Price range, if applicable.', 'woocommerce' ), 'type' => [ 'object', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'min_amount' => [ 'description' => __( 'Price amount.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'max_amount' => [ 'description' => __( 'Price amount.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ] ), ], 'price_html' => array( 'description' => __( 'Price string formatted as HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'average_rating' => [ 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'review_count' => [ 'description' => __( 'Amount of reviews that the product has.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'images' => [ 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => $this->image_attachment_schema->get_properties(), ], ], 'categories' => [ 'description' => __( 'List of categories, if applicable.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'Category ID', 'woocommerce' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Category name', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'slug' => [ 'description' => __( 'Category slug', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'link' => [ 'description' => __( 'Category link', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'tags' => [ 'description' => __( 'List of tags, if applicable.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'Tag ID', 'woocommerce' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Tag name', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'slug' => [ 'description' => __( 'Tag slug', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'link' => [ 'description' => __( 'Tag link', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'attributes' => [ 'description' => __( 'List of attributes assigned to the product/variation that are visible or used for variations.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'The attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'taxonomy' => [ 'description' => __( 'The attribute taxonomy, or null if the attribute is not taxonomy based.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'has_variations' => [ 'description' => __( 'True if this attribute is used by product variations.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'terms' => [ 'description' => __( 'List of assigned attribute terms.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'The term ID, or 0 if the attribute is not a global attribute.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'The term name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'slug' => [ 'description' => __( 'The term slug.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'default' => [ 'description' => __( 'If this is a default attribute', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], ], ], ], 'variations' => [ 'description' => __( 'List of variation IDs, if applicable.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'attributes' => [ 'description' => __( 'List of variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'description' => __( 'The attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'value' => [ 'description' => __( 'The assigned attribute.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], ], ], ], 'has_options' => [ 'description' => __( 'Does the product have additional options before it can be added to the cart?', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'is_purchasable' => [ 'description' => __( 'Is the product purchasable?', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'is_in_stock' => [ 'description' => __( 'Is the product in stock?', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'is_on_backorder' => [ 'description' => __( 'Is the product stock backordered? This will also return false if backorder notifications are turned off.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'low_stock_remaining' => [ 'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ), 'type' => [ 'integer', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'sold_individually' => [ 'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'quantity_limit' => [ 'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'add_to_cart' => [ 'description' => __( 'Add to cart button parameters.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'text' => [ 'description' => __( 'Button text.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'description' => [ 'description' => __( 'Button description.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'url' => [ 'description' => __( 'Add to cart URL.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ]; } /** * Convert a WooCommerce product into an object suitable for the response. * * @param \WC_Product $product Product instance. * @return array */ public function get_item_response( $product ) { return [ 'id' => $product->get_id(), 'name' => $this->prepare_html_response( $product->get_title() ), 'parent' => $product->get_parent_id(), 'type' => $product->get_type(), 'variation' => $this->prepare_html_response( $product->is_type( 'variation' ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ), 'permalink' => $product->get_permalink(), 'sku' => $this->prepare_html_response( $product->get_sku() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), 'on_sale' => $product->is_on_sale(), 'prices' => (object) $this->prepare_product_price_response( $product ), 'price_html' => $this->prepare_html_response( $product->get_price_html() ), 'average_rating' => (string) $product->get_average_rating(), 'review_count' => $product->get_review_count(), 'images' => $this->get_images( $product ), 'categories' => $this->get_term_list( $product, 'product_cat' ), 'tags' => $this->get_term_list( $product, 'product_tag' ), 'attributes' => $this->get_attributes( $product ), 'variations' => $this->get_variations( $product ), 'has_options' => $product->has_options(), 'is_purchasable' => $product->is_purchasable(), 'is_in_stock' => $product->is_in_stock(), 'is_on_backorder' => 'onbackorder' === $product->get_stock_status(), 'low_stock_remaining' => $this->get_low_stock_remaining( $product ), 'sold_individually' => $product->is_sold_individually(), 'quantity_limit' => $this->get_product_quantity_limit( $product ), 'add_to_cart' => (object) $this->prepare_html_response( [ 'text' => $product->add_to_cart_text(), 'description' => $product->add_to_cart_description(), 'url' => $product->add_to_cart_url(), ] ), ]; } /** * Get list of product images. * * @param \WC_Product $product Product instance. * @return array */ protected function get_images( \WC_Product $product ) { $attachment_ids = array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() ); return array_filter( array_map( [ $this->image_attachment_schema, 'get_item_response' ], $attachment_ids ) ); } /** * Gets remaining stock amount for a product. * * @param \WC_Product $product Product instance. * @return integer|null */ protected function get_remaining_stock( \WC_Product $product ) { if ( is_null( $product->get_stock_quantity() ) ) { return null; } return $product->get_stock_quantity(); } /** * If a product has low stock, return the remaining stock amount for display. * * @param \WC_Product $product Product instance. * @return integer|null */ protected function get_low_stock_remaining( \WC_Product $product ) { $remaining_stock = $this->get_remaining_stock( $product ); if ( ! is_null( $remaining_stock ) && $remaining_stock <= wc_get_low_stock_amount( $product ) ) { return max( $remaining_stock, 0 ); } return null; } /** * Get the quantity limit for an item in the cart. * * @param \WC_Product $product Product instance. * @return int */ protected function get_product_quantity_limit( \WC_Product $product ) { $limits = [ 99 ]; if ( $product->is_sold_individually() ) { $limits[] = 1; } elseif ( ! $product->backorders_allowed() ) { $limits[] = $this->get_remaining_stock( $product ); } return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product ); } /** * Returns true if the given attribute is valid. * * @param mixed $attribute Object or variable to check. * @return boolean */ protected function filter_valid_attribute( $attribute ) { return is_a( $attribute, '\WC_Product_Attribute' ); } /** * Returns true if the given attribute is valid and used for variations. * * @param mixed $attribute Object or variable to check. * @return boolean */ protected function filter_variation_attribute( $attribute ) { return $this->filter_valid_attribute( $attribute ) && $attribute->get_variation(); } /** * Get variation IDs and attributes from the DB. * * @param \WC_Product $product Product instance. * @returns array */ protected function get_variations( \WC_Product $product ) { if ( ! $product->is_type( 'variable' ) ) { return []; } global $wpdb; $variation_ids = $product->get_visible_children(); if ( ! count( $variation_ids ) ) { return []; } $attributes = array_filter( $product->get_attributes(), [ $this, 'filter_variation_attribute' ] ); $default_variation_meta_data = array_reduce( $attributes, function( $defaults, $attribute ) use ( $product ) { $meta_key = wc_variation_attribute_name( $attribute->get_name() ); $defaults[ $meta_key ] = [ 'name' => wc_attribute_label( $attribute->get_name(), $product ), 'value' => null, ]; return $defaults; }, [] ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $variation_meta_data = $wpdb->get_results( " SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $variation_ids ) ) . ") AND meta_key IN ('" . implode( "','", array_map( 'esc_sql', array_keys( $default_variation_meta_data ) ) ) . "') " ); // phpcs:enable $attributes_by_variation = array_reduce( $variation_meta_data, function( $values, $data ) { $values[ $data->variation_id ][ $data->attribute_key ] = $data->attribute_value; return $values; }, array_fill_keys( $variation_ids, [] ) ); $variations = []; foreach ( $variation_ids as $variation_id ) { $attribute_data = $default_variation_meta_data; foreach ( $attributes_by_variation[ $variation_id ] as $meta_key => $meta_value ) { if ( '' !== $meta_value ) { $attribute_data[ $meta_key ]['value'] = $meta_value; } } $variations[] = (object) [ 'id' => $variation_id, 'attributes' => array_values( $attribute_data ), ]; } return $variations; } /** * Get list of product attributes and attribute terms. * * @param \WC_Product $product Product instance. * @return array */ protected function get_attributes( \WC_Product $product ) { $attributes = array_filter( $product->get_attributes(), [ $this, 'filter_valid_attribute' ] ); $default_attributes = $product->get_default_attributes(); $return = []; foreach ( $attributes as $attribute_slug => $attribute ) { // Only visible and variation attributes will be exposed by this API. if ( ! $attribute->get_visible() || ! $attribute->get_variation() ) { continue; } $terms = $attribute->is_taxonomy() ? array_map( [ $this, 'prepare_product_attribute_taxonomy_value' ], $attribute->get_terms() ) : array_map( [ $this, 'prepare_product_attribute_value' ], $attribute->get_options() ); // Custom attribute names are sanitized to be the array keys. // So when we do the array_key_exists check below we also need to sanitize the attribute names. $sanitized_attribute_name = sanitize_key( $attribute->get_name() ); if ( array_key_exists( $sanitized_attribute_name, $default_attributes ) ) { foreach ( $terms as $term ) { $term->default = $term->slug === $default_attributes[ $sanitized_attribute_name ]; } } $return[] = (object) [ 'id' => $attribute->get_id(), 'name' => wc_attribute_label( $attribute->get_name(), $product ), 'taxonomy' => $attribute->is_taxonomy() ? $attribute->get_name() : null, 'has_variations' => true === $attribute->get_variation(), 'terms' => $terms, ]; } return $return; } /** * Prepare an attribute term for the response. * * @param \WP_Term $term Term object. * @return object */ protected function prepare_product_attribute_taxonomy_value( \WP_Term $term ) { return $this->prepare_product_attribute_value( $term->name, $term->term_id, $term->slug ); } /** * Prepare an attribute term for the response. * * @param string $name Attribute term name. * @param int $id Attribute term ID. * @param string $slug Attribute term slug. * @return object */ protected function prepare_product_attribute_value( $name, $id = 0, $slug = '' ) { return (object) [ 'id' => (int) $id, 'name' => $name, 'slug' => $slug ? $slug : $name, ]; } /** * Get an array of pricing data. * * @param \WC_Product $product Product instance. * @param string $tax_display_mode If returned prices are incl or excl of tax. * @return array */ protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) { $prices = []; $tax_display_mode = $this->get_tax_display_mode( $tax_display_mode ); $price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode ); // If we have a variable product, get the price from the variations (this will use the min value). if ( $product->is_type( 'variable' ) ) { $regular_price = $product->get_variation_regular_price(); $sale_price = $product->get_variation_sale_price(); } else { $regular_price = $product->get_regular_price(); $sale_price = $product->get_sale_price(); } $prices['price'] = $this->prepare_money_response( $price_function( $product ), wc_get_price_decimals() ); $prices['regular_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $regular_price ] ), wc_get_price_decimals() ); $prices['sale_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $sale_price ] ), wc_get_price_decimals() ); $prices['price_range'] = $this->get_price_range( $product, $tax_display_mode ); return $this->prepare_currency_response( $prices ); } /** * WooCommerce can return prices including or excluding tax; choose the correct method based on tax display mode. * * @param string $tax_display_mode Provided tax display mode. * @return string Valid tax display mode. */ protected function get_tax_display_mode( $tax_display_mode = '' ) { return in_array( $tax_display_mode, [ 'incl', 'excl' ], true ) ? $tax_display_mode : get_option( 'woocommerce_tax_display_shop' ); } /** * WooCommerce can return prices including or excluding tax; choose the correct method based on tax display mode. * * @param string $tax_display_mode If returned prices are incl or excl of tax. * @return string Function name. */ protected function get_price_function_from_tax_display_mode( $tax_display_mode ) { return 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax'; } /** * Get price range from certain product types. * * @param \WC_Product $product Product instance. * @param string $tax_display_mode If returned prices are incl or excl of tax. * @return object|null */ protected function get_price_range( \WC_Product $product, $tax_display_mode = '' ) { $tax_display_mode = $this->get_tax_display_mode( $tax_display_mode ); if ( $product->is_type( 'variable' ) ) { $prices = $product->get_variation_prices( true ); if ( ! empty( $prices['price'] ) && ( min( $prices['price'] ) !== max( $prices['price'] ) ) ) { return (object) [ 'min_amount' => $this->prepare_money_response( min( $prices['price'] ), wc_get_price_decimals() ), 'max_amount' => $this->prepare_money_response( max( $prices['price'] ), wc_get_price_decimals() ), ]; } } if ( $product->is_type( 'grouped' ) ) { $children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' ); $price_function = 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax'; foreach ( $children as $child ) { if ( '' !== $child->get_price() ) { $child_prices[] = $price_function( $child ); } } if ( ! empty( $child_prices ) ) { return (object) [ 'min_amount' => $this->prepare_money_response( min( $child_prices ), wc_get_price_decimals() ), 'max_amount' => $this->prepare_money_response( max( $child_prices ), wc_get_price_decimals() ), ]; } } return null; } /** * Returns a list of terms assigned to the product. * * @param \WC_Product $product Product object. * @param string $taxonomy Taxonomy name. * @return array Array of terms (id, name, slug). */ protected function get_term_list( \WC_Product $product, $taxonomy = '' ) { if ( ! $taxonomy ) { return []; } $terms = get_the_terms( $product->get_id(), $taxonomy ); if ( ! $terms || is_wp_error( $terms ) ) { return []; } $return = []; $default_category = (int) get_option( 'default_product_cat', 0 ); foreach ( $terms as $term ) { $link = get_term_link( $term, $taxonomy ); if ( is_wp_error( $link ) ) { continue; } if ( $term->term_id === $default_category ) { continue; } $return[] = (object) [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'link' => $link, ]; } return $return; } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php 0000644 00000006600 15132754524 0021735 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController; /** * CartCouponSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 * @since 3.9.0 Coupon type (`discount_type`) added. */ class CartCouponSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'cart_coupon'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart-coupon'; /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'code' => [ 'description' => __( 'The coupon\'s unique code.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'sanitize_callback' => 'wc_format_coupon_code', 'validate_callback' => [ $this, 'coupon_exists' ], ], ], 'discount_type' => [ 'description' => __( 'The discount type for the coupon (e.g. percentage or fixed amount)', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'arg_options' => [ 'validate_callback' => [ $this, 'coupon_exists' ], ], ], 'totals' => [ 'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'total_discount' => [ 'description' => __( 'Total discount applied by this coupon.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_discount_tax' => [ 'description' => __( 'Total tax removed due to discount applied by this coupon.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ] ), ], ]; } /** * Check given coupon exists. * * @param string $coupon_code Coupon code. * @return bool */ public function coupon_exists( $coupon_code ) { $coupon = new \WC_Coupon( $coupon_code ); return (bool) $coupon->get_id() || $coupon->get_virtual(); } /** * Generate a response from passed coupon code. * * @param string $coupon_code Coupon code from the cart. * @return array */ public function get_item_response( $coupon_code ) { $controller = new CartController(); $cart = $controller->get_cart_instance(); $total_discounts = $cart->get_coupon_discount_totals(); $total_discount_taxes = $cart->get_coupon_discount_tax_totals(); $coupon = new \WC_Coupon( $coupon_code ); return [ 'code' => $coupon_code, 'discount_type' => $coupon->get_discount_type(), 'totals' => (object) $this->prepare_currency_response( [ 'total_discount' => $this->prepare_money_response( isset( $total_discounts[ $coupon_code ] ) ? $total_discounts[ $coupon_code ] : 0, wc_get_price_decimals() ), 'total_discount_tax' => $this->prepare_money_response( isset( $total_discount_taxes[ $coupon_code ] ) ? $total_discount_taxes[ $coupon_code ] : 0, wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ), ] ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php 0000644 00000014645 15132754524 0022741 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\RestApi\Routes; /** * AddressSchema class. * * Provides a generic address schema for composition in other schemas. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 4.1.0 */ abstract class AbstractAddressSchema extends AbstractSchema { /** * Term properties. * * @internal Note that required properties don't require values, just that they are included in the request. * @return array */ public function get_properties() { return [ 'first_name' => [ 'description' => __( 'First name', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'last_name' => [ 'description' => __( 'Last name', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'company' => [ 'description' => __( 'Company', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'address_1' => [ 'description' => __( 'Address', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'address_2' => [ 'description' => __( 'Apartment, suite, etc.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'city' => [ 'description' => __( 'City', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'state' => [ 'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'postcode' => [ 'description' => __( 'Postal code', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'country' => [ 'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], 'phone' => [ 'description' => __( 'Phone', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => true, ], ]; } /** * Sanitize and format the given address object. * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return array */ public function sanitize_callback( $address, $request, $param ) { $address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address ); $address['country'] = wc_strtoupper( wc_clean( wp_unslash( $address['country'] ) ) ); $address['first_name'] = wc_clean( wp_unslash( $address['first_name'] ) ); $address['last_name'] = wc_clean( wp_unslash( $address['last_name'] ) ); $address['company'] = wc_clean( wp_unslash( $address['company'] ) ); $address['address_1'] = wc_clean( wp_unslash( $address['address_1'] ) ); $address['address_2'] = wc_clean( wp_unslash( $address['address_2'] ) ); $address['city'] = wc_clean( wp_unslash( $address['city'] ) ); $address['state'] = $this->format_state( wc_clean( wp_unslash( $address['state'] ) ), $address['country'] ); $address['postcode'] = $address['postcode'] ? wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; $address['phone'] = wc_clean( wp_unslash( $address['phone'] ) ); return $address; } /** * Format a state based on the country. If country has defined states, will return an upper case state code. * * @param string $state State name or code (sanitized). * @param string $country Country code. * @return string */ protected function format_state( $state, $country ) { $states = $country ? array_filter( (array) wc()->countries->get_states( $country ) ) : []; if ( count( $states ) ) { $state = wc_strtoupper( $state ); $state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $states ) ) ); if ( isset( $state_values[ $state ] ) ) { // Convert to state code if a state name was provided. return $state_values[ $state ]; } } return $state; } /** * Validate the given address object. * * @see rest_validate_value_from_schema * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return true|\WP_Error */ public function validate_callback( $address, $request, $param ) { $errors = new \WP_Error(); $address = $this->sanitize_callback( $address, $request, $param ); if ( empty( $address['country'] ) ) { $errors->add( 'missing_country', __( 'Country is required', 'woocommerce' ) ); return $errors; } if ( ! in_array( $address['country'], array_keys( wc()->countries->get_countries() ), true ) ) { $errors->add( 'invalid_country', sprintf( /* translators: %s valid country codes */ __( 'Invalid country code provided. Must be one of: %s', 'woocommerce' ), implode( ', ', array_keys( wc()->countries->get_countries() ) ) ) ); return $errors; } $states = array_filter( array_keys( (array) wc()->countries->get_states( $address['country'] ) ) ); if ( ! empty( $address['state'] ) && count( $states ) && ! in_array( $address['state'], $states, true ) ) { $errors->add( 'invalid_state', sprintf( /* translators: %s valid states */ __( 'The provided state is not valid. Must be one of: %s', 'woocommerce' ), implode( ', ', $states ) ) ); } if ( ! empty( $address['postcode'] ) && ! \WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) { $errors->add( 'invalid_postcode', __( 'The provided postcode / ZIP is not valid', 'woocommerce' ) ); } if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) { $errors->add( 'invalid_phone', __( 'The provided phone number is not valid', 'woocommerce' ) ); } return $errors->has_errors( $errors ) ? $errors : true; } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php 0000644 00000004453 15132754524 0021175 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * CartFeeSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartFeeSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'cart_fee'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart-fee'; /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'id' => [ 'description' => __( 'Unique identifier for the fee within the cart.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Fee name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'totals' => [ 'description' => __( 'Fee total amounts provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'total' => [ 'description' => __( 'Total amount for this fee.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'total_tax' => [ 'description' => __( 'Total tax amount for this fee.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ] ), ], ]; } /** * Convert a WooCommerce cart fee to an object suitable for the response. * * @param array $fee Cart fee data. * @return array */ public function get_item_response( $fee ) { return [ 'key' => $fee->id, 'name' => $this->prepare_html_response( $fee->name ), 'totals' => (object) $this->prepare_currency_response( [ 'total' => $this->prepare_money_response( $fee->total, wc_get_price_decimals() ), 'total_tax' => $this->prepare_money_response( $fee->tax, wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ), ] ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php 0000644 00000010471 15132754524 0024107 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * ProductCollectionDataSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ProductCollectionDataSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'product-collection-data'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'product-collection-data'; /** * Product collection data schema properties. * * @return array */ public function get_properties() { return [ 'price_range' => [ 'description' => __( 'Min and max prices found in collection of products, provided using the smallest unit of the currency.', 'woocommerce' ), 'type' => [ 'object', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => array_merge( $this->get_store_currency_properties(), [ 'min_price' => [ 'description' => __( 'Min price found in collection of products.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'max_price' => [ 'description' => __( 'Max price found in collection of products.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ] ), ], 'attribute_counts' => [ 'description' => __( 'Returns number of products within attribute terms.', 'woocommerce' ), 'type' => [ 'array', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'term' => [ 'description' => __( 'Term ID', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'count' => [ 'description' => __( 'Number of products.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'rating_counts' => [ 'description' => __( 'Returns number of products with each average rating.', 'woocommerce' ), 'type' => [ 'array', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'rating' => [ 'description' => __( 'Average rating', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'count' => [ 'description' => __( 'Number of products.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'stock_status_counts' => [ 'description' => __( 'Returns number of products with each stock status.', 'woocommerce' ), 'type' => [ 'array', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'status' => [ 'description' => __( 'Status', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'count' => [ 'description' => __( 'Number of products.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], ]; } /** * Format data. * * @param array $data Collection data to format and return. * @return array */ public function get_item_response( $data ) { return [ 'price_range' => ! is_null( $data['min_price'] ) && ! is_null( $data['max_price'] ) ? (object) $this->prepare_currency_response( [ 'min_price' => $this->prepare_money_response( $data['min_price'], wc_get_price_decimals() ), 'max_price' => $this->prepare_money_response( $data['max_price'], wc_get_price_decimals() ), ] ) : null, 'attribute_counts' => $data['attribute_counts'], 'rating_counts' => $data['rating_counts'], 'stock_status_counts' => $data['stock_status_counts'], ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php 0000644 00000002411 15132754524 0020745 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * ErrorSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ErrorSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'error'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'error'; /** * Product schema properties. * * @return array */ public function get_properties() { return [ 'code' => [ 'description' => __( 'Error code', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'message' => [ 'description' => __( 'Error message', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ]; } /** * Convert a WP_Error into an object suitable for the response. * * @param \WP_Error $error Error object. * @return array */ public function get_item_response( \WP_Error $error ) { return [ 'code' => $this->prepare_html_response( $error->get_error_code() ), 'message' => $this->prepare_html_response( $error->get_error_message() ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php 0000644 00000005234 15132754524 0022715 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * ImageAttachmentSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ImageAttachmentSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'image'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'image'; /** * Product schema properties. * * @return array */ public function get_properties() { return [ 'id' => [ 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], ], 'src' => [ 'description' => __( 'Full size image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => [ 'view', 'edit' ], ], 'thumbnail' => [ 'description' => __( 'Thumbnail URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => [ 'view', 'edit' ], ], 'srcset' => [ 'description' => __( 'Thumbnail srcset for responsive images.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'sizes' => [ 'description' => __( 'Thumbnail sizes for responsive images.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'name' => [ 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'alt' => [ 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], ]; } /** * Convert a WooCommerce product into an object suitable for the response. * * @param int $attachment_id Image attachment ID. * @return array|null */ public function get_item_response( $attachment_id ) { if ( ! $attachment_id ) { return null; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { return []; } $thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' ); return [ 'id' => (int) $attachment_id, 'src' => current( $attachment ), 'thumbnail' => current( $thumbnail ), 'srcset' => (string) wp_get_attachment_image_srcset( $attachment_id, 'full' ), 'sizes' => (string) wp_get_attachment_image_sizes( $attachment_id, 'full' ), 'name' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php 0000644 00000026476 15132754524 0023104 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use WC_Shipping_Rate as ShippingRate; /** * CartShippingRateSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CartShippingRateSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'cart-shipping-rate'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'cart-shipping-rate'; /** * Cart schema properties. * * @return array */ public function get_properties() { return [ 'package_id' => [ 'description' => __( 'The ID of the package the shipping rates belong to.', 'woocommerce' ), 'type' => [ 'integer', 'string' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Name of the package.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'destination' => [ 'description' => __( 'Shipping destination address.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'address_1' => [ 'description' => __( 'First line of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'address_2' => [ 'description' => __( 'Second line of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'city' => [ 'description' => __( 'City of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'state' => [ 'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'postcode' => [ 'description' => __( 'Zip or Postcode of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'country' => [ 'description' => __( 'ISO code for the country of the address being shipped to.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], 'items' => [ 'description' => __( 'List of cart items the returned shipping rates apply to.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'key' => [ 'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Name of the item.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'quantity' => [ 'description' => __( 'Quantity of the item in the current package.', 'woocommerce' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'shipping_rates' => [ 'description' => __( 'List of shipping rates.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => $this->get_rate_properties(), ], ], ]; } /** * Schema for a single rate. * * @return array */ protected function get_rate_properties() { return array_merge( [ 'rate_id' => [ 'description' => __( 'ID of the shipping rate.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'name' => [ 'description' => __( 'Name of the shipping rate, e.g. Express shipping.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'description' => [ 'description' => __( 'Description of the shipping rate, e.g. Dispatched via USPS.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'delivery_time' => [ 'description' => __( 'Delivery time estimate text, e.g. 3-5 business days.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'price' => [ 'description' => __( 'Price of this shipping rate using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'taxes' => [ 'description' => __( 'Taxes applied to this shipping rate using the smallest unit of the currency.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'method_id' => [ 'description' => __( 'ID of the shipping method that provided the rate.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'instance_id' => [ 'description' => __( 'Instance ID of the shipping method that provided the rate.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'meta_data' => [ 'description' => __( 'Meta data attached to the shipping rate.', 'woocommerce' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'key' => [ 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'value' => [ 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], ], ], 'selected' => [ 'description' => __( 'True if this is the rate currently selected by the customer for the cart.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ], $this->get_store_currency_properties() ); } /** * Convert a shipping rate from WooCommerce into a valid response. * * @param array $package Shipping package complete with rates from WooCommerce. * @return array */ public function get_item_response( $package ) { return [ 'package_id' => $package['package_id'], 'name' => $package['package_name'], 'destination' => $this->prepare_package_destination_response( $package ), 'items' => $this->prepare_package_items_response( $package ), 'shipping_rates' => $this->prepare_package_shipping_rates_response( $package ), ]; } /** * Gets and formats the destination address of a package. * * @param array $package Shipping package complete with rates from WooCommerce. * @return object */ protected function prepare_package_destination_response( $package ) { return (object) $this->prepare_html_response( [ 'address_1' => $package['destination']['address_1'], 'address_2' => $package['destination']['address_2'], 'city' => $package['destination']['city'], 'state' => $package['destination']['state'], 'postcode' => $package['destination']['postcode'], 'country' => $package['destination']['country'], ] ); } /** * Gets items from a package and creates an array of strings containing product names and quantities. * * @param array $package Shipping package complete with rates from WooCommerce. * @return array */ protected function prepare_package_items_response( $package ) { $items = array(); foreach ( $package['contents'] as $item_id => $values ) { $items[] = [ 'key' => $item_id, 'name' => $values['data']->get_name(), 'quantity' => $values['quantity'], ]; } return $items; } /** * Prepare an array of rates from a package for the response. * * @param array $package Shipping package complete with rates from WooCommerce. * @return array */ protected function prepare_package_shipping_rates_response( $package ) { $rates = $package['rates']; $selected_rates = wc()->session->get( 'chosen_shipping_methods', array() ); $selected_rate = isset( $selected_rates[ $package['package_id'] ] ) ? $selected_rates[ $package['package_id'] ] : ''; if ( empty( $selected_rate ) && ! empty( $package['rates'] ) ) { $selected_rate = wc_get_chosen_shipping_method_for_package( $package['package_id'], $package ); } $response = []; foreach ( $package['rates'] as $rate ) { $response[] = $this->get_rate_response( $rate, $selected_rate ); } return $response; } /** * Response for a single rate. * * @param WC_Shipping_Rate $rate Rate object. * @param string $selected_rate Selected rate. * @return array */ protected function get_rate_response( $rate, $selected_rate = '' ) { return $this->prepare_currency_response( [ 'rate_id' => $this->get_rate_prop( $rate, 'id' ), 'name' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'label' ) ), 'description' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'description' ) ), 'delivery_time' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'delivery_time' ) ), 'price' => $this->prepare_money_response( $this->get_rate_prop( $rate, 'cost' ), wc_get_price_decimals() ), 'taxes' => $this->prepare_money_response( array_sum( $this->get_rate_prop( $rate, 'taxes' ) ), wc_get_price_decimals() ), 'instance_id' => $this->get_rate_prop( $rate, 'instance_id' ), 'method_id' => $this->get_rate_prop( $rate, 'method_id' ), 'meta_data' => $this->get_rate_meta_data( $rate ), 'selected' => $selected_rate === $this->get_rate_prop( $rate, 'id' ), ] ); } /** * Gets a prop of the rate object, if callable. * * @param WC_Shipping_Rate $rate Rate object. * @param string $prop Prop name. * @return string */ protected function get_rate_prop( $rate, $prop ) { $getter = 'get_' . $prop; return \is_callable( array( $rate, $getter ) ) ? $rate->$getter() : ''; } /** * Converts rate meta data into a suitable response object. * * @param WC_Shipping_Rate $rate Rate object. * @return array */ protected function get_rate_meta_data( $rate ) { $meta_data = $rate->get_meta_data(); return array_reduce( array_keys( $meta_data ), function( $return, $key ) use ( $meta_data ) { $return[] = [ 'key' => $key, 'value' => $meta_data[ $key ], ]; return $return; }, [] ); } } packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php 0000644 00000007302 15132754524 0022776 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * ProductCategorySchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class ProductCategorySchema extends TermSchema { /** * The schema item name. * * @var string */ protected $title = 'product-category'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'product-category'; /** * Image attachment schema instance. * * @var ImageAttachmentSchema */ protected $image_attachment_schema; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. * @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance. */ public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) { $this->image_attachment_schema = $image_attachment_schema; parent::__construct( $extend ); } /** * Term properties. * * @return array */ public function get_properties() { $schema = parent::get_properties(); $schema['image'] = [ 'description' => __( 'Category image.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit', 'embed' ], 'readonly' => true, 'properties' => $this->image_attachment_schema->get_properties(), ]; $schema['review_count'] = [ 'description' => __( 'Number of reviews for products in this category.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ]; $schema['permalink'] = [ 'description' => __( 'Category URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => [ 'view', 'edit', 'embed' ], 'readonly' => true, ]; return $schema; } /** * Convert a term object into an object suitable for the response. * * @param \WP_Term $term Term object. * @return array */ public function get_item_response( $term ) { $response = parent::get_item_response( $term ); $count = get_term_meta( $term->term_id, 'product_count_product_cat', true ); if ( $count ) { $response['count'] = (int) $count; } $response['image'] = $this->image_attachment_schema->get_item_response( get_term_meta( $term->term_id, 'thumbnail_id', true ) ); $response['review_count'] = $this->get_category_review_count( $term ); $response['permalink'] = get_term_link( $term->term_id, 'product_cat' ); return $response; } /** * Get total number of reviews for products in a category. * * @param \WP_Term $term Term object. * @return int */ protected function get_category_review_count( $term ) { global $wpdb; $children = get_term_children( $term->term_id, 'product_cat' ); if ( ! $children || is_wp_error( $children ) ) { $terms_to_count_str = absint( $term->term_id ); } else { $terms_to_count = array_unique( array_map( 'absint', array_merge( array( $term->term_id ), $children ) ) ); $terms_to_count_str = implode( ',', $terms_to_count ); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $products_of_category_sql = $wpdb->prepare( "SELECT SUM(comment_count) as review_count FROM {$wpdb->posts} AS posts INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id WHERE term_relationships.term_taxonomy_id IN ({$terms_to_count_str})" ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $review_count = $wpdb->get_var( $products_of_category_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return (int) $review_count; } } packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php 0000644 00000005173 15132754524 0023170 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; /** * ProductAttributeSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. * @since 2.5.0 */ class ProductAttributeSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'product_attribute'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'product-attribute'; /** * Term properties. * * @return array */ public function get_properties() { return [ 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxonomy' => array( 'description' => __( 'The attribute taxonomy name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Attribute type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'order' => array( 'description' => __( 'How terms in this attribute are sorted by default.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'has_archives' => array( 'description' => __( 'If this attribute has term archive pages.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'count' => array( 'description' => __( 'Number of terms in the attribute taxonomy.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ]; } /** * Convert an attribute object into an object suitable for the response. * * @param object $attribute Attribute object. * @return array */ public function get_item_response( $attribute ) { return [ 'id' => (int) $attribute->id, 'name' => $this->prepare_html_response( $attribute->name ), 'taxonomy' => $attribute->slug, 'type' => $attribute->type, 'order' => $attribute->order_by, 'has_archives' => $attribute->has_archives, 'count' => (int) \wp_count_terms( $attribute->slug ), ]; } } packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php 0000644 00000016331 15132754524 0021427 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Payments\PaymentResult; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; /** * CheckoutSchema class. * * @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions. */ class CheckoutSchema extends AbstractSchema { /** * The schema item name. * * @var string */ protected $title = 'checkout'; /** * The schema item identifier. * * @var string */ const IDENTIFIER = 'checkout'; /** * Billing address schema instance. * * @var BillingAddressSchema */ protected $billing_address_schema; /** * Shipping address schema instance. * * @var ShippingAddressSchema */ protected $shipping_address_schema; /** * Constructor. * * @param ExtendRestApi $extend Rest Extending instance. * @param BillingAddressSchema $billing_address_schema Billing address schema instance. * @param ShippingAddressSchema $shipping_address_schema Shipping address schema instance. */ public function __construct( ExtendRestApi $extend, BillingAddressSchema $billing_address_schema, ShippingAddressSchema $shipping_address_schema ) { $this->billing_address_schema = $billing_address_schema; $this->shipping_address_schema = $shipping_address_schema; parent::__construct( $extend ); } /** * Checkout schema properties. * * @return array */ public function get_properties() { return [ 'order_id' => [ 'description' => __( 'The order ID to process during checkout.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'status' => [ 'description' => __( 'Order status. Payment providers will update this value after payment.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'order_key' => [ 'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'customer_note' => [ 'description' => __( 'Note added to the order by the customer during checkout.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], ], 'customer_id' => [ 'description' => __( 'Customer ID if registered. Will return 0 for guests.', 'woocommerce' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], 'billing_address' => [ 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'properties' => $this->billing_address_schema->get_properties(), 'arg_options' => [ 'sanitize_callback' => [ $this->billing_address_schema, 'sanitize_callback' ], 'validate_callback' => [ $this->billing_address_schema, 'validate_callback' ], ], 'required' => true, ], 'shipping_address' => [ 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'properties' => $this->shipping_address_schema->get_properties(), 'arg_options' => [ 'sanitize_callback' => [ $this->shipping_address_schema, 'sanitize_callback' ], 'validate_callback' => [ $this->shipping_address_schema, 'validate_callback' ], ], 'required' => true, ], 'payment_method' => [ 'description' => __( 'The ID of the payment method being used to process the payment.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'enum' => wc()->payment_gateways->get_payment_gateway_ids(), ], 'create_account' => [ 'description' => __( 'Whether to create a new user account as part of order processing.', 'woocommerce' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], ], 'payment_result' => [ 'description' => __( 'Result of payment processing, or false if not yet processed.', 'woocommerce' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => [ 'payment_status' => [ 'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woocommerce' ), 'readonly' => true, 'type' => 'string', ], 'payment_details' => [ 'description' => __( 'An array of data being returned from the payment gateway.', 'woocommerce' ), 'readonly' => true, 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'key' => [ 'type' => 'string', ], 'value' => [ 'type' => 'string', ], ], ], ], 'redirect_url' => [ 'description' => __( 'A URL to redirect the customer after checkout. This could be, for example, a link to the payment processors website.', 'woocommerce' ), 'readonly' => true, 'type' => 'string', ], ], ], self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), ]; } /** * Return the response for checkout. * * @param object $item Results from checkout action. * @return array */ public function get_item_response( $item ) { return $this->get_checkout_response( $item->order, $item->payment_result ); } /** * Get the checkout response based on the current order and any payments. * * @param \WC_Order $order Order object. * @param PaymentResult $payment_result Payment result object. * @return array */ protected function get_checkout_response( \WC_Order $order, PaymentResult $payment_result = null ) { return [ 'order_id' => $order->get_id(), 'status' => $order->get_status(), 'order_key' => $order->get_order_key(), 'customer_note' => $order->get_customer_note(), 'customer_id' => $order->get_customer_id(), 'billing_address' => $this->billing_address_schema->get_item_response( $order ), 'shipping_address' => $this->shipping_address_schema->get_item_response( $order ), 'payment_method' => $order->get_payment_method(), 'payment_result' => [ 'payment_status' => $payment_result->status, 'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ), 'redirect_url' => $payment_result->redirect_url, ], self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), ]; } /** * This prepares the payment details for the response so it's following the * schema where it's an array of objects. * * @param array $payment_details An array of payment details from the processed payment. * * @return array An array of objects where each object has the key and value * as distinct properties. */ protected function prepare_payment_details_for_response( array $payment_details ) { return array_map( function( $key, $value ) { return (object) [ 'key' => $key, 'value' => $value, ]; }, array_keys( $payment_details ), $payment_details ); } } packages/woocommerce-blocks/global.d.ts 0000644 00000000171 15132754524 0014155 0 ustar 00 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore declare let __webpack_public_path__: string; packages/woocommerce-blocks/LICENSE 0000644 00000104515 15132754524 0013137 0 ustar 00 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright (C) <year> <name of author> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/licenses/why-not-lgpl.html>. packages/woocommerce-blocks/images/payment-methods/unionpay.svg 0000644 00000000754 15132754524 0021100 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" viewBox="0 0 42 16"> <path fill="#E21836" d="M13.8 1h5.8c.8 0 1.3.7 1 1.5L18 14a2 2 0 01-1.8 1.5h-5.8c-.8 0-1.3-.6-1.1-1.5L12 2.5A2 2 0 0113.8 1z"/> <path fill="#00447C" d="M19 1h6.7c.8 0 .5.7.3 1.5L23.3 14c-.2.9-.1 1.5-1 1.5h-6.6c-.8 0-1.3-.6-1.1-1.5l2.7-11.5A2 2 0 0119 1z"/> <path fill="#007B84" d="M25.5 1h5.8c.8 0 1.3.7 1 1.5L29.8 14a2 2 0 01-1.8 1.5H22c-.8 0-1.3-.6-1.1-1.5l2.7-11.5A2 2 0 0125.5 1z"/> </svg> packages/woocommerce-blocks/images/payment-methods/alipay.svg 0000644 00000012431 15132754524 0020510 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" preserveAspectRatio="xMidYMid meet" viewBox="0 0 750 374"> <defs/> <path d="M325.8 287.4l-7.3 1.4-7.3 2.8-5.6 4.2a10.2 10.2 0 00-2.1 5.6l1.7 4.5c1.4 2.1 4.6 2.1 4.6 2.1l5.9-1.7 2-3.9-1.3-7.7 3.8-2 5.6-.8 6.7 1.8 3.4 3.8 1.4 3.9.7 3.1v5.6l-12.2 4.2L314 318l-5.2 2.5-4.2 3.1a16.4 16.4 0 00-3.1 4.6l-1 6.2c0 6.3 4.8 10.5 4.8 10.5a19.8 19.8 0 0011.9 4.2l8.7-1.4 2.8-1 9.4-6.3a9.7 9.7 0 004.2 6c2.8 2.4 8 2.4 8 2.4l5.3-.7 5.2-1.8V343h-4.2l-3.1-1-2.8-2.1-1-3.8.7-29.3-1.8-9.5-5.2-5.9-7.4-3.1-10-.7zM338 315l-.3 20.6-6 4.9-6 2h-2.4a10.3 10.3 0 01-7-2.4c-2.8-2.4-2.8-7.3-2.8-7.3a13.2 13.2 0 011.8-6.3l5.2-4.9 7.7-3.5 9.8-3.1zm23-25.8h29.3v4.2l-4.5.7s-2.8.6-2.8 1.7l.3 1 16.1 35 15.4-34.3-2.8-2.7-5.9-1.8v-3.8H430v3.8l-4.9 1.7a13.6 13.6 0 00-4.9 5.3l-13.6 29.7-16.8 32.8-6.2 7.3-5.3 3.5-5.2.7s-4.5 0-7.7-2.4a6.3 6.3 0 01-2.8-5l1.4-4.5a7.7 7.7 0 015-2l3.4.6 2.4 1.8 3.9 4.9 8-8s6.6-10.2 4.2-18.2l-19.2-41.9-4.6-4.2-6-2.1V289m-99.8-1.4l-8.4 2.2-8.4 5.9h-.7v-6.6l-1-.7-21.7 1.4v3.8l4.2.7 3.2 1 2.4 2.5 1 3.5v62.1l-.7 3.5-2.8 2.1-3.5.7H221v4.2h36v-4.2l-9.4-1.4-2.5-2-1.4-3.6v-17.4h.7l6.3 2.4 9.4 1.4h1l10.5-2 9.4-6.7 7-9.4 2.5-13-2.1-12.2a33 33 0 00-6.3-9.8 27.5 27.5 0 00-8.4-6.3l-10.7-2zm-.4 8.8h.4a20 20 0 0113.3 7.3c4.1 5.6 3.8 14 3.8 14a25.6 25.6 0 01-4.5 14.3c-4.6 6.7-12.6 7.7-12.6 7.7-7.7.7-17.5-4.5-17.5-4.5v-34.6a42.5 42.5 0 0117.1-4.2zM95 262.3l-30.4 70.5-2.8 4.9-4.2 3.5-4.2 1.7-4.2.4v4.2h35v-4.2l-9.1-1.4c-4.6-1.4-4.6-3.9-4.6-3.9l.4-2.4 6.3-17.5h32.4l7.7 19.2.4 1.4v1.4c0 1.4-3.2 1.8-3.2 1.8l-8.3 1.4v4.2h38.4v-4.2l-3.9-.4-4.2-1.4-3.1-3.1a6.7 6.7 0 01-2.4-3.5l-30.7-72.6zm-1.8 18.5h.4l13.6 32.4H79.6zm101.3 0a8 8 0 01-5.6-2.5 7.8 7.8 0 01-2.4-5.6 9.3 9.3 0 012.4-5.5 8.9 8.9 0 015.6-2.5 9.9 9.9 0 016.3 2.5l2.1 5.2-2 6a9.5 9.5 0 01-6.4 2.4m11.5 8.7V337l1 3.5 2.8 1.7 6 1.4v3.9h-31v-3.9l5.5-.7 2.8-1.7 1-3.5v-36l-1-3.8-2.8-2.5-3.5-1-4.2-.7v-3.9l22.7-1.4.7 1M166 255.7l-22.7 1.4-1.4.3v3.5h1.4l3.5.7 4.2 1.4 2.8 2.8 1 4.2v67.8l-1 3.5-2.8 1.7-6 .7v3.8h31.5v-3.8l-3.2-.3-2.8-1-2.8-1.8-.7-3.5v-80.3l-1-1m424.3 34.2l21.6-1.4 1 1v8h.4l3.8-3.4 4.6-3.2a22.8 22.8 0 015.5-2.4l7.4-.7 10.8 2.8 6.6 7.3 3.9-3.5 4.9-3.1a23.7 23.7 0 015.6-2.8l7.6-.7s8.4 0 13 4.9c0 0 4.9 4.8 4.9 14.6V337l.6 3.5 2.5 2 3.1.8 3.5.3v3.9h-31.4v-3.9l5.6-.7 2.8-1.7.7-3.5v-30s0-6.3-2.8-9.8a11.9 11.9 0 00-7.6-3.1l-6.3.7-5.3 2.4-3.1 2.8-1.8 2.8V337l.7 3.5 2.8 2 2.8.8 3.2.3v3.9H631v-3.9l5.2-.7 2.8-1.7.7-3.5v-30s0-6.3-2.8-9.8a10.8 10.8 0 00-7.6-3.1l-6 .7-5.2 2.4-3.1 3.1-2.1 2.5V337l.7 3.5 2.8 1.7 2.8 1 3.1.4v3.8h-31v-3.8l5.9-.7 2.8-1.8.7-3.4v-36.3l-1-3.5-2.5-2.5-3.2-1-3.9-.7v-3.8m-125.6 30.7s0 11.8 5.9 18.8c0 0 5.6 7.4 15 7.4l11.5-3.2 8.7-8.7 4.2 2.8a37.3 37.3 0 01-11.8 11.5 36 36 0 01-16 4.2s-14.7 0-23.5-8.7a35.4 35.4 0 01-8.7-23.8 35 35 0 012.4-11.8 35.8 35.8 0 016.7-10.5 38.5 38.5 0 0110.5-7 40 40 0 0113.6-2.8s10.1 0 16.8 4.2c0 0 6.2 4.9 6.2 11.9l-1.7 5.6-5.2 2-6.7-1.7s-2.4-1.4-2.4-3.8l1-5.6 1.1-4.5-3.9-2.5-5.5-.7-6.3 1-6 5-4.2 8.3a43.8 43.8 0 00-1.7 12.6m84.8-31.8a39.8 39.8 0 00-24.4 9c-9.1 9.1-9.1 23.5-9.1 23.5a37 37 0 002.4 13.6l7.4 10.5a38.7 38.7 0 0010.8 6.2l12.6 2.1h.3l14.4-2.4a34.1 34.1 0 0010.8-7.3l7-10.5 2-12.9a33.4 33.4 0 00-9.4-23c-9.4-8.8-24.1-8.8-24.1-8.8h-.7zm-.2 4.9h.6l9 2 6 6.4 3.1 8.7 1 10.1c0 13-4.9 20.6-4.9 20.6a19 19 0 01-14.3 7.7h-.3l-8.8-2.4-5.9-6.3-3.5-9.1a61.3 61.3 0 01-1-11.2l.7-9.4 3.1-8.4a26 26 0 016.7-6.6 20.6 20.6 0 018.5-2.1zm-118.2 55.5a5.2 5.2 0 005.3-5.6s0-5.2-5.3-5.2c0 0-5.6 0-5.6 5.2 0 0 0 5.6 5.6 5.6" class="cls-1"/> <path d="M700.5 0v2.4h6v15.8h2V2.4h6V0zM717 0v18.2h2.5v-15l5.2 15h2.1l4.9-14.7v14.7h2.5V0h-3.2l-5.2 15.4L720.4 0z"/> <path d="M585.6 0l-9 .7S567 2.7 567 7v14l-40.8 1.4v-7.7h-18.9s-18.8 1.4-18.8 7v51H567v34.9H513v14h54.1v80l40.2-4.2v-75.9h57.6v-14h-57.6V72.6H675v-7h1.8s5.2 0 8.3-8.3l3.2-23S688 17.4 670 17.4l-62.8 2V0zm62.2 32.1a5.6 5.6 0 016 5.6L650.8 59H526.3V36l40.8-1v.3l40.2-1.7z" class="cls-2"/> <path d="M341.5 107.6l13 15 15 13.2s4.1 4.2 8.3 0l14.7-13.2 13-15s4.5-3.9 0-8.8l-13-14.6-14.6-13s-4.2-4.2-8.4 0l-14.7 13-13.3 14.6s-3.8 5 0 8.8m335.3 46a5.7 5.7 0 000-6.6l-10.1-11.2-11.2-9.7a4.7 4.7 0 00-6.7 0l-11.1 9.7-9.8 11.2s-2.8 3.5 0 6.6l9.8 11.2 11.1 9.8s3.5 3.5 6.7 0l11.1-9.8 10.2-11.2" class="cls-1"/> <path d="M77.9 90.8h119.8a166.4 166.4 0 01-12.3 30.7s-15 29.4-38 51.4c0 0-43.3 40.5-71.3 40.5 0 0-20.6 0-33.8-7.7 0 0-16.5-10.1-16.5-31 0 0 0-27.7 42.3-35.7 0 0-27.2-1.8-41.6 12.6 0 0-13.2 12.5-13.2 34.5 0 0 0 21 19.2 33.6A78.6 78.6 0 0074 230.8l5.6.4S127 228.7 171 192c0 0 37.7-31.5 56.6-76.5a187.8 187.8 0 009-28l2.1-10.8v.4h-64.6V46.4h81.4V32.9h-81.3V1.1H153l-9 .7s-9.6 2-9.6 6.2v24.8H56v13.7h78.5v30.7H78v13.6M346.4 0l-.7 6.6-4.5 16.4A94.5 94.5 0 01329 43v157.5l-40.2-4.9v-125s-15 7.7-27.9 12.6l-5.2-8.4A238.3 238.3 0 00282.9 52s26.9-26.9 26.9-45c0 0 0-4.2 9.4-6.3l9-.7h18.2" class="cls-2"/> <path d="M335.3 60.4h75.4v145.3l40.1.7v-146h21v-14h-21V0h-21.6l-9 .7s-9.5 2-9.5 6.3v39.5h-75.4v14" class="cls-2"/> <path d="M152.6 145s-51-22.4-82.4-23.5c0 0-25.8-1.4-47.1 13.6 0 0-22.4 15.8-23 39.2 0 0 0 27.2 21.6 42.6 0 0 20 14.3 52 14.3h3.2l-5.3-.4s-23.4 0-39.8-11.5c0 0-18.2-13-18.2-34.2 0 0 0-23 16.1-35 0 0 13.6-9.7 35.6-10l31.1 3.4 24.1 9.5 57.6 29.3s39.5 20.6 77.2 33.2c0 0 131.3 43.6 425 15l15.7-3.2s5.6-2.4 10.1-9.7c0 0 3.9-6 30-29.4 0 0 18.9-17 12.6-22.7l-9 1.4s-96 21.7-242.5 27.6c0 0-164.4 6.7-245.8-16.7 0 0-56.2-16.1-98.8-32.9" class="cls-1"/> </svg> packages/woocommerce-blocks/images/payment-methods/wechat.svg 0000644 00000005550 15132754524 0020510 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" preserveAspectRatio="xMidYMid meet" viewBox="0 0 750 212"> <defs> <style> .cls-1{fill:#2e3233}.cls-2{fill:#9de60b} </style> </defs> <path d="M492.4 67.2c16.2-4 36-.3 46.7 13.6a46.8 46.8 0 018.1 19.2l-21.5.1c-3-15.4-24.2-21.6-35.5-11-7.9 8.6-9.3 21.4-7.7 32.6 1.8 8.5 5.2 18.5 14 22 12.6 4.7 27.5-3.7 29.2-17.2l22 .1c-1.6 13-7.9 26.7-20.7 32-19.8 8.6-48.2 6-59.7-14.5 1.9-8.3 4-17 2-25.6-1-6.3-4.3-11.9-6.3-17.9 0-16.3 14-30.2 29.4-33.4z" class="cls-1"/> <path d="M274 69.6l23.6-.1c4.7 20.5 7.3 41.6 13 62 5.6-16.9 8.3-34.6 13.1-51.7 1.5-5.2 5.5-10.7 11.5-10.3 6.4-.8 10.8 5.2 12.1 10.9 4.7 16.8 7.7 34 12.9 50.7 6-20.2 9.2-41 13.5-61.5h23c-6.4 25.8-13.7 51.4-20.2 77.1-1.6 6.4-4.5 13.7-11.3 16.1-7.8 1.8-14.8-4.4-16.7-11.6-4.9-15.5-8.4-31.3-13.1-46.8-3.9 15.4-7.8 30.7-12 46-1.6 6.9-7.6 14-15.3 12.5-7.5-.8-11.1-8.2-12.8-14.6l-21.2-78.7z" class="cls-2"/> <path d="M552.4 69.5h21.1c0 7.8-.1 15.5.5 23.2 11.3.8 23.7-2.4 34.1 3.3 9 4.8 14.2 15 14.1 25v42l-20.5.1c-.1-13.4.2-26.8 0-40.3.2-4.5-3-9.3-7.7-9.8-6.8-.8-13.7-.4-20.5-.3v50.4h-21.1V69.7zm161.6 0h20v21h16v15.6h-16.2c.4 11.9-1 23.9.7 35.7 3.5 4.4 9.9 3.8 15 5.2l-.1 16c-11.5.2-27.2 1.7-33-10.8-5.1-14.7-1.3-30.8-2.5-46l-15.4-.1V90.7l15.6-.1-.1-21z" class="cls-1"/> <path d="M462.4 131.7c.3-13.4-2.6-28.6-14.5-36.6-15.6-10-40.5-8.6-51 8.1-8.7 14.7-9.2 35.6 2.7 48.9 11.4 12.8 31.5 13.5 46.4 7 8.2-3.3 12.7-11.3 15.8-19q-9.8-.4-19.6-.2a20 20 0 01-24.1 5.4c-5.2-2.5-6.3-8.6-7.9-13.5 17.4-.2 34.8 0 52.2-.1zm-52.7-14c2.3-7.2 8.2-13.4 16.4-12.1 8.1-1.3 13.6 5.2 15.9 12.1q-16.1.5-32.3 0z" class="cls-2"/> <path d="M695 114c-.4-12.4-11.4-22.3-23.4-23.5s-25.3-1.3-35 6.6c-5.3 3.8-7 10.3-8.3 16.2 6.1.2 12.3.2 18.4.1 3.5-10.4 17.1-11.7 25.3-6.3 3 2.6 2.8 7 3.7 10.5-12.8.5-27-2-38.3 5.4-10.8 6.4-12.8 22-6.5 32.2 4.4 6.6 13 7.8 20.3 7.8h44c-.2-16.3.3-32.7-.2-49zm-47 30c-1-3.7 1-7 2.3-10.1 8.1-1.9 16.5-2.1 24.8-2v17.2c-9-.6-20.1 2.2-27.1-5.2z" class="cls-1"/> <path d="M145 13.2a107.7 107.7 0 00-88.3-6.8 89.9 89.9 0 00-46.2 36.4A68.3 68.3 0 002 95a80.1 80.1 0 0034.7 47c-3 9-6.2 18-9 27.2 10.4-5.4 20.7-11 31.2-16.6a115.9 115.9 0 0039 5.5 67 67 0 01-2.7-31.4 67 67 0 0121.3-37.9 92 92 0 0169.1-22.7 80.7 80.7 0 00-40.6-53zM73.8 54.4c-1.8 9-13.8 13.2-20.7 7.2C45 56 47.2 41.8 56.7 39c9.3-3.6 19.9 5.8 17.1 15.4zm64.5-2.6c0 10.3-13.6 16.7-21.2 9.7-8-5.7-5.7-19.5 3.6-22.4 8.3-3.3 18.2 3.7 17.6 12.7z" class="cls-2"/> <path d="M250.6 114a70.4 70.4 0 00-32.9-33.5 90.6 90.6 0 00-81.2.3 67.5 67.5 0 00-36 44.3 57 57 0 007.2 42.2 77.6 77.6 0 0053 34.5 97.4 97.4 0 0046.1-3c9 3.5 17 9.1 25.7 13.3q-3.4-11.3-7.2-22.4c9.7-7 18.6-15.6 23.9-26.4a57.2 57.2 0 001.4-49.3zm-87.2 5.6c-2.2 7-12.3 9-17.2 3.7-5.4-4.8-3.3-15 3.8-17.2 7.8-3.3 16.8 5.7 13.4 13.5zm51 1c-2.7 6.3-12 7.7-16.6 2.9-2.3-2-2.8-5.1-3.7-7.8 1.2-4.9 4.4-10 10-10.2 7.6-1 14.3 8.2 10.3 15z" class="cls-1"/> </svg> packages/woocommerce-blocks/images/payment-methods/ideal.svg 0000644 00000001621 15132754524 0020306 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 750 661.2"> <path fill="#fff" d="M0 0v661.2h385.5c254.2 0 364.5-142.4 364.5-331.3C750 142 639.7 0 385.5 0z"/> <path d="M41.7 41.7h343.8c233.4 0 322.8 127.5 322.8 288.2 0 192.8-124.9 289.5-322.8 289.5H41.7zM75 75v511.1h310.5C573 586.1 675 498.8 675 330 675 156.4 564.4 75 385.5 75H75z"/> <path d="M116.7 367.5H216v177h-99.4z"/> <circle cx="166.3" cy="278.4" r="61.8"/> <path fill="#d50072" d="M424.5 307.3v28.6h-70.8V221h68.6v28.6h-40v13.3h37.8v28.6h-37.8v15.8zm12.4 28.6l34.7-115h40.7L547 336H517l-6.5-22.2h-37.4l-6.5 22.2zm44.7-50.8h20.7l-9.5-32.5h-1.7l-9.5 32.5zm79.8-64.2H590v86.4h42.4c-11.6-156.7-134.8-190.6-246.9-190.6H266.1V221h17.7c32.2 0 52.2 21.8 52.2 57 0 36.3-19.5 58-52.2 58H266v208.5h119.4c182.1 0 245.5-84.6 247.7-208.6h-71.8V221zM266 249.6v57.7h17.7c12.3 0 23.6-3.6 23.6-29.3 0-25.2-12.6-28.4-23.6-28.4z"/> </svg> packages/woocommerce-blocks/images/payment-methods/mastercard.svg 0000644 00000005613 15132754524 0021362 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471"> <g fill="none" fill-rule="evenodd"> <rect width="750" height="471" fill="#f4f4f4" rx="40"/> <path fill="#000" d="M618.6 422.1c-1.2 0-2.2.5-3 1.2a4 4 0 000 5.9 4.2 4.2 0 006 0 4 4 0 000-5.8 4.2 4.2 0 00-3-1.3zm0 7.4a3.2 3.2 0 110-6.4 3.2 3.2 0 010 6.4zm.2-5.2h-1.7v3.9h.8v-1.5h.3l1.2 1.5h1l-1.3-1.5c.4 0 .7-.2 1-.4.1-.2.3-.4.3-.7 0-.4-.2-.7-.4-1-.3-.2-.7-.3-1.2-.3zm0 .7c.2 0 .4 0 .5.2a.4.4 0 01.2.4.4.4 0 01-.2.3l-.5.2h-1v-1zm-458.6 2.8h-8.7v-41h8.5v5s7.6-6 12-6c8.8.2 14 7.6 14 7.6s4.2-7.6 13.7-7.6c14 0 16.1 13 16.1 13v28.8h-8.4v-25.4s0-7.8-9-7.8c-9.5 0-10.4 7.8-10.4 7.8v25.4h-8.7V402s-.9-8-8.8-8c-10.3 0-10.5 8.2-10.5 8.2zm266.3-42c-4.5 0-12 6-12 6v-5h-8.6v41h8.7l-.2-25.6s.2-8.3 10.5-8.3c2 0 3.4.5 4.6 1.2l2.8-8c-1.7-.7-3.6-1.2-5.8-1.3zm123.3 0c-4.5 0-12 6-12 6v-5h-8.6v41h8.7l-.1-25.6s.2-8.3 10.4-8.3c2 0 3.4.5 4.6 1.2l2.9-8c-1.8-.7-3.7-1.2-6-1.3zm-305.7-.1c-13 0-20 11.7-20 21.6 0 10 7.9 21.7 20.4 21.7 7.3 0 13.3-5.4 13.3-5.4v4.2h8.6v-41h-8.6v5.2s-5.7-6.3-13.7-6.3zm1.7 8.3c7 0 12.8 6.1 12.8 13.7 0 7.5-5.7 13.6-12.8 13.6-7 0-12.7-6-12.7-13.6s5.7-13.7 12.7-13.7zm249.7-8.3c-13.1 0-20 11.7-20.1 21.6 0 10 7.9 21.7 20.4 21.7 7.3 0 13.4-5.4 13.4-5.4v4.2h8.6v-41H509v5.2s-5.6-6.3-13.6-6.3zm1.6 8.3c7 0 12.8 6.1 12.8 13.7 0 7.5-5.7 13.6-12.8 13.6-7 0-12.7-6-12.7-13.6s5.7-13.7 12.7-13.7zm81.1-8.3c-13.1 0-20 11.7-20 21.6-.1 10 7.8 21.7 20.4 21.7 7.3 0 13.3-5.4 13.3-5.4v4.2h8.6v-57.1H592V392s-5.7-6.3-13.7-6.3zm1.7 8.3c7 0 12.7 6.1 12.7 13.7 0 7.5-5.7 13.6-12.7 13.6s-12.7-6-12.7-13.6 5.6-13.7 12.7-13.7zm-287.2 35.1c-8.9 0-17.1-5.5-17.1-5.5l3.7-5.8s7.8 3.6 13.4 3.6c3.7 0 9.7-1.2 9.8-4.8.1-3.9-10.2-5-10.2-5s-15.4-.2-15.4-12.9c0-8 7.7-13 17.6-13 5.6 0 16.3 5 16.3 5l-4.3 6.6s-8.2-3.2-12.6-3.4c-3.6-.1-8 1.6-8 4.8 0 8.7 25.5-.7 25.5 16.9 0 11.4-10.4 13.5-18.7 13.5zm33-54v11.8H318v8.6h7.7v20.6S325 430 339.9 430c4.2 0 12.2-3 12.2-3l-3.4-9s-3.2 2.7-6.9 2.6c-6.9-.1-6.7-4.6-6.7-4.6v-20.5h14.3v-8.6H335V375h-9.4zm51.8 11c-14 0-21 11.6-21 21.7 0 10.3 6.4 22 21.9 22 6.6 0 15.9-5.8 15.9-5.8l-4-7s-6.4 4.5-12 4.5c-11.1 0-11.8-11-11.8-11h29.9s2.2-24.3-18.9-24.3zm-1.3 8.1h1c10.6 0 10.5 10 10.5 10h-21.2s-.5-9.4 9.7-10zm90.2 22.7l4 8s-6.4 4.1-13.5 4.1c-14.7 0-23-11-23-21.6C434 391 447 386 456 386c8 0 14.8 4.7 14.8 4.7l-4.4 8s-2.8-4.3-10.7-4.3c-8 0-12.2 6.9-12.2 13.4 0 7.3 5 13.5 12.3 13.5 5.8 0 10.7-4.4 10.7-4.4z"/> <path fill="#f79f1a" d="M624.5 278.6v-5.5h-1.4l-1.7 3.8-1.6-3.8h-1.5v5.5h1v-4.1l1.6 3.6h1l1.6-3.6v4.1zm-9.1 0v-4.5h1.8v-1h-4.7v1h1.9v4.5zm9.4-82c0 85.4-69 154.6-154.3 154.6-85.2 0-154.3-69.2-154.3-154.6S385.3 41.9 470.5 41.9c85.2 0 154.3 69.2 154.3 154.7z"/> <path fill="#ea001b" d="M434.5 196.6c0 85.4-69.1 154.6-154.3 154.6-85.2 0-154.3-69.2-154.3-154.6s69-154.7 154.3-154.7c85.2 0 154.3 69.2 154.3 154.7z"/> <path fill="#ff5f01" d="M375.3 74.8c-36 28.3-59 72.3-59 121.7s23 93.5 59 121.8c36-28.3 59.1-72.3 59.1-121.8s-23-93.4-59-121.7z"/> </g> </svg> packages/woocommerce-blocks/images/payment-methods/laser.svg 0000644 00000005703 15132754524 0020343 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="-45 62.3 750 471"> <path id="Rectangle-1" d="M-5 62.3h670a40 40 0 0140 40v391a40 40 0 01-40 40H-5a40 40 0 01-40-40v-391a40 40 0 0140-40z" class="st0"/> <path d="M160.2 477.3h340v-359h-340z" class="st0"/> <path d="